STEP 1: Prep data and run introgress (For large number of loci this needs to be run on a computing cluster)
Load data
r
r Gdata<-fread(../snpData/IlluminaMix_3489/rawData/minor012.txt,sep=\t,header=T,data.table=F) Pops<-read.table(../snpData/PopInd.txt,header=T,sep=\t,stringsAsFactors = FALSE) parentals<-read.table(../snpData/summaryStats_3489/MAF_parentals.txt,header=T,sep=\t)
Due to low overall differentiation between the two species I am using, I will use a very low allele frequency difference cutoff to obtain the input loci. As states in the paper, the approach should not be biased by this. Maximising the number of loci will also help the assesment of fold enrichment.
r
r alleleFreq<-cbind(diff=abs(parentals\(LP_pure-parentals\)SWWP_pure),parentals) alleleFreq<-alleleFreq[alleleFreq$diff>0, ]
Gdata<-Gdata[ ,colnames(Gdata)%in%alleleFreq$loci] Gdata<-cbind(Pops,Gdata) Gdata<-Gdata[!(Gdata$Pop==1L), ] Pops<-Pops[!(Pops$Pop==1L), ]
loci<-as.matrix(Gdata[ ,-c(1:5)]) loci[is.na(loci)]<-/NA
loci[loci==1]<-/D
loci[loci==0]<-/A
loci[loci==2]<-/D
Define groups
r
r LP<-c(,,,,,,,,,,,2L,3H) SWWP<-c(2L,,2H,2L,1L,,,,,,,,,,,,1H,1L,3,1L,1H,2H) pop98<-read.table(../snpData/summaryStats_3489/bayenvOut/98PopIDs)
Gdata<-cbind.data.frame(Pops,loci)
P1_swwp<-Gdata[Gdata$Pop%in%SWWP, ] P2_lp<-Gdata[Gdata$Pop%in%LP, ] mixed<-Gdata[Gdata\(Pop%in%pop98\)V1, ]
Data prep for running INTROGRESS
r
r P1_swwp<-P1_swwp[ ,-c(1:5)] P2_lp<-P2_lp[ ,-c(1:5)] mixed<-cbind(Pop=mixed\(Pop,Ind=mixed\)PopInd,mixed[ ,-c(1:5)])
P1_transpose<-t(P1_swwp) P2_transpose<-t(P2_lp) mixed_transpose<-t(mixed)
Locus<-cbind(locus=alleleFreq$loci,type=rep(,nrow(alleleFreq)))
admixCount<-prepare.data(admix.gen=mixed_transpose,loci.data=Locus,parental1=P1_transpose,parental2=P2_transpose,pop.id=TRUE,ind.id=TRUE,fixed=FALSE,sep.rows = FALSE,sep.columns = FALSE)
Estimate hybrid index and interspecfic het, this is important to determine outlier loci with respect to genome wide ancestry. (takes a long time and so save the output after it is run)
r
r HIndex<-est.h(introgress.data = admixCount,loci.data = Locus,ind.touse = NULL,fixed = FALSE) head(HIndex) #write.table(cbind(HIndex,mixed[ ,1:2]),file=98Pops.txt,row.names=F,quote=F,sep=\t)
int.het<-calc.intersp.het(introgress.data=admixCount)
Now, conduct genomic cline analysis using the parametric approach since the SNPs used don’t exhibit fixed differences (Takes a long time and needs to be run on the cluster)
r
r Gclines_para<-genomic.clines(introgress.data = admixCount,hi.index = HIndex,loci.data = Locus, method=,sig.test=TRUE,loci.touse=NULL,ind.touse=NULL)
Write output files
r
r write.table(Gclines_para\(Summary.data, file=\Summ_para1000.txt\,quote=FALSE, sep=\\t\) write.table(Gclines_para\)Fitted.AA,file=1_para1000.txt,sep=\t,quote=F) write.table(Gclines_para\(Fitted.aa,file=\HomoP2_para1000.txt\,sep=\\t\,quote=F) write.table(Gclines_para\)Neutral.AA,file=1_CIpara1000.txt,sep=\t,quote=F) write.table(Gclines_para\(Neutral.aa,file=\HomoP2_CIpara1000.txt\,sep=\\t\,quote=F) write.table(Gclines_para\)Quantiles,file=_para1000.txt,sep=\t,quote=F)
The parametric approach is prone to false positives, specifically due to the large number of tests done, we will conduct a P.val correction and use only loci that pass p val threshold.
r
r Summ<-read.table(../snpData/summaryStats_3489/introgress/all1000reps/Summ_para1000.txt,header=T,sep=\t)
num<-0.05/nrow(Summ) #Bonferonni correction SummBH<-Summ[Summ$P.value<num, ]
Across all individuals we will only retain loci that have passed the Bonferonni correction. Since we have estimates for each individual, we will then classify the loci within an individual as having excess ancestry from LP using the CI estimates.
r
r LPci<-fread(../snpData/summaryStats_3489/introgress/allLoci/HomoP2_CIpara1000.txt,sep=\t,data.table=F) LPout<-fread(../snpData/summaryStats_3489/introgress/allLoci/HomoP2_para1000.txt,sep=\t,data.table=F)
LP_upper<-LPci[ ,1:ncol(LPout)]
ExcessLP<-matrix(nrow = nrow(LPout),ncol=950)
for (c in 2:ncol(LPout)){ n<-c-1 for (r in 1:nrow(LPout)){ ExcessLP[r,n]<-ifelse(LPout[r,c]>LP_upper[r,c],1,0) } }
rownames(ExcessLP)<-LPout\(V1 colnames(ExcessLP)<-colnames(LPout)[2:ncol(LPout)] ExcessLP_out<-ExcessLP[rownames(ExcessLP)%in%SummBH\)locus, ]
After assigning introgressed loci as 1 and non-introgressed as 0, we can add them across all individuals to determine what proportion of times it was introgressed across the hybrid zone. We can then use a cutoff (mostly arbitary) to classify a loci as significantly introgressed. This arbitary cutoff for here is set as 0.2 (20% of individiuals) and was assessed through a series of cutoffs being subjected to downstream analysis, all of which gave similar results.
r
r ExcessLP_out<-apply(ExcessLP,1,function(X) return(sum(X)/length(X))) ExcessLP_out2<-ExcessLP_out[ExcessLP_out>0.20]
STEP 2: Determining whether the introgressed loci are adaptive.
Here we will utilize the outliers identified through GEA (bayenv in my case) and intersect them with the introgressed loci. Since the number of outliers loci vary across environmental variables, we will utilize a permutation approach to assess the significance of fold enrichment.
Load GEA outliers SNPs
r
r clim<-list.files(../snpData/summaryStats_3489/bayenvOut/clim/convergence/,full.names = T,pattern=) Soil<-list.files(../snpData/summaryStats_3489/bayenvOut/Soil/convergence/,full.names = T,pattern=) outliers<-c(clim,Soil)
filesBF<-vector(,length(outliers))
for (f in 1:length(filesBF)){
df<-read.table(outliers[f],header=T,sep=\t) df<-df[ ,1:5] filesBF[[f]]<-df }
files<-list.files(../snpData/summaryStats_3489/bayenvOut/clim/convergence/,pattern=) files<-c(files,list.files(../snpData/summaryStats_3489/bayenvOut/Soil/convergence/,pattern=)) varID<-sapply(strsplit(files,1_3chainConvg),[,2) varID<-sapply(strsplit(varID,.txt),[,1)
names(filesBF)<-varID remove<-c( ,,) outliers<-filesBF[!(names(filesBF)%in%remove)]
Some simple manipulation to reatain only the SNPs used in INTROGRESS
r
r outliers2<-lapply(outliers,function(df) df[df\(loci%in%LPout\)V1, ]) #NULL SET BF_gClines<-lapply(outliers2,function(df) return(df[df$loci%in%names(ExcessLP_out2), ])) #SHARED SET BF_gClinesID<-unlist(lapply(BF_gClines,function(df) nrow(df)))
Estimating fold change for each environmental variable
r
r Nr<-BF_gClinesID/unlist(lapply(outliers2,function(df) nrow(df))) Dn<-length(ExcessLP_out2)/nrow(LPout)
FC<-Nr/Dn
Function for randomization to obtain null distribution of FC
Takes 4 inputs:
df
is a dataframe of bayenv outliers, where nrow = total outliers loci
is a character vector of IDs of all snps used in introgress analysis Int
total number of loci identified as significantly introgressed from LP in STEP 1 R
number of bootstrap replicates to run
r
r randFC<-function(df,loci,Int,R){
boot<-NULL for(i in 1:R){
BFrand<-sample(loci,nrow(df),replace = F)
PFrand<-sample(loci,Int,replace = F)
shared<-length(PFrand[PFrand%in%BFrand])
boot[i]<-(shared/length(BFrand))/(length(PFrand)/length(loci))
}
return(boot) }
The actual test
r
r ID<-LPout$V1 Int<-length(ExcessLP_out2)
FCrandOut<-lapply(outliers2,function(df) return(randFC(df,ID,Int,10000)))
Check which variables have observed FC outside the 99th percentile of null
r
r
for (i in 1:length(FCrandOut)){ Env<-names(FCrandOut)[i] emp<-FCrandOut[[Env]] if (quantile(emp,probs = c(0.999))<=FC[[Env]]){ cat(,Env,significantly enriched,\n) }
}
LS0tDQp0aXRsZTogIklOVFJPR1JFU1MgYW5kIEZvbGQgRW5yaWNobWVudCBhbmFseXNpcyINCm91dHB1dDogaHRtbF9ub3RlYm9vaw0KLS0tDQoNCiNCQUNLR1JPVU5EIw0KIypPdmVyYWxsIGdvYWwgaXMgdG8gaWRlbnRpZnkgYWRhcHRpdmVseSBpbnRyb2dyZXNzZWQgbG9jaSBhbmQgZGV0ZXJtaW5lIHRoZSBtYWpvciBlbnZpcm9ubWVudGFsIGF4aXMgYXNzb2NpYXRlZCB3aXRoIGFkYXB0aXZlIGludHJvZ3Jlc3Npb24qIw0KDQpIZXJlIEkgdXRpbGl6ZSB0aGUgcmVncmVzc2lvbiBhcHByb2FjaCBpbXBsZW1lbnRlZCB3aXRoaW4gYElOVFJPR1JFU1NgIHRvIGZpcnN0IGlkZW50aWZ5IGxvY2kgZXhoaWJpdGluZyBleGNlcHRpb25hbCBwYXR0ZXJucyBvZiBpbnRyb2dyZXNzaW9uLiBUaGVzZSB3aWxsIHRoZW4gYmUgc3ViamVjdGVkIHRvIGEgZm9sZCBlbnJpY2htZW50IGFwcHJvYWNoIHRvIGRldGVybWluZSBpZiBsb2NpIGFzc29jaWF0ZWQgd2l0aCBhIGdpdmVuIGVudmlyb25tZW50YWwgdmFyaWFibGUgYXJlIG1vcmUgbGlrZWx5IHRvIGJlIGNhbmRpZGF0ZXMgZm9yIGFkYXB0aXZlIGludHJvZ3Jlc3Npb24gdGhhbiB0aG9zZSBhc3NvY2lhdGVkIHdpdGggb3RoZXIgZW52aXJvbm1lbnRhbCB2YXJpYWJsZXMuIFdlIHdpbGwgZm9jdXNzaW5nIHByaW1hcmlseSBvbiBlbnZpcm9ubWVudGFsIHZhcmlhYmxlcyB0aGF0IGFyZSBzdHJvbmdseSBkaXN0aW5ndWlzaGVkIGJldHdlZW4gdGhlIHR3byBoeWJyaWRpemluZyBzcGVjaWVzLCBvciB0aG9zZSBkcml2aW5nIHRoZSBuaWNoZXMgb2YgdGhlIHR3byBzcGVjaWVzIChpZiB5b3UgaGF2ZSBwcmlvciBpbmZvcm1hdGlvbiBvbiB0aGF0KS4gDQoNCmBgYHtyfQ0KbGlicmFyeShkYXRhLnRhYmxlKQ0KbGlicmFyeShpbnRyb2dyZXNzKQ0KYGBgDQoNCiNTVEVQIDE6IFByZXAgZGF0YSBhbmQgcnVuIGludHJvZ3Jlc3MgKCpGb3IgbGFyZ2UgbnVtYmVyIG9mIGxvY2kgdGhpcyBuZWVkcyB0byBiZSBydW4gb24gYSBjb21wdXRpbmcgY2x1c3RlciopDQoNCkxvYWQgZGF0YQ0KYGBge3J9DQpHZGF0YTwtZnJlYWQoIi4uL3NucERhdGEvSWxsdW1pbmFNaXhfMzQ4OS9yYXdEYXRhL21pbm9yMDEyLnR4dCIsc2VwPSJcdCIsaGVhZGVyPVQsZGF0YS50YWJsZT1GKQ0KUG9wczwtcmVhZC50YWJsZSgiLi4vc25wRGF0YS9Qb3BJbmQudHh0IixoZWFkZXI9VCxzZXA9Ilx0IixzdHJpbmdzQXNGYWN0b3JzID0gRkFMU0UpDQpwYXJlbnRhbHM8LXJlYWQudGFibGUoIi4uL3NucERhdGEvc3VtbWFyeVN0YXRzXzM0ODkvTUFGX3BhcmVudGFscy50eHQiLGhlYWRlcj1ULHNlcD0iXHQiKQ0KYGBgDQoNCkR1ZSB0byBsb3cgb3ZlcmFsbCBkaWZmZXJlbnRpYXRpb24gYmV0d2VlbiB0aGUgdHdvIHNwZWNpZXMgSSBhbSB1c2luZywgSSB3aWxsIHVzZSBhIHZlcnkgbG93IGFsbGVsZSBmcmVxdWVuY3kgZGlmZmVyZW5jZSBjdXRvZmYgdG8gb2J0YWluIHRoZSBpbnB1dCBsb2NpLiBBcyBzdGF0ZXMgaW4gdGhlIHBhcGVyLCB0aGUgYXBwcm9hY2ggc2hvdWxkIG5vdCBiZSBiaWFzZWQgYnkgdGhpcy4gTWF4aW1pc2luZyB0aGUgbnVtYmVyIG9mIGxvY2kgd2lsbCBhbHNvIGhlbHAgdGhlIGFzc2VzbWVudCBvZiBmb2xkIGVucmljaG1lbnQuDQpgYGB7cn0NCmFsbGVsZUZyZXE8LWNiaW5kKGRpZmY9YWJzKHBhcmVudGFscyRMUF9wdXJlLXBhcmVudGFscyRTV1dQX3B1cmUpLHBhcmVudGFscykNCmFsbGVsZUZyZXE8LWFsbGVsZUZyZXFbYWxsZWxlRnJlcSRkaWZmPjAsIF0gDQoNCkdkYXRhPC1HZGF0YVsgLGNvbG5hbWVzKEdkYXRhKSVpbiVhbGxlbGVGcmVxJGxvY2ldDQpHZGF0YTwtY2JpbmQoUG9wcyxHZGF0YSkNCkdkYXRhPC1HZGF0YVshKEdkYXRhJFBvcD09Ik1FU0MxTCIpLCBdDQpQb3BzPC1Qb3BzWyEoUG9wcyRQb3A9PSJNRVNDMUwiKSwgXQ0KDQpsb2NpPC1hcy5tYXRyaXgoR2RhdGFbICwtYygxOjUpXSkNCmxvY2lbaXMubmEobG9jaSldPC0iTkEvTkEiDQpsb2NpW2xvY2k9PTFdPC0iQS9EIg0KbG9jaVtsb2NpPT0wXTwtIkEvQSINCmxvY2lbbG9jaT09Ml08LSJEL0QiDQpgYGANCg0KRGVmaW5lIGdyb3Vwcw0KYGBge3J9DQpMUDwtYygiQkNLIiwiQlUiLCJDSCIsIkNQIiwiSE8iLCJKRU4iLCJKViIsIkxQIiwiTUVMIiwiTVNMIiwiVkMiLCJDT0xPMkwiLCJDT0xPM0giKQ0KU1dXUDwtYygiREFWMkwiLCJBTFQiLCJIVUEySCIsIkhVQTJMIiwiU0FOUklUMUwiLCJCQUwiLCJCQVMiLCJDSEkiLCJFUEEiLCJFUk0iLCJHVUEiLCJHVUkiLCJTQU0iLCJUT00iLCJWRVIiLCJZQVEiLCJDSEkxSCIsIkNISTFMIiwiQ0hJMyIsIkRBVjFMIiwiREFWMUgiLCJEQVYySCIpDQpwb3A5ODwtcmVhZC50YWJsZSgiLi4vc25wRGF0YS9zdW1tYXJ5U3RhdHNfMzQ4OS9iYXllbnZPdXQvOThQb3BJRHMiKQ0KDQpHZGF0YTwtY2JpbmQuZGF0YS5mcmFtZShQb3BzLGxvY2kpDQoNClAxX3N3d3A8LUdkYXRhW0dkYXRhJFBvcCVpbiVTV1dQLCBdDQpQMl9scDwtR2RhdGFbR2RhdGEkUG9wJWluJUxQLCBdDQptaXhlZDwtR2RhdGFbR2RhdGEkUG9wJWluJXBvcDk4JFYxLCBdDQpgYGANCg0KRGF0YSBwcmVwIGZvciBydW5uaW5nIElOVFJPR1JFU1MNCmBgYHtyfQ0KUDFfc3d3cDwtUDFfc3d3cFsgLC1jKDE6NSldDQpQMl9scDwtUDJfbHBbICwtYygxOjUpXQ0KbWl4ZWQ8LWNiaW5kKFBvcD1taXhlZCRQb3AsSW5kPW1peGVkJFBvcEluZCxtaXhlZFsgLC1jKDE6NSldKQ0KDQpQMV90cmFuc3Bvc2U8LXQoUDFfc3d3cCkNClAyX3RyYW5zcG9zZTwtdChQMl9scCkNCm1peGVkX3RyYW5zcG9zZTwtdChtaXhlZCkNCg0KDQpMb2N1czwtY2JpbmQobG9jdXM9YWxsZWxlRnJlcSRsb2NpLHR5cGU9cmVwKCJDIixucm93KGFsbGVsZUZyZXEpKSkNCg0KYWRtaXhDb3VudDwtcHJlcGFyZS5kYXRhKGFkbWl4Lmdlbj1taXhlZF90cmFuc3Bvc2UsbG9jaS5kYXRhPUxvY3VzLHBhcmVudGFsMT1QMV90cmFuc3Bvc2UscGFyZW50YWwyPVAyX3RyYW5zcG9zZSxwb3AuaWQ9VFJVRSxpbmQuaWQ9VFJVRSxmaXhlZD1GQUxTRSxzZXAucm93cyA9IEZBTFNFLHNlcC5jb2x1bW5zID0gRkFMU0UpDQpgYGANCg0KRXN0aW1hdGUgaHlicmlkIGluZGV4IGFuZCBpbnRlcnNwZWNmaWMgaGV0LCB0aGlzIGlzIGltcG9ydGFudCB0byBkZXRlcm1pbmUgb3V0bGllciBsb2NpIHdpdGggcmVzcGVjdCB0byBnZW5vbWUgd2lkZSBhbmNlc3RyeS4gKCp0YWtlcyBhIGxvbmcgdGltZSBhbmQgc28gc2F2ZSB0aGUgb3V0cHV0IGFmdGVyIGl0IGlzIHJ1biopDQpgYGB7cn0NCkhJbmRleDwtZXN0LmgoaW50cm9ncmVzcy5kYXRhID0gYWRtaXhDb3VudCxsb2NpLmRhdGEgPSBMb2N1cyxpbmQudG91c2UgPSBOVUxMLGZpeGVkID0gRkFMU0UpDQpoZWFkKEhJbmRleCkNCiN3cml0ZS50YWJsZShjYmluZChISW5kZXgsbWl4ZWRbICwxOjJdKSxmaWxlPSJIeWJyaWRJbmRleDk4UG9wcy50eHQiLHJvdy5uYW1lcz1GLHF1b3RlPUYsc2VwPSJcdCIpDQoNCmludC5oZXQ8LWNhbGMuaW50ZXJzcC5oZXQoaW50cm9ncmVzcy5kYXRhPWFkbWl4Q291bnQpDQpgYGANCg0KTm93LCBjb25kdWN0IGdlbm9taWMgY2xpbmUgYW5hbHlzaXMgdXNpbmcgdGhlIHBhcmFtZXRyaWMgYXBwcm9hY2ggc2luY2UgdGhlIFNOUHMgdXNlZCBkb24ndCBleGhpYml0IGZpeGVkIGRpZmZlcmVuY2VzICgqVGFrZXMgYSBsb25nIHRpbWUgYW5kIG5lZWRzIHRvIGJlIHJ1biBvbiB0aGUgY2x1c3RlciopDQoNCmBgYHtyfQ0KR2NsaW5lc19wYXJhPC1nZW5vbWljLmNsaW5lcyhpbnRyb2dyZXNzLmRhdGEgPSBhZG1peENvdW50LGhpLmluZGV4ID0gSEluZGV4LGxvY2kuZGF0YSA9IExvY3VzLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1ldGhvZD0icGFyYW1ldHJpYyIsc2lnLnRlc3Q9VFJVRSxsb2NpLnRvdXNlPU5VTEwsaW5kLnRvdXNlPU5VTEwpDQpgYGANCg0KV3JpdGUgb3V0cHV0IGZpbGVzIA0KYGBge3J9DQp3cml0ZS50YWJsZShHY2xpbmVzX3BhcmEkU3VtbWFyeS5kYXRhLCBmaWxlPSJTdW1tX3BhcmExMDAwLnR4dCIscXVvdGU9RkFMU0UsIHNlcD0iXHQiKQ0Kd3JpdGUudGFibGUoR2NsaW5lc19wYXJhJEZpdHRlZC5BQSxmaWxlPSJIb21vUDFfcGFyYTEwMDAudHh0IixzZXA9Ilx0IixxdW90ZT1GKQ0Kd3JpdGUudGFibGUoR2NsaW5lc19wYXJhJEZpdHRlZC5hYSxmaWxlPSJIb21vUDJfcGFyYTEwMDAudHh0IixzZXA9Ilx0IixxdW90ZT1GKQ0Kd3JpdGUudGFibGUoR2NsaW5lc19wYXJhJE5ldXRyYWwuQUEsZmlsZT0iSG9tb1AxX0NJcGFyYTEwMDAudHh0IixzZXA9Ilx0IixxdW90ZT1GKQ0Kd3JpdGUudGFibGUoR2NsaW5lc19wYXJhJE5ldXRyYWwuYWEsZmlsZT0iSG9tb1AyX0NJcGFyYTEwMDAudHh0IixzZXA9Ilx0IixxdW90ZT1GKQ0Kd3JpdGUudGFibGUoR2NsaW5lc19wYXJhJFF1YW50aWxlcyxmaWxlPSJRdWFudGlsZV9wYXJhMTAwMC50eHQiLHNlcD0iXHQiLHF1b3RlPUYpDQpgYGANCg0KVGhlIHBhcmFtZXRyaWMgYXBwcm9hY2ggaXMgcHJvbmUgdG8gZmFsc2UgcG9zaXRpdmVzLCBzcGVjaWZpY2FsbHkgZHVlIHRvIHRoZSBsYXJnZSBudW1iZXIgb2YgdGVzdHMgZG9uZSwgd2Ugd2lsbCBjb25kdWN0IGEgUC52YWwgY29ycmVjdGlvbiBhbmQgdXNlIG9ubHkgbG9jaSB0aGF0IHBhc3MgcCB2YWwgdGhyZXNob2xkLg0KYGBge3J9DQpTdW1tPC1yZWFkLnRhYmxlKCIuLi9zbnBEYXRhL3N1bW1hcnlTdGF0c18zNDg5L2ludHJvZ3Jlc3MvYWxsMTAwMHJlcHMvU3VtbV9wYXJhMTAwMC50eHQiLGhlYWRlcj1ULHNlcD0iXHQiKQ0KDQpudW08LTAuMDUvbnJvdyhTdW1tKSAjQm9uZmVyb25uaSBjb3JyZWN0aW9uDQpTdW1tQkg8LVN1bW1bU3VtbSRQLnZhbHVlPG51bSwgXQ0KYGBgDQoNCkFjcm9zcyBhbGwgaW5kaXZpZHVhbHMgd2Ugd2lsbCBvbmx5IHJldGFpbiBsb2NpIHRoYXQgaGF2ZSBwYXNzZWQgdGhlIEJvbmZlcm9ubmkgY29ycmVjdGlvbi4gU2luY2Ugd2UgaGF2ZSBlc3RpbWF0ZXMgZm9yIGVhY2ggaW5kaXZpZHVhbCwgd2Ugd2lsbCB0aGVuIGNsYXNzaWZ5IHRoZSBsb2NpIHdpdGhpbiBhbiBpbmRpdmlkdWFsIGFzIGhhdmluZyBleGNlc3MgYW5jZXN0cnkgZnJvbSBMUCB1c2luZyB0aGUgQ0kgZXN0aW1hdGVzLg0KYGBge3J9DQpMUGNpPC1mcmVhZCgiLi4vc25wRGF0YS9zdW1tYXJ5U3RhdHNfMzQ4OS9pbnRyb2dyZXNzL2FsbExvY2kvSG9tb1AyX0NJcGFyYTEwMDAudHh0IixzZXA9Ilx0IixkYXRhLnRhYmxlPUYpDQpMUG91dDwtZnJlYWQoIi4uL3NucERhdGEvc3VtbWFyeVN0YXRzXzM0ODkvaW50cm9ncmVzcy9hbGxMb2NpL0hvbW9QMl9wYXJhMTAwMC50eHQiLHNlcD0iXHQiLGRhdGEudGFibGU9RikNCg0KTFBfdXBwZXI8LUxQY2lbICwxOm5jb2woTFBvdXQpXQ0KDQoNCkV4Y2Vzc0xQPC1tYXRyaXgobnJvdyA9IG5yb3coTFBvdXQpLG5jb2w9OTUwKQ0KDQpmb3IgKGMgaW4gMjpuY29sKExQb3V0KSl7DQogIG48LWMtMQ0KICBmb3IgKHIgaW4gMTpucm93KExQb3V0KSl7DQogICAgRXhjZXNzTFBbcixuXTwtaWZlbHNlKExQb3V0W3IsY10+TFBfdXBwZXJbcixjXSwxLDApDQogIH0NCn0NCg0Kcm93bmFtZXMoRXhjZXNzTFApPC1MUG91dCRWMQ0KY29sbmFtZXMoRXhjZXNzTFApPC1jb2xuYW1lcyhMUG91dClbMjpuY29sKExQb3V0KV0NCkV4Y2Vzc0xQX291dDwtRXhjZXNzTFBbcm93bmFtZXMoRXhjZXNzTFApJWluJVN1bW1CSCRsb2N1cywgXQ0KDQpgYGANCg0KQWZ0ZXIgYXNzaWduaW5nIGludHJvZ3Jlc3NlZCBsb2NpIGFzIDEgYW5kIG5vbi1pbnRyb2dyZXNzZWQgYXMgMCwgd2UgY2FuIGFkZCB0aGVtIGFjcm9zcyBhbGwgaW5kaXZpZHVhbHMgdG8gZGV0ZXJtaW5lIHdoYXQgcHJvcG9ydGlvbiBvZiB0aW1lcyBpdCB3YXMgaW50cm9ncmVzc2VkIGFjcm9zcyB0aGUgaHlicmlkIHpvbmUuIFdlIGNhbiB0aGVuIHVzZSBhIGN1dG9mZiAobW9zdGx5IGFyYml0YXJ5KSB0byBjbGFzc2lmeSBhIGxvY2kgYXMgc2lnbmlmaWNhbnRseSBpbnRyb2dyZXNzZWQuIFRoaXMgYXJiaXRhcnkgY3V0b2ZmIGZvciBoZXJlIGlzIHNldCBhcyAwLjIgKDIwJSBvZiBpbmRpdmlkaXVhbHMpIGFuZCB3YXMgYXNzZXNzZWQgdGhyb3VnaCBhIHNlcmllcyBvZiBjdXRvZmZzIGJlaW5nIHN1YmplY3RlZCB0byBkb3duc3RyZWFtIGFuYWx5c2lzLCBhbGwgb2Ygd2hpY2ggZ2F2ZSBzaW1pbGFyIHJlc3VsdHMuDQpgYGB7cn0NCkV4Y2Vzc0xQX291dDwtYXBwbHkoRXhjZXNzTFAsMSxmdW5jdGlvbihYKSByZXR1cm4oc3VtKFgpL2xlbmd0aChYKSkpDQpFeGNlc3NMUF9vdXQyPC1FeGNlc3NMUF9vdXRbRXhjZXNzTFBfb3V0PjAuMjBdDQpgYGANCg0KI1NURVAgMjogRGV0ZXJtaW5pbmcgd2hldGhlciB0aGUgaW50cm9ncmVzc2VkIGxvY2kgYXJlIGFkYXB0aXZlLg0KSGVyZSB3ZSB3aWxsIHV0aWxpemUgdGhlIG91dGxpZXJzIGlkZW50aWZpZWQgdGhyb3VnaCBHRUEgKGJheWVudiBpbiBteSBjYXNlKSBhbmQgaW50ZXJzZWN0IHRoZW0gd2l0aCB0aGUgaW50cm9ncmVzc2VkIGxvY2kuIFNpbmNlIHRoZSBudW1iZXIgb2Ygb3V0bGllcnMgbG9jaSB2YXJ5IGFjcm9zcyBlbnZpcm9ubWVudGFsIHZhcmlhYmxlcywgd2Ugd2lsbCB1dGlsaXplIGEgcGVybXV0YXRpb24gYXBwcm9hY2ggdG8gYXNzZXNzIHRoZSBzaWduaWZpY2FuY2Ugb2YgZm9sZCBlbnJpY2htZW50Lg0KDQpMb2FkIEdFQSBvdXRsaWVycyBTTlBzDQpgYGB7cn0NCmNsaW08LWxpc3QuZmlsZXMoIi4uL3NucERhdGEvc3VtbWFyeVN0YXRzXzM0ODkvYmF5ZW52T3V0L2NsaW0vY29udmVyZ2VuY2UvIixmdWxsLm5hbWVzID0gVCxwYXR0ZXJuPSJPdmVybGFwIikNClNvaWw8LWxpc3QuZmlsZXMoIi4uL3NucERhdGEvc3VtbWFyeVN0YXRzXzM0ODkvYmF5ZW52T3V0L1NvaWwvY29udmVyZ2VuY2UvIixmdWxsLm5hbWVzID0gVCxwYXR0ZXJuPSJPdmVybGFwIikNCm91dGxpZXJzPC1jKGNsaW0sU29pbCkNCg0KDQpmaWxlc0JGPC12ZWN0b3IoImxpc3QiLGxlbmd0aChvdXRsaWVycykpDQoNCmZvciAoZiBpbiAxOmxlbmd0aChmaWxlc0JGKSl7DQogIA0KICBkZjwtcmVhZC50YWJsZShvdXRsaWVyc1tmXSxoZWFkZXI9VCxzZXA9Ilx0IikNCiAgZGY8LWRmWyAsMTo1XQ0KICBmaWxlc0JGW1tmXV08LWRmDQp9DQoNCmZpbGVzPC1saXN0LmZpbGVzKCIuLi9zbnBEYXRhL3N1bW1hcnlTdGF0c18zNDg5L2JheWVudk91dC9jbGltL2NvbnZlcmdlbmNlLyIscGF0dGVybj0iT3ZlcmxhcCIpDQpmaWxlczwtYyhmaWxlcyxsaXN0LmZpbGVzKCIuLi9zbnBEYXRhL3N1bW1hcnlTdGF0c18zNDg5L2JheWVudk91dC9Tb2lsL2NvbnZlcmdlbmNlLyIscGF0dGVybj0iT3ZlcmxhcCIpKQ0KdmFySUQ8LXNhcHBseShzdHJzcGxpdChmaWxlcywiT3ZlcmxhcDFfM2NoYWluQ29udmciKSwiWyIsMikNCnZhcklEPC1zYXBwbHkoc3Ryc3BsaXQodmFySUQsIi50eHQiKSwiWyIsMSkNCg0KDQpuYW1lcyhmaWxlc0JGKTwtdmFySUQNCnJlbW92ZTwtYygiRWxldmF0aW9uIiAsIkxhdGl0dWRlIiwiTG9uZ2l0dWRlIikNCm91dGxpZXJzPC1maWxlc0JGWyEobmFtZXMoZmlsZXNCRiklaW4lcmVtb3ZlKV0NCmBgYA0KDQpTb21lIHNpbXBsZSBtYW5pcHVsYXRpb24gdG8gcmVhdGFpbiBvbmx5IHRoZSBTTlBzIHVzZWQgaW4gYElOVFJPR1JFU1NgDQpgYGB7cn0NCm91dGxpZXJzMjwtbGFwcGx5KG91dGxpZXJzLGZ1bmN0aW9uKGRmKSBkZltkZiRsb2NpJWluJUxQb3V0JFYxLCBdKSAjTlVMTCBTRVQNCkJGX2dDbGluZXM8LWxhcHBseShvdXRsaWVyczIsZnVuY3Rpb24oZGYpIHJldHVybihkZltkZiRsb2NpJWluJW5hbWVzKEV4Y2Vzc0xQX291dDIpLCBdKSkgI1NIQVJFRCBTRVQNCkJGX2dDbGluZXNJRDwtdW5saXN0KGxhcHBseShCRl9nQ2xpbmVzLGZ1bmN0aW9uKGRmKSBucm93KGRmKSkpDQpgYGANCg0KRXN0aW1hdGluZyBmb2xkIGNoYW5nZSBmb3IgZWFjaCBlbnZpcm9ubWVudGFsIHZhcmlhYmxlDQpgYGB7cn0NCk5yPC1CRl9nQ2xpbmVzSUQvdW5saXN0KGxhcHBseShvdXRsaWVyczIsZnVuY3Rpb24oZGYpIG5yb3coZGYpKSkNCkRuPC1sZW5ndGgoRXhjZXNzTFBfb3V0MikvbnJvdyhMUG91dCkNCg0KRkM8LU5yL0RuDQpgYGANCg0KKkZ1bmN0aW9uIGZvciByYW5kb21pemF0aW9uIHRvIG9idGFpbiBudWxsIGRpc3RyaWJ1dGlvbiBvZiBGQyoNCg0KVGFrZXMgNCBpbnB1dHM6DQoNCmBkZmAgaXMgYSBkYXRhZnJhbWUgb2YgYmF5ZW52IG91dGxpZXJzLCB3aGVyZSBucm93ID0gdG90YWwgb3V0bGllcnMNCmBsb2NpYCBpcyBhIGNoYXJhY3RlciB2ZWN0b3Igb2YgSURzIG9mIGFsbCBzbnBzIHVzZWQgaW4gaW50cm9ncmVzcyBhbmFseXNpcw0KYEludGAgdG90YWwgbnVtYmVyIG9mIGxvY2kgaWRlbnRpZmllZCBhcyBzaWduaWZpY2FudGx5IGludHJvZ3Jlc3NlZCBmcm9tIExQIGluIFNURVAgMQ0KYFJgIG51bWJlciBvZiBib290c3RyYXAgcmVwbGljYXRlcyB0byBydW4NCg0KYGBge3J9DQpyYW5kRkM8LWZ1bmN0aW9uKGRmLGxvY2ksSW50LFIpew0KDQogIGJvb3Q8LU5VTEwNCiAgZm9yKGkgaW4gMTpSKXsNCiAgICANCiAgICBCRnJhbmQ8LXNhbXBsZShsb2NpLG5yb3coZGYpLHJlcGxhY2UgPSBGKQ0KICAgIFBGcmFuZDwtc2FtcGxlKGxvY2ksSW50LHJlcGxhY2UgPSBGKQ0KICAgIHNoYXJlZDwtbGVuZ3RoKFBGcmFuZFtQRnJhbmQlaW4lQkZyYW5kXSkNCiAgICANCiAgICBib290W2ldPC0oc2hhcmVkL2xlbmd0aChCRnJhbmQpKS8obGVuZ3RoKFBGcmFuZCkvbGVuZ3RoKGxvY2kpKQ0KICB9DQoNCiAgcmV0dXJuKGJvb3QpDQp9DQoNCmBgYA0KDQpUaGUgYWN0dWFsIHRlc3QNCmBgYHtyfQ0KSUQ8LUxQb3V0JFYxDQpJbnQ8LWxlbmd0aChFeGNlc3NMUF9vdXQyKQ0KDQoNCkZDcmFuZE91dDwtbGFwcGx5KG91dGxpZXJzMixmdW5jdGlvbihkZikgcmV0dXJuKHJhbmRGQyhkZixJRCxJbnQsMTAwMDApKSkNCmBgYA0KDQpDaGVjayB3aGljaCB2YXJpYWJsZXMgaGF2ZSBvYnNlcnZlZCBGQyBvdXRzaWRlIHRoZSA5OXRoIHBlcmNlbnRpbGUgb2YgbnVsbA0KYGBge3J9DQoNCmZvciAoaSBpbiAxOmxlbmd0aChGQ3JhbmRPdXQpKXsNCiAgRW52PC1uYW1lcyhGQ3JhbmRPdXQpW2ldDQogIGVtcDwtRkNyYW5kT3V0W1tFbnZdXQ0KICBpZiAocXVhbnRpbGUoZW1wLHByb2JzID0gYygwLjk5OSkpPD1GQ1tbRW52XV0pew0KICAgIGNhdCgidmFyaWFibGUiLEVudiwiaXMgc2lnbmlmaWNhbnRseSBlbnJpY2hlZCIsIlxuIikNCiAgfQ0KICANCn0NCg0KYGBgDQoNCg==