Author: Shantel A. Martinez
Purpose: Analyze the PHS data under mutliple GS models and determine which model bests fits out data.
Last updated: 2019.02.19
The majority of the tutorials I learned basics of genomic selection came from the wheat cimmyt tutorial and validation examples and for rrBLUP I started with this tutorial and the rrBLUP CRAN page.
rrBLUP
folds=5
K=A.mat(X)
y = y[,2]
test_CV_parallel<-function(x,folds){
set.seed(x)
results=c()
for(i in 1:folds){
tst <- sample(rep(1:folds,length.out=n))
yNA<-y
yNA[which(i==tst)]<-NA
ans <- mixed.solve(yNA,K=K)
yHat_1=ans$u[-tst]
results <- append(results,cor(yHat_1,y[-tst]))
}
return(results)
}
cv_results6=foreach(cores=1:5, .combine='c') %dopar% test_CV_parallel(x=cores, folds=5)
accuracy5 = as.data.frame(matrix(cv_results6))
accuracy5$model<-"rrBLUP"
names(accuracy5) <- c("PA", "model")
accuracyrr<-accuracy5
The script step by step:
Lets start from the basics within all the loops and work outwards.
Running genomic prediction for a phenotype y
while using a kinship matrix K
consists of the following commands:
K=A.mat(X)
The A.mat function in rrBLUP will calculate the addititve relationship matrix
y = y[,2]
define which phenotype you want to predict (this will leave y as just a vector of n x 1)
pred <- mixed.solve(y,K=K)
mixed.solve salculates maximum-likelihood (ML/REML) solutions for mixed models, but since you are using K
in your model, it will also calculate the GEBVs
pred$u[]
the output pred
will contain a dataframe u
that containts the GEBVs
However, this is using all 100% of your data, to calculate the GEBVs, so since you arent training on a subset of the data to predict another subset, the prediction accuracy of the observed variable y
to the predicted variable u
is not useful.
5-fold cross validation
To cross validate, you can divide you dataset into 5 subset, train your model on 4/5 of the subsets, and test for accuracy on the remaining 1/5 of the subset.
NOTE: There are many ways to subset your data, depending on the question you are asking for your project and also depending on how you wish to write your script. Below is how I initially learned to subset for 5-folds based on the CIMMYT tutorials, but it is not the only way.
Lets start with a for loop, for i
from 1 to folds (and we can define as folds=5
for tutorial purposes):
for(i in 1:folds){
}
Within that loop, we want to subset the dataset:
tst <- sample(rep(1:folds,length.out=n))
create a vector tst
that has the values 1:folds
(in our case 1:5) and a length
of n
which is the number of lines in X
which should be the same number of lines in y
yNA<-y
define a new vector yNA
with the same values as y
yNA[which(i==tst)]<-NA
make 1/5 of the pheno data NA, or omitted, so that later on when we calculate the mixed.solve model, we are only using 4/5 of the dataset. More specifically, this command
Now, yNA[tst]
is a vector of the training dataset while yNA[-tst]
is a vector of the testing dataset
which means you can specifically pull out the GEBVs u
from the test set -tsts
to use in your prediction accuracy (PA) calculation. Here yHat_1=ans$u[-tst]
we call the test sets GEBVs yHat_1
Predicion Accuracy
The PA of the observed variable y
to the predicted variable u
. However, since we are doing CV, we want the observed values of the test group y[-tst]
to the predicted values/GEBVs, which we just defined above as yHat_1
.
results <- append(results,cor(yHat_1,y[-tst]))
Add/append this new PA calculated to the results vector
for(i in 1:folds){
tst <- sample(rep(1:folds,length.out=n))
yNA<-y
yNA[which(i==tst)]<-NA
ans <- mixed.solve(yNA,K=K)
yHat_1=ans$u[-tst]
results <- append(results,cor(yHat_1,y[-tst]))
}
Replicate CV and Randomization
Creating a function for the rrBLUP analysis helps with replication, but also keeps your code more simple downstream. Also, I use the %dopar%
command to distribute the computation over 5 cores at a time.
Define a function called test_CV_parallel
with variable x
and folds
that need to be defined.
test_CV_parallel<-function(x,folds){
}
set.seed(x)
setting the randomization based on x
which will change through the function. As shown below, x is based on the number of cores used to run the function x=cores
and we will define cores=1:5
. So as the function is using 1, 2, 3, 4, 5 cores, the random seed will change to seed(1)
… seed(5)
. This is very useful when you have a heavy computational load.
results=c()
create an empty vector called results
return(results)
foreach command
cv_results6 = foreach(cores=1:5, .combine='c') %dopar% test_CV_parallel(x=cores, folds=5)
5 (multi) core computer; dopar will do the command in parallel whereas %do% can be used for a one core computer
And this completes the whole Genomic Prediction Analysis:
test_CV_parallel<-function(x,folds){
set.seed(x)
results=c()
for(i in 1:folds){
tst <- sample(rep(1:folds,length.out=n))
yNA<-y
yNA[which(i==tst)]<-NA
ans <- mixed.solve(yNA,K=K)
yHat_1=ans$u[-tst]
results <- append(results,cor(yHat_1,y[-tst]))
}
return(results)
}
cv_results6=foreach(cores=1:5, .combine='c') %dopar% test_CV_parallel(x=cores, folds=5)
Data Organization
The last portion is organizing the output into a workable dataframe the I can use to plot of combined with other results downstream.
accuracy5 = as.data.frame(matrix(cv_results6))
the matrix cv_results6
is made into a dataframe called aaccuracy5
accuracy5$model<-"rrBLUP"
add in a new column model
into the df and fill the column with the characters rrBLUP
names(accuracy5) <- c("PA", "model")
rename the column names to PA
and model
accuracyrr<-accuracy5
rename the df to accuracyrr
Now if you’ve already defined your function upstream, all you need to write for each run is:
cv_results6=foreach(cores=1:5, .combine='c') %dopar% test_CV_parallel(x=cores, folds=5)
accuracy5 = as.data.frame(matrix(cv_results6))
accuracy5$model<-"rrBLUP"; names(accuracy5) <- c("PA", "model")
accuracyrr<-accuracy5
Note: folds=5
does not need to be defined anymore, since it is integrated into the function as x
and folds
.
RKHS
The 5-fold cross validation subsetting and data organization is all the same as explained in the rrBLUP section, however the model within the CV is now changed. An explanation of the RKHS model is below.
nIter = 12000
burnIn=2000
h <- 0.5
D<-(as.matrix(dist(X,method='euclidean'))^2)/p
U<-exp(-h*D)
test_CV_parallel<-function(x,folds){
set.seed(x)
results=c()
for(i in 1:folds){
tst <- sample(rep(1:folds,length.out=n))
yNA<-y
yNA[which(i==tst)]<-NA
ETA<-list(list(K=U,model='RKHS'))
fm<-BGLR(y=yNA,ETA=ETA,nIter=nIter, burnIn=burnIn)
yHat_1=fm$yHat[-tst]
results <- append(results,cor(yHat_1,y[-tst]))
}
return(results)
}
cv_results5=foreach(cores=1:5, .combine='c') %dopar% test_CV_parallel(x=cores, folds=5)
accuracy5 = as.data.frame(matrix(cv_results5))
accuracy5$model<-"RKHS"
names(accuracy5) <- c("PA", "model")
accuracyrk<-accuracy5
As stated in the Campos Lab document:
h is a bandwidth parameter controlling how fast the kernel decay as the two points, (xi , xi’ ), get further apart
I have attempted to determine h
with my dataset founf in this R Notebook
h <- 0.5
just for tutorial purposes, we will set h to 0.5. However I suggest reading the Campos Lab document section 4 and Campos et al., 2010 to better understand h
Distance function and reproducing kernel
D<-(as.matrix(dist(X,method='euclidean'))^2)/p
U<-exp(-h*D)
Fitting the RKHS Model
ETA<-list(list(K=U,model='RKHS'))
fm<-BGLR(y=yNA,ETA=ETA,nIter=nIter, burnIn=burnIn)
fm$yHat[-tst]
Running LASSO
The 5-fold cross validation subsetting and data organization is all the same as explained in the rrBLUP section, however the model within the CV is now changed. An explanation of the LASSO model is below.
nIter = 12000
burnIn=2000
test_CV_parallel<-function(x,folds){
set.seed(x)
results=c()
for(i in 1:folds){
tst <- sample(rep(1:folds,length.out=n))
yNA<-y
yNA[which(i==tst)]<-NA
ETA<-list(list(X=X,model='BL'))
fm<-BGLR(y=yNA, response_type = "gaussian", ETA = ETA, nIter=nIter, burnIn=burnIn)
yHat_1=fm$yHat[-tst]
results <- append(results,cor(yHat_1,y[-tst]))
}
return(results)
}
cv_results7=foreach(cores=1:5, .combine='c') %dopar% test_CV_parallel(x=cores, folds=5)
accuracy5 = as.data.frame(matrix(cv_results7))
accuracy5$model<-"LASSO"
names(accuracy5) <- c("PA", "model")
accuracyBL<-accuracy5
Fitting the RKHS Model
ETA<-list(list(X=X,model='BL'))
fm<-BGLR(y=yNA, response_type = "gaussian", ETA = ETA, nIter=nIter, burnIn=burnIn)
Plots
A simple ggplot boxplot of Prediction Accuracies (PA) across the three models
accuracy <- rbind( accuracyrk, accuracyrr, accuracyBL)
ggplot(accuracy, aes(accuracy$model, accuracy$PA, fill = factor(model))) +
geom_boxplot()+ theme_bw() +
theme(axis.text = font2, axis.title = font, strip.text.x = font, plot.title = font)+
ylab("PA") + xlab("")+ ylim(0.6, 0.9) +
ggtitle("Wheat | Model Comparison")
Remember to fix you ylim
range to (0,1)
at first, then minimize it if its hard to see the differences
LS0tDQp0aXRsZTogIkdlbm9taWMgUHJlZGljdGlvbiBNb2RlbHMiDQphdXRob3I6ICJTaGFudGVsIEEgTWFydGluZXoiDQpvdXRwdXQ6DQogIGh0bWxfZG9jdW1lbnQ6DQogICAgY29kZV9kb3dubG9hZDogeWVzDQogICAgdG9jOiB0cnVlDQogICAgdG9jX2RlcHRoOiA1DQogICAgdG9jX2Zsb2F0OiB0cnVlDQogICAgZGZfcHJpbnQ6IHBhZ2VkDQogICAgdGhlbWU6IGZsYXRseQ0KLS0tDQoNCioqQXV0aG9yOioqIFNoYW50ZWwgQS4gTWFydGluZXogIA0KKipQdXJwb3NlOioqIEFuYWx5emUgdGhlIFBIUyBkYXRhIHVuZGVyIG11dGxpcGxlIEdTIG1vZGVscyBhbmQgZGV0ZXJtaW5lIHdoaWNoIG1vZGVsIGJlc3RzIGZpdHMgb3V0IGRhdGEuICAgDQoqKkxhc3QgdXBkYXRlZDoqKiAyMDE5LjAyLjE5ICAgDQoNClRoZSBtYWpvcml0eSBvZiB0aGUgdHV0b3JpYWxzIEkgbGVhcm5lZCBiYXNpY3Mgb2YgZ2Vub21pYyBzZWxlY3Rpb24gY2FtZSBmcm9tIHRoZSB3aGVhdCBbY2ltbXl0IHR1dG9yaWFsXShodHRwOi8vYmdsci5yLWZvcmdlLnItcHJvamVjdC5vcmcvQkdMUi10dXRvcmlhbC5wZGYpIGFuZCBbdmFsaWRhdGlvbiBleGFtcGxlc10oaHR0cHM6Ly9naXRodWIuY29tL2dkbGMvQkdMUi1SL2Jsb2IvbWFzdGVyL2luc3QvbWQvVmFsaWRhdGlvbi5tZCkgYW5kIGZvciByckJMVVAgSSBzdGFydGVkIHdpdGggdGhpcyBbdHV0b3JpYWxdKGh0dHA6Ly9wYmd3b3Jrcy5vcmcvc2l0ZXMvcGJnd29ya3Mub3JnL2ZpbGVzL0ludHJvZHVjdGlvbiUyMHRvJTIwR2Vub21pYyUyMFNlbGVjdGlvbiUyMGluJTIwUi5wZGYpIGFuZCB0aGUgW3JyQkxVUCBDUkFOIHBhZ2VdKGh0dHBzOi8vY3Jhbi5yLXByb2plY3Qub3JnL3dlYi9wYWNrYWdlcy9yckJMVVAvcnJCTFVQLnBkZikuICAgDQoNCiMjIyMgSW5wdXQgZGF0YXNldDoNCkZvciBleGFtcGxlIHB1cnBvc2VzLCB3ZSB3aWxsIHVzZSB0aGUgZXhhbXBsZSBkYXRhc2V0IHVzaW5nIENhbXBvcyBldCBhbC4sIDIwMTAgcmF0aGVyIHRoYW4gbXkgcHJvamVjdCBkYXRhc2V0LiAgIA0KYGBge3J9DQojIGluc3RhbGwucGFja2FnZXMoIkJHTFIiKSAjSW5zdGFsbCBwYWNrYWdlIGlmIG5vdCBhbHJlYWR5IGluc3RhbGxlZA0KbGlicmFyeShCR0xSKQ0KZGF0YSh3aGVhdCkNCnkgPC0gd2hlYXQuWSAjUGhlbm90eXBpYyBkYXRhDQpYIDwtIHdoZWF0LlggI0dlbm90eXBpYyBkYXRhDQpuPC1ucm93KFgpOyBwPC1uY29sKFgpICNkZWZpbmluZyBuIGFzIHRoZSByb3cgbnVtYmVyIG9mIG1hdHJpeCBgWGAgYW5kIHAgYXMgdGhlIGNvbCBudW1iZXIgb2YgbWF0cml4IGBYYA0KeVsxOjQsXSAjRWFjaCBjb2x1bW4gaXMgYSBkaWZmZXJlbnQgcGhlbm90eXBlOyBsaW5lcy92YXJpZXRpZXMgKHJvd3MpDQpYWzE6NSwxOjVdICNsaW5lcy92YXJpZXRpZXMocm93cykgYW5kIG1hcmtlcnMgKGNvbCkNCg0KYGBgDQoNCkxvYWQgYWxsIHBhY2thZ2VzIGZvciB0aGUgZW50aXJlIHNjcmlwdCBhbmQgc2V0IHRoZSB3b3JraW5nIGRpcmVjdG9yeSAgIA0KKlJlbWVtYmVyIHRvIGluc3RhbGwgYWxsIHBhY2thZ2VzIG9uY2UgYmVmb3JlIGxvYWRpbmcgdGhlbSBiZWxvdyogIA0KYGBge3IgZXZhbD1GQUxTRX0NCmxpYnJhcnkoQkdMUikNCmxpYnJhcnkocnJCTFVQKQ0KbGlicmFyeShmb3JlYWNoKQ0KbGlicmFyeShkb01DKSAgIyBVc2VkIGZvciBydW5uaW5nIHRoZSBsb29wcyBvbiBtdWx0aXBsZSBjb3JlcyAoZXhhbXBsZSBoZXJlLCA1IGNvcmVzKQ0KcmVnaXN0ZXJEb01DKGNvcmVzPTUpDQoNCmxpYnJhcnkoZ2dwbG90MikNCmZvbnQ8LWVsZW1lbnRfdGV4dChmYWNlID0gImJvbGQiLCAgc2l6ZSA9IDE2KSAgI2dncGxvdCBncmFwaGljYWwgcHJlZmVyZW5jZXMNCmZvbnQyPC1lbGVtZW50X3RleHQoc2l6ZSA9IDE2KQ0KDQojIHNldHdkKCJEOi9QSFMgR2Vub21pYyBTZWxlY3Rpb24gUHJvamVjdC9EYXRhIEFuYWx5c2lzL0dTLzIwMTkwMjE5IikNCg0KYGBgDQoNCi0tLS0tLS0tLS0tLS0NCg0KIyMjIyByckJMVVAgIA0KDQpgYGB7ciBldmFsPUZBTFNFfQ0KI1BhcmFtZXRlcnMgZGVmaW5lZCAgDQpmb2xkcz01ICAgICAgICAgICAgICANCks9QS5tYXQoWCkNCnkgPSB5WywyXQ0KDQojRnVuY3Rpb24NCnRlc3RfQ1ZfcGFyYWxsZWw8LWZ1bmN0aW9uKHgsZm9sZHMpeyAgIA0KICBzZXQuc2VlZCh4KQ0KICByZXN1bHRzPWMoKQ0KICBmb3IoaSBpbiAxOmZvbGRzKXsgICANCiAgICB0c3QgPC0gIHNhbXBsZShyZXAoMTpmb2xkcyxsZW5ndGgub3V0PW4pKQ0KICAgIHlOQTwteQ0KICAgIHlOQVt3aGljaChpPT10c3QpXTwtTkEgDQogICAgYW5zIDwtIG1peGVkLnNvbHZlKHlOQSxLPUspDQogICAgeUhhdF8xPWFucyR1Wy10c3RdDQogICAgcmVzdWx0cyA8LSBhcHBlbmQocmVzdWx0cyxjb3IoeUhhdF8xLHlbLXRzdF0pKQ0KICB9DQogIHJldHVybihyZXN1bHRzKQ0KfQ0KDQojQW5hbHlzaXMNCmN2X3Jlc3VsdHM2PWZvcmVhY2goY29yZXM9MTo1LCAuY29tYmluZT0nYycpICVkb3BhciUgdGVzdF9DVl9wYXJhbGxlbCh4PWNvcmVzLCBmb2xkcz01KSAjNSAobXVsdGkpIGNvcmUgY29tcHV0ZXI7IGRvcGFyIHdpbGwgZG8gdGhlIGNvbW1hbmQgaW4gcGFyYWxsZWwNCiMgY3ZfcmVzdWx0czY9Zm9yZWFjaChjb3Jlcz0xOjUsIC5jb21iaW5lPSdjJykgJWRvJSB0ZXN0X0NWX3BhcmFsbGVsKHg9Y29yZXMsIGZvbGRzPTUpICNvbmUgY29yZSBjb21wdXRlcg0KDQojRGF0YSBPcmdhbml6YXRpb24NCmFjY3VyYWN5NSA9IGFzLmRhdGEuZnJhbWUobWF0cml4KGN2X3Jlc3VsdHM2KSkgDQphY2N1cmFjeTUkbW9kZWw8LSJyckJMVVAiDQpuYW1lcyhhY2N1cmFjeTUpIDwtIGMoIlBBIiwgICJtb2RlbCIpICAgICAgDQphY2N1cmFjeXJyPC1hY2N1cmFjeTUNCmBgYCANCg0KKipUaGUgc2NyaXB0IHN0ZXAgYnkgc3RlcDoqKiAgICANCg0KKipMZXRzIHN0YXJ0IGZyb20gdGhlIGJhc2ljcyB3aXRoaW4gYWxsIHRoZSBsb29wcyBhbmQgd29yayBvdXR3YXJkcy4qKiAgDQpSdW5uaW5nIGdlbm9taWMgcHJlZGljdGlvbiBmb3IgYSBwaGVub3R5cGUgYHlgIHdoaWxlIHVzaW5nIGEga2luc2hpcCBtYXRyaXggYEtgIGNvbnNpc3RzIG9mIHRoZSBmb2xsb3dpbmcgY29tbWFuZHM6ICANCmBLPUEubWF0KFgpYCBUaGUgQS5tYXQgZnVuY3Rpb24gaW4gcnJCTFVQIHdpbGwgY2FsY3VsYXRlIHRoZSBhZGRpdGl0dmUgcmVsYXRpb25zaGlwIG1hdHJpeCAgDQpgeSA9IHlbLDJdYCBkZWZpbmUgd2hpY2ggcGhlbm90eXBlIHlvdSB3YW50IHRvIHByZWRpY3QgKHRoaXMgd2lsbCBsZWF2ZSB5IGFzIGp1c3QgYSB2ZWN0b3Igb2YgbiB4IDEpICANCmBwcmVkIDwtIG1peGVkLnNvbHZlKHksSz1LKWAgbWl4ZWQuc29sdmUgc2FsY3VsYXRlcyBtYXhpbXVtLWxpa2VsaWhvb2QgKE1ML1JFTUwpIHNvbHV0aW9ucyBmb3IgbWl4ZWQgbW9kZWxzLCBidXQgc2luY2UgeW91IGFyZSB1c2luZyBgS2AgaW4geW91ciBtb2RlbCwgaXQgd2lsbCBhbHNvIGNhbGN1bGF0ZSB0aGUgR0VCVnMgIA0KYHByZWQkdVtdYCB0aGUgb3V0cHV0IGBwcmVkYCB3aWxsIGNvbnRhaW4gYSBkYXRhZnJhbWUgYHVgIHRoYXQgY29udGFpbnRzIHRoZSBHRUJWcw0KICANCkhvd2V2ZXIsIHRoaXMgaXMgdXNpbmcgYWxsIDEwMCUgb2YgeW91ciBkYXRhLCB0byBjYWxjdWxhdGUgdGhlIEdFQlZzLCBzbyBzaW5jZSB5b3UgYXJlbnQgdHJhaW5pbmcgb24gYSBzdWJzZXQgb2YgdGhlIGRhdGEgdG8gcHJlZGljdCBhbm90aGVyIHN1YnNldCwgdGhlIHByZWRpY3Rpb24gYWNjdXJhY3kgb2YgdGhlIG9ic2VydmVkIHZhcmlhYmxlIGB5YCB0byB0aGUgcHJlZGljdGVkIHZhcmlhYmxlIGB1YCBpcyBub3QgdXNlZnVsLiAgDQoNCioqNS1mb2xkIGNyb3NzIHZhbGlkYXRpb24gKiogIA0KVG8gY3Jvc3MgdmFsaWRhdGUsIHlvdSBjYW4gZGl2aWRlIHlvdSBkYXRhc2V0IGludG8gNSBzdWJzZXQsIHRyYWluIHlvdXIgbW9kZWwgb24gNC81IG9mIHRoZSBzdWJzZXRzLCBhbmQgdGVzdCBmb3IgYWNjdXJhY3kgb24gdGhlIHJlbWFpbmluZyAxLzUgb2YgdGhlIHN1YnNldC4gICANCioqTk9URSoqOiBUaGVyZSBhcmUgbWFueSB3YXlzIHRvIHN1YnNldCB5b3VyIGRhdGEsIGRlcGVuZGluZyBvbiB0aGUgcXVlc3Rpb24geW91IGFyZSBhc2tpbmcgZm9yIHlvdXIgcHJvamVjdCBhbmQgYWxzbyBkZXBlbmRpbmcgb24gaG93IHlvdSB3aXNoIHRvIHdyaXRlIHlvdXIgc2NyaXB0LiBCZWxvdyBpcyBob3cgSSBpbml0aWFsbHkgbGVhcm5lZCB0byBzdWJzZXQgZm9yIDUtZm9sZHMgYmFzZWQgb24gdGhlIENJTU1ZVCB0dXRvcmlhbHMsIGJ1dCBpdCBpcyBub3QgdGhlIG9ubHkgd2F5LiANCg0KTGV0cyBzdGFydCB3aXRoIGEgZm9yIGxvb3AsIGZvciBgaWAgZnJvbSAxIHRvIGZvbGRzIChhbmQgd2UgY2FuIGRlZmluZSBhcyBgZm9sZHM9NWAgZm9yIHR1dG9yaWFsIHB1cnBvc2VzKTogIA0KYCAgZm9yKGkgaW4gMTpmb2xkcyl7YCAgICAgDQpgICB9YCAgIA0KICANCldpdGhpbiB0aGF0IGxvb3AsIHdlIHdhbnQgdG8gc3Vic2V0IHRoZSBkYXRhc2V0OiAgDQpgdHN0IDwtICBzYW1wbGUocmVwKDE6Zm9sZHMsbGVuZ3RoLm91dD1uKSlgIGNyZWF0ZSBhIHZlY3RvciBgdHN0YCB0aGF0IGhhcyB0aGUgdmFsdWVzIGAxOmZvbGRzYCAoaW4gb3VyIGNhc2UgMTo1KSBhbmQgYSBgbGVuZ3RoYCBvZiBgbmAgd2hpY2ggaXMgdGhlIG51bWJlciBvZiBsaW5lcyBpbiBgWGAgd2hpY2ggc2hvdWxkIGJlIHRoZSBzYW1lIG51bWJlciBvZiBsaW5lcyBpbiBgeWAgICAgIA0KDQpgeU5BPC15YCBkZWZpbmUgYSBuZXcgdmVjdG9yIGB5TkFgIHdpdGggdGhlIHNhbWUgdmFsdWVzIGFzIGB5YCAgICANCmB5TkFbd2hpY2goaT09dHN0KV08LU5BYCBtYWtlIDEvNSBvZiB0aGUgcGhlbm8gZGF0YSBOQSwgb3Igb21pdHRlZCwgc28gdGhhdCBsYXRlciBvbiB3aGVuIHdlIGNhbGN1bGF0ZSB0aGUgbWl4ZWQuc29sdmUgbW9kZWwsIHdlIGFyZSBvbmx5IHVzaW5nIDQvNSBvZiB0aGUgZGF0YXNldC4gTW9yZSBzcGVjaWZpY2FsbHksIHRoaXMgY29tbWFuZCAgICANCg0KKipOb3csIGB5TkFbdHN0XWAgaXMgYSB2ZWN0b3Igb2YgdGhlIHRyYWluaW5nIGRhdGFzZXQgd2hpbGUgYHlOQVstdHN0XWAgaXMgYSB2ZWN0b3Igb2YgdGhlIHRlc3RpbmcgZGF0YXNldCoqICAgDQp3aGljaCBtZWFucyB5b3UgY2FuIHNwZWNpZmljYWxseSBwdWxsIG91dCB0aGUgR0VCVnMgYHVgIGZyb20gdGhlIHRlc3Qgc2V0IGAtdHN0c2AgdG8gdXNlIGluIHlvdXIgcHJlZGljdGlvbiBhY2N1cmFjeSAoUEEpIGNhbGN1bGF0aW9uLiBIZXJlIGB5SGF0XzE9YW5zJHVbLXRzdF1gIHdlIGNhbGwgdGhlIHRlc3Qgc2V0cyBHRUJWcyBgeUhhdF8xYCAgIA0KDQoqKlByZWRpY2lvbiBBY2N1cmFjeSoqICANClRoZSBQQSBvZiB0aGUgb2JzZXJ2ZWQgdmFyaWFibGUgYHlgIHRvIHRoZSBwcmVkaWN0ZWQgdmFyaWFibGUgYHVgLiBIb3dldmVyLCBzaW5jZSB3ZSBhcmUgZG9pbmcgQ1YsIHdlIHdhbnQgdGhlIG9ic2VydmVkIHZhbHVlcyBvZiB0aGUgdGVzdCBncm91cCBgeVstdHN0XWAgdG8gdGhlIHByZWRpY3RlZCB2YWx1ZXMvR0VCVnMsIHdoaWNoIHdlIGp1c3QgZGVmaW5lZCBhYm92ZSBhcyBgeUhhdF8xYC4gICAgDQpgcmVzdWx0cyA8LSBhcHBlbmQocmVzdWx0cyxjb3IoeUhhdF8xLHlbLXRzdF0pKWAgQWRkL2FwcGVuZCB0aGlzIG5ldyBQQSBjYWxjdWxhdGVkIHRvIHRoZSByZXN1bHRzIHZlY3RvciANCg0KYGBge3IgZXZhbD1GQUxTRX0NCiAgZm9yKGkgaW4gMTpmb2xkcyl7ICAgDQogICAgdHN0IDwtICBzYW1wbGUocmVwKDE6Zm9sZHMsbGVuZ3RoLm91dD1uKSkNCiAgICB5TkE8LXkNCiAgICB5TkFbd2hpY2goaT09dHN0KV08LU5BIA0KICAgIGFucyA8LSBtaXhlZC5zb2x2ZSh5TkEsSz1LKQ0KICAgIHlIYXRfMT1hbnMkdVstdHN0XQ0KICAgIHJlc3VsdHMgPC0gYXBwZW5kKHJlc3VsdHMsY29yKHlIYXRfMSx5Wy10c3RdKSkNCiAgfQ0KYGBgDQoNCioqUmVwbGljYXRlIENWIGFuZCBSYW5kb21pemF0aW9uKiogIA0KQ3JlYXRpbmcgYSBmdW5jdGlvbiBmb3IgdGhlIHJyQkxVUCBhbmFseXNpcyBoZWxwcyB3aXRoIHJlcGxpY2F0aW9uLCBidXQgYWxzbyBrZWVwcyB5b3VyIGNvZGUgbW9yZSBzaW1wbGUgZG93bnN0cmVhbS4gQWxzbywgIEkgdXNlIHRoZSBgJWRvcGFyJWAgY29tbWFuZCB0byBkaXN0cmlidXRlIHRoZSBjb21wdXRhdGlvbiBvdmVyIDUgY29yZXMgYXQgYSB0aW1lLiAgIA0KDQpEZWZpbmUgYSBmdW5jdGlvbiBjYWxsZWQgYHRlc3RfQ1ZfcGFyYWxsZWxgIHdpdGggdmFyaWFibGUgYHhgIGFuZCBgZm9sZHNgIHRoYXQgbmVlZCB0byBiZSBkZWZpbmVkLiAgDQpgYGB7ciBldmFsPUZBTFNFfQ0KdGVzdF9DVl9wYXJhbGxlbDwtZnVuY3Rpb24oeCxmb2xkcyl7ICAgIA0KfSAgDQpgYGANCg0KDQpgc2V0LnNlZWQoeClgIHNldHRpbmcgdGhlIHJhbmRvbWl6YXRpb24gYmFzZWQgb24gYHhgIHdoaWNoIHdpbGwgY2hhbmdlIHRocm91Z2ggdGhlIGZ1bmN0aW9uLiBBcyBzaG93biBiZWxvdywgeCBpcyBiYXNlZCBvbiB0aGUgbnVtYmVyIG9mIGNvcmVzIHVzZWQgdG8gcnVuIHRoZSBmdW5jdGlvbiBgeD1jb3Jlc2AgYW5kIHdlIHdpbGwgZGVmaW5lIGBjb3Jlcz0xOjVgLiBTbyBhcyB0aGUgZnVuY3Rpb24gaXMgdXNpbmcgMSwgMiwgMywgNCwgNSBjb3JlcywgdGhlIHJhbmRvbSBzZWVkIHdpbGwgY2hhbmdlIHRvIGBzZWVkKDEpYCAuLi4gYHNlZWQoNSlgLiBUaGlzIGlzIHZlcnkgdXNlZnVsIHdoZW4geW91IGhhdmUgYSBoZWF2eSBjb21wdXRhdGlvbmFsIGxvYWQuICANCmByZXN1bHRzPWMoKWAgIGNyZWF0ZSBhbiBlbXB0eSB2ZWN0b3IgY2FsbGVkIGByZXN1bHRzYA0KDQpgcmV0dXJuKHJlc3VsdHMpYCAgDQoNCioqZm9yZWFjaCBjb21tYW5kKiogIA0KYGN2X3Jlc3VsdHM2ID0gZm9yZWFjaChjb3Jlcz0xOjUsIC5jb21iaW5lPSdjJykgJWRvcGFyJSB0ZXN0X0NWX3BhcmFsbGVsKHg9Y29yZXMsIGZvbGRzPTUpYCA1IChtdWx0aSkgY29yZSBjb21wdXRlcjsgZG9wYXIgd2lsbCBkbyB0aGUgY29tbWFuZCBpbiBwYXJhbGxlbCB3aGVyZWFzICVkbyUgY2FuIGJlIHVzZWQgZm9yIGEgb25lIGNvcmUgY29tcHV0ZXINCg0KDQpBbmQgdGhpcyBjb21wbGV0ZXMgdGhlIHdob2xlICoqR2Vub21pYyBQcmVkaWN0aW9uIEFuYWx5c2lzKio6ICANCmBgYHtyIGV2YWw9RkFMU0V9DQojRnVuY3Rpb24NCnRlc3RfQ1ZfcGFyYWxsZWw8LWZ1bmN0aW9uKHgsZm9sZHMpeyAgIA0KICBzZXQuc2VlZCh4KQ0KICByZXN1bHRzPWMoKQ0KICBmb3IoaSBpbiAxOmZvbGRzKXsgICANCiAgICB0c3QgPC0gIHNhbXBsZShyZXAoMTpmb2xkcyxsZW5ndGgub3V0PW4pKQ0KICAgIHlOQTwteQ0KICAgIHlOQVt3aGljaChpPT10c3QpXTwtTkEgDQogICAgYW5zIDwtIG1peGVkLnNvbHZlKHlOQSxLPUspDQogICAgeUhhdF8xPWFucyR1Wy10c3RdDQogICAgcmVzdWx0cyA8LSBhcHBlbmQocmVzdWx0cyxjb3IoeUhhdF8xLHlbLXRzdF0pKQ0KICB9DQogIHJldHVybihyZXN1bHRzKQ0KfQ0KI0FuYWx5c2lzDQpjdl9yZXN1bHRzNj1mb3JlYWNoKGNvcmVzPTE6NSwgLmNvbWJpbmU9J2MnKSAlZG9wYXIlIHRlc3RfQ1ZfcGFyYWxsZWwoeD1jb3JlcywgZm9sZHM9NSkgIzUgKG11bHRpKSBjb3JlIGNvbXB1dGVyOyBkb3BhciB3aWxsIGRvIHRoZSBjb21tYW5kIGluIHBhcmFsbGVsDQojIGN2X3Jlc3VsdHM2PWZvcmVhY2goY29yZXM9MTo1LCAuY29tYmluZT0nYycpICVkbyUgdGVzdF9DVl9wYXJhbGxlbCh4PWNvcmVzLCBmb2xkcz01KSAjb25lIGNvcmUgY29tcHV0ZXINCmBgYA0KDQoqKkRhdGEgT3JnYW5pemF0aW9uKiogIA0KVGhlIGxhc3QgcG9ydGlvbiBpcyBvcmdhbml6aW5nIHRoZSBvdXRwdXQgaW50byBhIHdvcmthYmxlIGRhdGFmcmFtZSB0aGUgSSBjYW4gdXNlIHRvIHBsb3Qgb2YgY29tYmluZWQgd2l0aCBvdGhlciByZXN1bHRzIGRvd25zdHJlYW0uICANCg0KYGFjY3VyYWN5NSA9IGFzLmRhdGEuZnJhbWUobWF0cml4KGN2X3Jlc3VsdHM2KSlgIHRoZSBtYXRyaXggYGN2X3Jlc3VsdHM2YCBpcyBtYWRlIGludG8gYSBkYXRhZnJhbWUgY2FsbGVkIGBhYWNjdXJhY3k1YCAgICANCmBhY2N1cmFjeTUkbW9kZWw8LSJyckJMVVAiYCBhZGQgaW4gYSBuZXcgY29sdW1uIGBtb2RlbGAgaW50byB0aGUgZGYgYW5kIGZpbGwgdGhlIGNvbHVtbiB3aXRoIHRoZSBjaGFyYWN0ZXJzIGByckJMVVBgICANCmBuYW1lcyhhY2N1cmFjeTUpIDwtIGMoIlBBIiwgICJtb2RlbCIpYCByZW5hbWUgdGhlIGNvbHVtbiBuYW1lcyB0byBgUEFgIGFuZCBgbW9kZWxgICAgICAgICAgDQpgYWNjdXJhY3lycjwtYWNjdXJhY3k1YCByZW5hbWUgdGhlIGRmIHRvIGBhY2N1cmFjeXJyYCAgICANCg0KDQpOb3cgaWYgeW91J3ZlIGFscmVhZHkgZGVmaW5lZCB5b3VyIGZ1bmN0aW9uIHVwc3RyZWFtLCBhbGwgeW91IG5lZWQgdG8gd3JpdGUgZm9yIGVhY2ggcnVuIGlzOiAgDQoNCmBgYHtyIGV2YWw9RkFMU0V9DQojQW5hbHlzaXMNCmN2X3Jlc3VsdHM2PWZvcmVhY2goY29yZXM9MTo1LCAuY29tYmluZT0nYycpICVkb3BhciUgdGVzdF9DVl9wYXJhbGxlbCh4PWNvcmVzLCBmb2xkcz01KSAjNSAobXVsdGkpIGNvcmUgY29tcHV0ZXI7IGRvcGFyIHdpbGwgZG8gdGhlIGNvbW1hbmQgaW4gcGFyYWxsZWwNCg0KI0RhdGEgT3JnYW5pemF0aW9uDQphY2N1cmFjeTUgPSBhcy5kYXRhLmZyYW1lKG1hdHJpeChjdl9yZXN1bHRzNikpIA0KYWNjdXJhY3k1JG1vZGVsPC0icnJCTFVQIjsgbmFtZXMoYWNjdXJhY3k1KSA8LSBjKCJQQSIsICAibW9kZWwiKSAgICAgIA0KYWNjdXJhY3lycjwtYWNjdXJhY3k1DQoNCmBgYA0KDQpOb3RlOiBgZm9sZHM9NWAgZG9lcyBub3QgbmVlZCB0byBiZSBkZWZpbmVkIGFueW1vcmUsIHNpbmNlIGl0IGlzIGludGVncmF0ZWQgaW50byB0aGUgZnVuY3Rpb24gYXMgYHhgIGFuZCBgZm9sZHNgLiANCg0KLS0tLS0tLS0tLQ0KDQojIyMjIFJLSFMgIA0KDQpUaGUgNS1mb2xkIGNyb3NzIHZhbGlkYXRpb24gc3Vic2V0dGluZyBhbmQgZGF0YSBvcmdhbml6YXRpb24gaXMgYWxsIHRoZSBzYW1lIGFzIGV4cGxhaW5lZCBpbiB0aGUgcnJCTFVQIHNlY3Rpb24sIGhvd2V2ZXIgdGhlIG1vZGVsIHdpdGhpbiB0aGUgQ1YgaXMgbm93IGNoYW5nZWQuIEFuIGV4cGxhbmF0aW9uIG9mIHRoZSBSS0hTIG1vZGVsIGlzIGJlbG93LiANCg0KYGBge3IgZXZhbD1GQUxTRX0gDQojUGFyYW1ldGVycyBkZWZpbmVkDQpuSXRlciA9IDEyMDAwDQpidXJuSW49MjAwMA0KaCA8LSAwLjUNCkQ8LShhcy5tYXRyaXgoZGlzdChYLG1ldGhvZD0nZXVjbGlkZWFuJykpXjIpL3ANClU8LWV4cCgtaCpEKSANCiMgeSA9IHlbLDJdDQoNCiNGdW5jdGlvbg0KdGVzdF9DVl9wYXJhbGxlbDwtZnVuY3Rpb24oeCxmb2xkcyl7ICAgDQogIHNldC5zZWVkKHgpDQogIHJlc3VsdHM9YygpDQogIGZvcihpIGluIDE6Zm9sZHMpeyAgICANCiAgICB0c3QgPC0gIHNhbXBsZShyZXAoMTpmb2xkcyxsZW5ndGgub3V0PW4pKQ0KICAgIHlOQTwteQ0KICAgIHlOQVt3aGljaChpPT10c3QpXTwtTkEgDQogICAgRVRBPC1saXN0KGxpc3QoSz1VLG1vZGVsPSdSS0hTJykpDQogICAgZm08LUJHTFIoeT15TkEsRVRBPUVUQSxuSXRlcj1uSXRlciwgYnVybkluPWJ1cm5JbikNCiAgICB5SGF0XzE9Zm0keUhhdFstdHN0XQ0KICAgIHJlc3VsdHMgPC0gYXBwZW5kKHJlc3VsdHMsY29yKHlIYXRfMSx5Wy10c3RdKSkNCiAgfQ0KICByZXR1cm4ocmVzdWx0cykNCn0NCg0KI0FuYWx5c2lzDQpjdl9yZXN1bHRzNT1mb3JlYWNoKGNvcmVzPTE6NSwgLmNvbWJpbmU9J2MnKSAlZG9wYXIlIHRlc3RfQ1ZfcGFyYWxsZWwoeD1jb3JlcywgZm9sZHM9NSkNCg0KI0RhdGEgT3JnYW5pemF0aW9uDQphY2N1cmFjeTUgPSBhcy5kYXRhLmZyYW1lKG1hdHJpeChjdl9yZXN1bHRzNSkpIA0KYWNjdXJhY3k1JG1vZGVsPC0iUktIUyINCm5hbWVzKGFjY3VyYWN5NSkgPC0gYygiUEEiLCAibW9kZWwiKSAgICAgICAgDQphY2N1cmFjeXJrPC1hY2N1cmFjeTUgDQoNCmBgYA0KDQoNCkFzIHN0YXRlZCBpbiB0aGUgW0NhbXBvcyBMYWIgZG9jdW1lbnRdKGh0dHBzOi8vanZhbmRlcncudW5lLmVkdS5hdS9HZGxDSGFuZG91dHMucGRmKTogDQoNCj4gaCBpcyBhIGJhbmR3aWR0aCBwYXJhbWV0ZXIgY29udHJvbGxpbmcgaG93IGZhc3QgdGhlIGtlcm5lbCBkZWNheSBhcyB0aGUgdHdvIHBvaW50cywgKHhpICwgeGknICksIGdldCBmdXJ0aGVyIGFwYXJ0DQoNCkkgaGF2ZSBhdHRlbXB0ZWQgdG8gZGV0ZXJtaW5lIGBoYCB3aXRoIG15IGRhdGFzZXQgZm91bmYgaW4gdGhpcyBbUiBOb3RlYm9va10oaHR0cHM6Ly9uYnZpZXdlci5qdXB5dGVyLm9yZy9naXRodWIvc2hhbnRlbC1tYXJ0aW5lei9zaGFudGVsLW1hcnRpbmV6LmdpdGh1Yi5pby9ibG9iL21hc3Rlci9SbWQlMjBQcm90b2NvbHMvQmFuZHdpZHRoUGFyYW1ldGVyX1JLSFMubmIuaHRtbD9mbHVzaF9jYWNoZT10cnVlKSAgDQoNCmBoIDwtIDAuNWAganVzdCBmb3IgdHV0b3JpYWwgcHVycG9zZXMsIHdlIHdpbGwgc2V0IGggdG8gMC41LiBIb3dldmVyIEkgc3VnZ2VzdCByZWFkaW5nIHRoZSBbQ2FtcG9zIExhYiBkb2N1bWVudF0oaHR0cHM6Ly9qdmFuZGVydy51bmUuZWR1LmF1L0dkbENIYW5kb3V0cy5wZGYpIHNlY3Rpb24gNCBhbmQgW0NhbXBvcyBldCBhbC4sIDIwMTBdKGh0dHBzOi8vd3d3LmNhbWJyaWRnZS5vcmcvY29yZS9qb3VybmFscy9nZW5ldGljcy1yZXNlYXJjaC9hcnRpY2xlL3NlbWlwYXJhbWV0cmljLWdlbm9taWNlbmFibGVkLXByZWRpY3Rpb24tb2YtZ2VuZXRpYy12YWx1ZXMtdXNpbmctcmVwcm9kdWNpbmcta2VybmVsLWhpbGJlcnQtc3BhY2VzLW1ldGhvZHMvMkI4MjM5MTZDRjRENzZGQUU2QkM4MTQ1NUZENzNBQkIvY29yZS1yZWFkZXIpIHRvIGJldHRlciB1bmRlcnN0YW5kIGBoYCAgIA0KDQoqKkRpc3RhbmNlIGZ1bmN0aW9uIGFuZCByZXByb2R1Y2luZyBrZXJuZWwqKiAgDQpgRDwtKGFzLm1hdHJpeChkaXN0KFgsbWV0aG9kPSdldWNsaWRlYW4nKSleMikvcGAgIA0KYFU8LWV4cCgtaCpEKWAgICANCg0KKipGaXR0aW5nIHRoZSBSS0hTIE1vZGVsKiogIA0KYEVUQTwtbGlzdChsaXN0KEs9VSxtb2RlbD0nUktIUycpKWAgIA0KYGZtPC1CR0xSKHk9eU5BLEVUQT1FVEEsbkl0ZXI9bkl0ZXIsIGJ1cm5Jbj1idXJuSW4pYCAgDQpgZm0keUhhdFstdHN0XWAgIA0KIA0KLS0tLS0tLS0tLQ0KDQojIyMjIFJ1bm5pbmcgTEFTU08gIA0KDQpUaGUgNS1mb2xkIGNyb3NzIHZhbGlkYXRpb24gc3Vic2V0dGluZyBhbmQgZGF0YSBvcmdhbml6YXRpb24gaXMgYWxsIHRoZSBzYW1lIGFzIGV4cGxhaW5lZCBpbiB0aGUgcnJCTFVQIHNlY3Rpb24sIGhvd2V2ZXIgdGhlIG1vZGVsIHdpdGhpbiB0aGUgQ1YgaXMgbm93IGNoYW5nZWQuIEFuIGV4cGxhbmF0aW9uIG9mIHRoZSBMQVNTTyBtb2RlbCBpcyBiZWxvdy4gDQoNCmBgYHtyIGV2YWw9RkFMU0V9DQojUGFyYW1ldGVycyBkZWZpbmVkDQpuSXRlciA9IDEyMDAwDQpidXJuSW49MjAwMA0KIyB5ID0geVssMl0NCg0KI0Z1bmN0aW9uDQp0ZXN0X0NWX3BhcmFsbGVsPC1mdW5jdGlvbih4LGZvbGRzKXsgICANCiAgc2V0LnNlZWQoeCkNCiAgcmVzdWx0cz1jKCkNCiAgZm9yKGkgaW4gMTpmb2xkcyl7ICAgDQogICAgdHN0IDwtICBzYW1wbGUocmVwKDE6Zm9sZHMsbGVuZ3RoLm91dD1uKSkNCiAgICB5TkE8LXkNCiAgICB5TkFbd2hpY2goaT09dHN0KV08LU5BIA0KICAgIEVUQTwtbGlzdChsaXN0KFg9WCxtb2RlbD0nQkwnKSkNCiAgICBmbTwtQkdMUih5PXlOQSwgcmVzcG9uc2VfdHlwZSA9ICJnYXVzc2lhbiIsIEVUQSA9IEVUQSwgbkl0ZXI9bkl0ZXIsIGJ1cm5Jbj1idXJuSW4pIA0KICAgIHlIYXRfMT1mbSR5SGF0Wy10c3RdDQogICAgcmVzdWx0cyA8LSBhcHBlbmQocmVzdWx0cyxjb3IoeUhhdF8xLHlbLXRzdF0pKQ0KICB9DQogIHJldHVybihyZXN1bHRzKQ0KfQ0KDQojQW5hbHlzaXMNCmN2X3Jlc3VsdHM3PWZvcmVhY2goY29yZXM9MTo1LCAuY29tYmluZT0nYycpICVkb3BhciUgdGVzdF9DVl9wYXJhbGxlbCh4PWNvcmVzLCBmb2xkcz01KQ0KDQojRGF0YSBPcmdhbml6YXRpb24NCmFjY3VyYWN5NSA9IGFzLmRhdGEuZnJhbWUobWF0cml4KGN2X3Jlc3VsdHM3KSkgDQphY2N1cmFjeTUkbW9kZWw8LSJMQVNTTyINCm5hbWVzKGFjY3VyYWN5NSkgPC0gYygiUEEiLCAibW9kZWwiKSAgICAgIA0KYWNjdXJhY3lCTDwtYWNjdXJhY3k1DQoNCmBgYCANCg0KKipGaXR0aW5nIHRoZSBSS0hTIE1vZGVsKiogIA0KYEVUQTwtbGlzdChsaXN0KFg9WCxtb2RlbD0nQkwnKSlgICAgIA0KYGZtPC1CR0xSKHk9eU5BLCByZXNwb25zZV90eXBlID0gImdhdXNzaWFuIiwgRVRBID0gRVRBLCBuSXRlcj1uSXRlciwgYnVybkluPWJ1cm5JbikgYCAgICANCg0KLS0tLS0tLS0tLQ0KIyMjIyBQbG90cyAgDQpBIHNpbXBsZSBnZ3Bsb3QgYm94cGxvdCAgb2YgUHJlZGljdGlvbiBBY2N1cmFjaWVzIChQQSkgYWNyb3NzIHRoZSB0aHJlZSBtb2RlbHMgIA0KYGBge3IgZXZhbD1GQUxTRX0NCmFjY3VyYWN5IDwtIHJiaW5kKCBhY2N1cmFjeXJrLCBhY2N1cmFjeXJyLCBhY2N1cmFjeUJMKSAjaGVhZGVyczogUEEgbW9kZWwgDQojIHdyaXRlLmNzdihhY2N1cmFjeSwgZmlsZT0iQWNjdXJhY3lfUEhTQkxVUF8yMDE4MTIyMS5jc3YiLCByb3cubmFtZXMgPSBGQUxTRSkNCiMgYWNjdXJhY3kgPC0gcmVhZC5jc3YoIkFjY3VyYWN5X1BIU0JMVVBfMjAxODEyMjEuY3N2IikNCg0KZ2dwbG90KGFjY3VyYWN5LCBhZXMoYWNjdXJhY3kkbW9kZWwsIGFjY3VyYWN5JFBBLCBmaWxsID0gZmFjdG9yKG1vZGVsKSkpICsNCiAgZ2VvbV9ib3hwbG90KCkrIHRoZW1lX2J3KCkgKw0KICB0aGVtZShheGlzLnRleHQgPSBmb250MiwgIGF4aXMudGl0bGUgPSBmb250LCBzdHJpcC50ZXh0LnggPSBmb250LCBwbG90LnRpdGxlID0gZm9udCkrDQogIHlsYWIoIlBBIikgKyB4bGFiKCIiKSsgeWxpbSgwLjYsIDAuOSkgKw0KICBnZ3RpdGxlKCJXaGVhdCB8IE1vZGVsIENvbXBhcmlzb24iKQ0KYGBgDQoNClJlbWVtYmVyIHRvIGZpeCB5b3UgYHlsaW1gIHJhbmdlIHRvIGAoMCwxKWAgYXQgZmlyc3QsIHRoZW4gbWluaW1pemUgaXQgaWYgaXRzIGhhcmQgdG8gc2VlIHRoZSBkaWZmZXJlbmNlcyAgDQo=