1 Creating a strain classifier using our data

I have not yet implemented a real ML classifier, so I will use some of the caret documentation in order to write a trainer and classifier of the TMRC2 strains.

2 Splitting the data based on outcome

I am taking this pretty much directly from:

https://topepo.github.io/caret/data-splitting.html#simple-splitting-based-on-the-outcome

In order to perform the split by outcome, I think I need to slightly reformat the structure of our variant data to more closely match what I see in the datasets: etitanic/iris/Sonar.

2.1 Define the outcome factor

Setting this to character here because I will be messing with the factor levels and I would rather not have to relevel multiple times.

outcome_factor <- as.character(pData(both_snps)[["zymodemecategorical"]])

2.2 Figure out why old samples were dropped.

In my previous attempt at this, I noticed that the older samples were getting dropped by na.omit, I want to take a moment here to figure out why.

dim(exprs(both_snps))
## [1] 1611844     134
both_norm <- normalize_expt(both_snps, transform="log2", norm="quant")
## transform_counts: Found 207502544 values equal to 0, adding 1 to the matrix.

2.3 Filter the data using near-zero variance

Stealing now from:

https://github.com/compgenomr/book/blob/master/05-supervisedLearning.Rmd

Multiple sections of the above document provide nice ways to remove redundant predictor variables. For my purposes, I would like to keep all of them because I am hoping to make use of the same model in a dataset which is vastly sparser than this. However, for the purposes of me learning, I will apply all of these methods and use it on the rest of the TMRC2 data.

texprs <- t(exprs(both_norm))
nzv <- caret::preProcess(texprs, method="nzv", uniqueCut=15)
nzv_texprs <- predict(nzv, texprs)
dim(nzv_texprs)
## [1]   134 95016
standard_devs <- apply(nzv_texprs, 2, sd)
top_predictors <- order(standard_devs, decreasing = TRUE)[1:2000]
nzv_texprs <- nzv_texprs[, top_predictors]

2.4 Center the data

I think centering may not be needed for this data, but here is how:

nzv_center <- preProcess(nzv_texprs, method = "center")
## Error in preProcess(nzv_texprs, method = "center"): could not find function "preProcess"
nzv_texprs <- predict(nzv_center, nzv_texprs)
## Error in predict(nzv_center, nzv_texprs): object 'nzv_center' not found

2.5 Drop correlated

nzv_correlated <- preProcess(nzv_texprs, method = "corr", cutoff = 0.9)
## Error in preProcess(nzv_texprs, method = "corr", cutoff = 0.9): could not find function "preProcess"
nzv_texprs <- predict(nzv_correlated, nzv_texprs)
## Error in predict(nzv_correlated, nzv_texprs): object 'nzv_correlated' not found
dim(nzv_texprs)
## [1]  134 2000
anyNA(nzv_texprs)
## [1] FALSE

2.6 Reformat the metadata to be used by caret

na_idx <- is.na(outcome_factor)
outcome_factor[na_idx] <- "unknown"
meta_factors <- pData(both_snps)[, c("zymodemecategorical", "clinicalcategorical", "sus_category_current")]
meta_factors[["zymodemecategorical"]] <- outcome_factor

2.7 Add the metadata to the filtered variables

In addition, I want make sure that no unknown samples are used in training.

snp_df <- cbind(meta_factors, nzv_texprs)
dim(snp_df)
## [1]  134 2003
snp_train_idx <- createDataPartition(outcome_factor, p = 0.4,
                                     list = FALSE,
                                     times = 1)
## Error in createDataPartition(outcome_factor, p = 0.4, list = FALSE, times = 1): could not find function "createDataPartition"
summary(outcome_factor[snp_train_idx])
## Error in h(simpleError(msg, call)): error in evaluating the argument 'object' in selecting a method for function 'summary': object 'snp_train_idx' not found
snp_train_start <- rownames(snp_df)[snp_train_idx]
## Error in eval(expr, envir, enclos): object 'snp_train_idx' not found
train_known_idx <- meta_factors[snp_train_start, "zymodemecategorical"] != "unknown"
## Error in `[.data.frame`(meta_factors, snp_train_start, "zymodemecategorical"): object 'snp_train_start' not found
train_rownames <- snp_train_start[train_known_idx]
## Error in eval(expr, envir, enclos): object 'snp_train_start' not found
not_train_idx <- ! rownames(snp_df) %in% train_rownames
## Error in h(simpleError(msg, call)): error in evaluating the argument 'table' in selecting a method for function '%in%': object 'train_rownames' not found
not_train_rownames <- rownames(snp_df)[not_train_idx]
## Error in eval(expr, envir, enclos): object 'not_train_idx' not found
train_df <- snp_df[train_rownames, ]
## Error in `[.data.frame`(snp_df, train_rownames, ): object 'train_rownames' not found
train_df[["clinicalcategorical"]] <- NULL
## Error in train_df[["clinicalcategorical"]] <- NULL: object 'train_df' not found
train_df[["sus_category_current"]] <- NULL
## Error in train_df[["sus_category_current"]] <- NULL: object 'train_df' not found
train_df[[1]] <- as.factor(as.character(train_df[[1]]))
## Error in h(simpleError(msg, call)): error in evaluating the argument 'x' in selecting a method for function 'as.factor': object 'train_df' not found
test_df <- snp_df[not_train_rownames, ]
## Error in `[.data.frame`(snp_df, not_train_rownames, ): object 'not_train_rownames' not found
test_df[["zymodemecategorical"]] <- NULL
## Error in test_df[["zymodemecategorical"]] <- NULL: object 'test_df' not found
test_df[["clinicalcategorical"]] <- NULL
## Error in test_df[["clinicalcategorical"]] <- NULL: object 'test_df' not found
test_df[["sus_category_current"]] <- NULL
## Error in test_df[["sus_category_current"]] <- NULL: object 'test_df' not found

2.8 KNN

Perform the fit and test the training set against itself.

knn_fit <- knn3(x = train_df[, -1],
                y = train_df[["zymodemecategorical"]],
                k = 5)
## Error in knn3(x = train_df[, -1], y = train_df[["zymodemecategorical"]], : could not find function "knn3"
train_predict <- predict(knn_fit, train_df[, -1], type = "class")
## Error in predict(knn_fit, train_df[, -1], type = "class"): object 'knn_fit' not found
confusionMatrix(data = train_df[[1]], reference = train_predict)
## Error in confusionMatrix(data = train_df[[1]], reference = train_predict): could not find function "confusionMatrix"
train_df[[1]]
## Error in eval(expr, envir, enclos): object 'train_df' not found
train_predict
## Error in eval(expr, envir, enclos): object 'train_predict' not found

It looks like my knn model has some disagreements in the already difficult cases of poorly represented strains, so I will call it a win. E.g it appears to fail on a z1.0, b2904, z3.0, z2.0, a 2.1, and 2.4.

Now lets try it out on the rest of the data.

test_predict <- predict(knn_fit, test_df, type = "class")
## Error in predict(knn_fit, test_df, type = "class"): object 'knn_fit' not found
names(test_predict) <- rownames(test_df)
## Error in h(simpleError(msg, call)): error in evaluating the argument 'x' in selecting a method for function 'rownames': object 'test_df' not found
test_predict
## Error in eval(expr, envir, enclos): object 'test_predict' not found

Now let us manually compare these to the sample sheet and see how we did…

|———-|———-|—–|——|—-| |Sample |Annotation|MyML |Agree?|IGV | |TMRC20001 |z2.3 |z1.0 |No |I agree it is not a 2.3, but to me it doesn’t look like a 1.0 | |TMRC20007 |unknown |z2.3 | | | |TMRC20008 |unknown |z2.3 | | | |TMRC20039 |z2.2 |z2.2 |Yes | | |TMRC20037 |z2.3 |z2.3 |Yes | | |TMRC20038 |z2.3 |z2.3 |Yes | | |TMRC20068 |z2.3 |z2.3 |Yes | | |TMRC20041 |z2.2 |z2.1 |No |maybe more like 2.2? It doesn’t look like either to me | |TMRC20009 |z2.2 |z2.2 |Yes | | |TMRC20010 |z2.3 |z2.3 | Yes | | |TMRC20016 |z2.3 |z2.3 | Yes || |TMRC20014 |z2.2|z2.2|Yes|| |TMRC20019 |z2.2 |z2.2|Yes|| |TMRC20070 |z2.3 |z2.3|Yes|| |TMRC20021 |z2.3 |z2.3|Yes|| |TMRC20022 |z2.2 |z2.2|Yes|| |TMRC20036 |z2.2 |z2.1|No| It is definitely an odd 2.2; I can see why it said it is 2.1| |TMRC20031 |z2.2 |z2.2|Yes|| |TMRC20076 |z2.2|z2.2|Yes|| |TMRC20073 |z2.3|z2.3|Yes|| |TMRC20078 |z2.2|z2.2|Yes|| |TMRC20042 |z2.2|z2.2|Yes|| |TMRC20059 |z2.3|z2.3|Yes|| |TMRC20048 |z2.3|z2.3|Yes|| |TMRC20088 |z2.2|z2.2|Yes|| |TMRC20077 |z2.2|z2.2|Yes|| |TMRC20074 |z2.2|z2.2|Yes|| |TMRC20063 |z2.2|z2.2|Yes|| |TMRC20052 |z2.3|z2.3|Yes|| |TMRC20064 |z2.3|z2.3|Yes|| |TMRC20075 |z2.3|z2.3|Yes|| |TMRC20050 |z2.2|z2.2|Yes|| |TMRC20049 |z2.2|z2.2|Yes|| |TMRC20062 |z2.3|z2.3|Yes|| |TMRC20110 |z2.2|z2.2|Yes|| |TMRC20080 |z2.3|z2.3|Yes|| |TMRC20043 |z2.3|z2.3|Yes|| |TMRC20085 |z2.3|z2.3|Yes|| |TMRC20046 |z2.2|z2.1|No|This looks almost exactly like TMRC20036| |TMRC20093 |z2.1|z2.1|Yes|Interestingly, this does not look like TMRC20046 nor TMRC20036 in all places| |TMRC20047 |z2.4|z2.3|No|I see places where this looks like it uniquely matches both strains, they are crazy similar| |TMRC20109 |z2.2|z2.2|Yes|| |TMRC20096 |z2.2|z2.2|Yes|| |TMRC20097 |z2.2|z2.1|No|I bet it looks like TMRC20046/TMRC20036 before it opens, it does!| |TMRC20101 |z2.2|z2.2|Yes|| |TMRC20102 |z2.3|z2.3|Yes|| |TMRC20099 |z2.3|z2.3|Yes|| |TMRC20100 |z2.3|z2.3|Yes|| |TMRC20091 |z2.1|z2.2|No|This looks like what I think of as the ‘canonical’ z2.2 and not like TMRC20047.| |TMRC20084 |z2.1|z2.1|Yes|| |TMRC20087 |z2.2|z2.2|Yes|| |TMRC20103 |z2.1|z2.1|Yes|| |TMRC20104 |z2.3|z2.3|Yes|| |TMRC20086 |z2.2|z2.2|Yes|| |TMRC20107 |z2.3|z2.3|Yes|| |TMRC20095 |z2.3|z2.3|Yes||

LS0tCnRpdGxlOiAiVE1SQzIgTUwgQ2xhc3NpZmljYXRpb24gb2Ygc3RyYWluczogMjAyMjEyIgphdXRob3I6ICJhdGIgYWJlbGV3QGdtYWlsLmNvbSIKZGF0ZTogImByIFN5cy5EYXRlKClgIgpvdXRwdXQ6CiBodG1sX2RvY3VtZW50OgogIGNvZGVfZG93bmxvYWQ6IHRydWUKICBjb2RlX2ZvbGRpbmc6IHNob3cKICBmaWdfY2FwdGlvbjogdHJ1ZQogIGZpZ19oZWlnaHQ6IDcKICBmaWdfd2lkdGg6IDcKICBoaWdobGlnaHQ6IGRlZmF1bHQKICBrZWVwX21kOiBmYWxzZQogIG1vZGU6IHNlbGZjb250YWluZWQKICBudW1iZXJfc2VjdGlvbnM6IHRydWUKICBzZWxmX2NvbnRhaW5lZDogdHJ1ZQogIHRoZW1lOiByZWFkYWJsZQogIHRvYzogdHJ1ZQogIHRvY19mbG9hdDoKICAgY29sbGFwc2VkOiBmYWxzZQogICBzbW9vdGhfc2Nyb2xsOiBmYWxzZQotLS0KCjxzdHlsZT4KICBib2R5IC5tYWluLWNvbnRhaW5lciB7CiAgICBtYXgtd2lkdGg6IDE2MDBweDsKICB9Cjwvc3R5bGU+CgpgYGB7ciBvcHRpb25zLCBpbmNsdWRlID0gRkFMU0V9CmxpYnJhcnkoaHBnbHRvb2xzKQp0dCA8LSBkZXZ0b29sczo6bG9hZF9hbGwoIn4vaHBnbHRvb2xzIikKa25pdHI6Om9wdHNfa25pdCRzZXQocHJvZ3Jlc3MgPSBUUlVFLAogICAgICAgICAgICAgICAgICAgICB2ZXJib3NlID0gVFJVRSwKICAgICAgICAgICAgICAgICAgICAgd2lkdGggPSA5MCwKICAgICAgICAgICAgICAgICAgICAgZWNobyA9IFRSVUUpCmtuaXRyOjpvcHRzX2NodW5rJHNldChlcnJvciA9IFRSVUUsCiAgICAgICAgICAgICAgICAgICAgICBmaWcud2lkdGggPSA4LAogICAgICAgICAgICAgICAgICAgICAgZmlnLmhlaWdodCA9IDgsCiAgICAgICAgICAgICAgICAgICAgICBkcGkgPSA5NikKb2xkX29wdGlvbnMgPC0gb3B0aW9ucyhkaWdpdHMgPSA0LAogICAgICAgICAgICAgICAgICAgICAgIHN0cmluZ3NBc0ZhY3RvcnMgPSBGQUxTRSwKICAgICAgICAgICAgICAgICAgICAgICBrbml0ci5kdXBsaWNhdGUubGFiZWwgPSAiYWxsb3ciKQpnZ3Bsb3QyOjp0aGVtZV9zZXQoZ2dwbG90Mjo6dGhlbWVfYncoYmFzZV9zaXplID0gMTIpKQp2ZXIgPC0gIjIwMjIxMiIKcnVuZGF0ZSA8LSBmb3JtYXQoU3lzLkRhdGUoKSwgZm9ybWF0ID0gIiVZJW0lZCIpCgojIyB0bXAgPC0gdHJ5KHNtKGxvYWRtZShmaWxlbmFtZSA9IGdzdWIocGF0dGVybiA9ICJcXC5SbWQiLCByZXBsYWNlID0gIlxcLnJkYVxcLnh6IiwgeCA9IHByZXZpb3VzX2ZpbGUpKSkpCnJtZF9maWxlIDwtIGdsdWU6OmdsdWUoInRtcmMyX2NsYXNzaWZpZXJfe3Zlcn0uUm1kIikKc2F2ZWZpbGUgPC0gZ3N1YihwYXR0ZXJuID0gIlxcLlJtZCIsIHJlcGxhY2UgPSAiXFwucmRhXFwueHoiLCB4ID0gcm1kX2ZpbGUpCmxvYWRlZCA8LSBsb2FkKGZpbGU9Z2x1ZTo6Z2x1ZSgicmRhL2JvdGhfc25wcy12e3Zlcn0ucmRhIikpCmBgYAoKIyBDcmVhdGluZyBhIHN0cmFpbiBjbGFzc2lmaWVyIHVzaW5nIG91ciBkYXRhCgpJIGhhdmUgbm90IHlldCBpbXBsZW1lbnRlZCBhIHJlYWwgTUwgY2xhc3NpZmllciwgc28gSSB3aWxsIHVzZSBzb21lIG9mCnRoZSBjYXJldCBkb2N1bWVudGF0aW9uIGluIG9yZGVyIHRvIHdyaXRlIGEgdHJhaW5lciBhbmQgY2xhc3NpZmllciBvZgp0aGUgVE1SQzIgc3RyYWlucy4KCiMgU3BsaXR0aW5nIHRoZSBkYXRhIGJhc2VkIG9uIG91dGNvbWUKCkkgYW0gdGFraW5nIHRoaXMgcHJldHR5IG11Y2ggZGlyZWN0bHkgZnJvbToKCmh0dHBzOi8vdG9wZXBvLmdpdGh1Yi5pby9jYXJldC9kYXRhLXNwbGl0dGluZy5odG1sI3NpbXBsZS1zcGxpdHRpbmctYmFzZWQtb24tdGhlLW91dGNvbWUKCkluIG9yZGVyIHRvIHBlcmZvcm0gdGhlIHNwbGl0IGJ5IG91dGNvbWUsIEkgdGhpbmsgSSBuZWVkIHRvIHNsaWdodGx5CnJlZm9ybWF0IHRoZSBzdHJ1Y3R1cmUgb2Ygb3VyIHZhcmlhbnQgZGF0YSB0byBtb3JlIGNsb3NlbHkgbWF0Y2ggd2hhdApJIHNlZSBpbiB0aGUgZGF0YXNldHM6IGV0aXRhbmljL2lyaXMvU29uYXIuCgojIyBEZWZpbmUgdGhlIG91dGNvbWUgZmFjdG9yCgpTZXR0aW5nIHRoaXMgdG8gY2hhcmFjdGVyIGhlcmUgYmVjYXVzZSBJIHdpbGwgYmUgbWVzc2luZyB3aXRoIHRoZQpmYWN0b3IgbGV2ZWxzIGFuZCBJIHdvdWxkIHJhdGhlciBub3QgaGF2ZSB0byByZWxldmVsIG11bHRpcGxlIHRpbWVzLgoKYGBge3IgY2xhc3NpZnlfc25wc30Kb3V0Y29tZV9mYWN0b3IgPC0gYXMuY2hhcmFjdGVyKHBEYXRhKGJvdGhfc25wcylbWyJ6eW1vZGVtZWNhdGVnb3JpY2FsIl1dKQpgYGAKCiMjIEZpZ3VyZSBvdXQgd2h5IG9sZCBzYW1wbGVzIHdlcmUgZHJvcHBlZC4KCkluIG15IHByZXZpb3VzIGF0dGVtcHQgYXQgdGhpcywgSSBub3RpY2VkIHRoYXQgdGhlIG9sZGVyIHNhbXBsZXMgd2VyZQpnZXR0aW5nIGRyb3BwZWQgYnkgbmEub21pdCwgSSB3YW50IHRvIHRha2UgYSBtb21lbnQgaGVyZSB0byBmaWd1cmUgb3V0CndoeS4KCmBgYHtyIGNoZWNrX29sZF9zYW1wbGVzfQpkaW0oZXhwcnMoYm90aF9zbnBzKSkKYm90aF9ub3JtIDwtIG5vcm1hbGl6ZV9leHB0KGJvdGhfc25wcywgdHJhbnNmb3JtPSJsb2cyIiwgbm9ybT0icXVhbnQiKQpgYGAKCiMjIEZpbHRlciB0aGUgZGF0YSB1c2luZyBuZWFyLXplcm8gdmFyaWFuY2UKClN0ZWFsaW5nIG5vdyBmcm9tOgoKaHR0cHM6Ly9naXRodWIuY29tL2NvbXBnZW5vbXIvYm9vay9ibG9iL21hc3Rlci8wNS1zdXBlcnZpc2VkTGVhcm5pbmcuUm1kCgpNdWx0aXBsZSBzZWN0aW9ucyBvZiB0aGUgYWJvdmUgZG9jdW1lbnQgcHJvdmlkZSBuaWNlIHdheXMgdG8gcmVtb3ZlCnJlZHVuZGFudCBwcmVkaWN0b3IgdmFyaWFibGVzLiAgRm9yIG15IHB1cnBvc2VzLCBJIHdvdWxkIGxpa2UgdG8ga2VlcAphbGwgb2YgdGhlbSBiZWNhdXNlIEkgYW0gaG9waW5nIHRvIG1ha2UgdXNlIG9mIHRoZSBzYW1lIG1vZGVsIGluIGEKZGF0YXNldCB3aGljaCBpcyB2YXN0bHkgc3BhcnNlciB0aGFuIHRoaXMuICBIb3dldmVyLCBmb3IgdGhlIHB1cnBvc2VzCm9mIG1lIGxlYXJuaW5nLCBJIHdpbGwgYXBwbHkgYWxsIG9mIHRoZXNlIG1ldGhvZHMgYW5kIHVzZSBpdCBvbiB0aGUKcmVzdCBvZiB0aGUgVE1SQzIgZGF0YS4KCmBgYHtyIG56dn0KdGV4cHJzIDwtIHQoZXhwcnMoYm90aF9ub3JtKSkKbnp2IDwtIGNhcmV0OjpwcmVQcm9jZXNzKHRleHBycywgbWV0aG9kPSJuenYiLCB1bmlxdWVDdXQ9MTUpCm56dl90ZXhwcnMgPC0gcHJlZGljdChuenYsIHRleHBycykKZGltKG56dl90ZXhwcnMpCgpzdGFuZGFyZF9kZXZzIDwtIGFwcGx5KG56dl90ZXhwcnMsIDIsIHNkKQp0b3BfcHJlZGljdG9ycyA8LSBvcmRlcihzdGFuZGFyZF9kZXZzLCBkZWNyZWFzaW5nID0gVFJVRSlbMToyMDAwXQpuenZfdGV4cHJzIDwtIG56dl90ZXhwcnNbLCB0b3BfcHJlZGljdG9yc10KYGBgCgojIyBDZW50ZXIgdGhlIGRhdGEKCkkgdGhpbmsgY2VudGVyaW5nIG1heSBub3QgYmUgbmVlZGVkIGZvciB0aGlzIGRhdGEsIGJ1dCBoZXJlIGlzIGhvdzoKCmBgYHtyIGNlbnRlcn0Kbnp2X2NlbnRlciA8LSBwcmVQcm9jZXNzKG56dl90ZXhwcnMsIG1ldGhvZCA9ICJjZW50ZXIiKQpuenZfdGV4cHJzIDwtIHByZWRpY3Qobnp2X2NlbnRlciwgbnp2X3RleHBycykKYGBgCgojIyBEcm9wIGNvcnJlbGF0ZWQKCmBgYHtyIGNvcnJlbGF0ZWR9Cm56dl9jb3JyZWxhdGVkIDwtIHByZVByb2Nlc3Mobnp2X3RleHBycywgbWV0aG9kID0gImNvcnIiLCBjdXRvZmYgPSAwLjkpCm56dl90ZXhwcnMgPC0gcHJlZGljdChuenZfY29ycmVsYXRlZCwgbnp2X3RleHBycykKZGltKG56dl90ZXhwcnMpCmFueU5BKG56dl90ZXhwcnMpCmBgYAoKIyMgUmVmb3JtYXQgdGhlIG1ldGFkYXRhIHRvIGJlIHVzZWQgYnkgY2FyZXQKCmBgYHtyIHJlZm9ybWF0X2RhdGF9Cm5hX2lkeCA8LSBpcy5uYShvdXRjb21lX2ZhY3RvcikKb3V0Y29tZV9mYWN0b3JbbmFfaWR4XSA8LSAidW5rbm93biIKbWV0YV9mYWN0b3JzIDwtIHBEYXRhKGJvdGhfc25wcylbLCBjKCJ6eW1vZGVtZWNhdGVnb3JpY2FsIiwgImNsaW5pY2FsY2F0ZWdvcmljYWwiLCAic3VzX2NhdGVnb3J5X2N1cnJlbnQiKV0KbWV0YV9mYWN0b3JzW1sienltb2RlbWVjYXRlZ29yaWNhbCJdXSA8LSBvdXRjb21lX2ZhY3RvcgpgYGAKCiMjIEFkZCB0aGUgbWV0YWRhdGEgdG8gdGhlIGZpbHRlcmVkIHZhcmlhYmxlcwoKSW4gYWRkaXRpb24sIEkgd2FudCBtYWtlIHN1cmUgdGhhdCBubyB1bmtub3duIHNhbXBsZXMgYXJlIHVzZWQgaW4gdHJhaW5pbmcuCgpgYGB7ciBtZXJnZV9waWVjZXN9CnNucF9kZiA8LSBjYmluZChtZXRhX2ZhY3RvcnMsIG56dl90ZXhwcnMpCmRpbShzbnBfZGYpCnNucF90cmFpbl9pZHggPC0gY3JlYXRlRGF0YVBhcnRpdGlvbihvdXRjb21lX2ZhY3RvciwgcCA9IDAuNCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGxpc3QgPSBGQUxTRSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRpbWVzID0gMSkKc3VtbWFyeShvdXRjb21lX2ZhY3RvcltzbnBfdHJhaW5faWR4XSkKc25wX3RyYWluX3N0YXJ0IDwtIHJvd25hbWVzKHNucF9kZilbc25wX3RyYWluX2lkeF0KdHJhaW5fa25vd25faWR4IDwtIG1ldGFfZmFjdG9yc1tzbnBfdHJhaW5fc3RhcnQsICJ6eW1vZGVtZWNhdGVnb3JpY2FsIl0gIT0gInVua25vd24iCnRyYWluX3Jvd25hbWVzIDwtIHNucF90cmFpbl9zdGFydFt0cmFpbl9rbm93bl9pZHhdCm5vdF90cmFpbl9pZHggPC0gISByb3duYW1lcyhzbnBfZGYpICVpbiUgdHJhaW5fcm93bmFtZXMKbm90X3RyYWluX3Jvd25hbWVzIDwtIHJvd25hbWVzKHNucF9kZilbbm90X3RyYWluX2lkeF0KCnRyYWluX2RmIDwtIHNucF9kZlt0cmFpbl9yb3duYW1lcywgXQp0cmFpbl9kZltbImNsaW5pY2FsY2F0ZWdvcmljYWwiXV0gPC0gTlVMTAp0cmFpbl9kZltbInN1c19jYXRlZ29yeV9jdXJyZW50Il1dIDwtIE5VTEwKdHJhaW5fZGZbWzFdXSA8LSBhcy5mYWN0b3IoYXMuY2hhcmFjdGVyKHRyYWluX2RmW1sxXV0pKQp0ZXN0X2RmIDwtIHNucF9kZltub3RfdHJhaW5fcm93bmFtZXMsIF0KdGVzdF9kZltbInp5bW9kZW1lY2F0ZWdvcmljYWwiXV0gPC0gTlVMTAp0ZXN0X2RmW1siY2xpbmljYWxjYXRlZ29yaWNhbCJdXSA8LSBOVUxMCnRlc3RfZGZbWyJzdXNfY2F0ZWdvcnlfY3VycmVudCJdXSA8LSBOVUxMCmBgYAoKIyMgS05OCgpQZXJmb3JtIHRoZSBmaXQgYW5kIHRlc3QgdGhlIHRyYWluaW5nIHNldCBhZ2FpbnN0IGl0c2VsZi4KCmBgYHtyIGtubn0Ka25uX2ZpdCA8LSBrbm4zKHggPSB0cmFpbl9kZlssIC0xXSwKICAgICAgICAgICAgICAgIHkgPSB0cmFpbl9kZltbInp5bW9kZW1lY2F0ZWdvcmljYWwiXV0sCiAgICAgICAgICAgICAgICBrID0gNSkKdHJhaW5fcHJlZGljdCA8LSBwcmVkaWN0KGtubl9maXQsIHRyYWluX2RmWywgLTFdLCB0eXBlID0gImNsYXNzIikKY29uZnVzaW9uTWF0cml4KGRhdGEgPSB0cmFpbl9kZltbMV1dLCByZWZlcmVuY2UgPSB0cmFpbl9wcmVkaWN0KQoKdHJhaW5fZGZbWzFdXQp0cmFpbl9wcmVkaWN0CmBgYAoKSXQgbG9va3MgbGlrZSBteSBrbm4gbW9kZWwgaGFzIHNvbWUgZGlzYWdyZWVtZW50cyBpbiB0aGUgYWxyZWFkeQpkaWZmaWN1bHQgY2FzZXMgb2YgcG9vcmx5IHJlcHJlc2VudGVkIHN0cmFpbnMsIHNvIEkgd2lsbCBjYWxsIGl0IGEKd2luLiAgRS5nIGl0IGFwcGVhcnMgdG8gZmFpbCBvbiBhIHoxLjAsIGIyOTA0LCB6My4wLCB6Mi4wLCBhIDIuMSwgYW5kCjIuNC4KCk5vdyBsZXRzIHRyeSBpdCBvdXQgb24gdGhlIHJlc3Qgb2YgdGhlIGRhdGEuCgpgYGB7ciBrbm5fdGVzdH0KdGVzdF9wcmVkaWN0IDwtIHByZWRpY3Qoa25uX2ZpdCwgdGVzdF9kZiwgdHlwZSA9ICJjbGFzcyIpCm5hbWVzKHRlc3RfcHJlZGljdCkgPC0gcm93bmFtZXModGVzdF9kZikKdGVzdF9wcmVkaWN0CmBgYAoKTm93IGxldCB1cyBtYW51YWxseSBjb21wYXJlIHRoZXNlIHRvIHRoZSBzYW1wbGUgc2hlZXQgYW5kIHNlZSBob3cgd2UKZGlkLi4uCgp8LS0tLS0tLS0tLXwtLS0tLS0tLS0tfC0tLS0tfC0tLS0tLXwtLS0tfAp8U2FtcGxlICAgIHxBbm5vdGF0aW9ufE15TUwgfEFncmVlP3xJR1YgfAp8VE1SQzIwMDAxIHx6Mi4zICAgICAgfHoxLjAgfE5vICAgIHxJIGFncmVlIGl0IGlzIG5vdCBhIDIuMywgYnV0IHRvIG1lIGl0IGRvZXNuJ3QgbG9vayBsaWtlIGEgMS4wICAgIHwKfFRNUkMyMDAwNyB8dW5rbm93biAgIHx6Mi4zIHwgICAgICB8ICAgIHwKfFRNUkMyMDAwOCB8dW5rbm93biAgIHx6Mi4zIHwgICAgICB8ICAgIHwKfFRNUkMyMDAzOSB8ejIuMiAgICAgIHx6Mi4yIHxZZXMgICB8ICAgIHwKfFRNUkMyMDAzNyB8ejIuMyAgICAgIHx6Mi4zIHxZZXMgICB8ICAgIHwKfFRNUkMyMDAzOCB8ejIuMyAgICAgIHx6Mi4zIHxZZXMgICB8ICAgIHwKfFRNUkMyMDA2OCB8ejIuMyAgICAgIHx6Mi4zIHxZZXMgICB8ICAgIHwKfFRNUkMyMDA0MSB8ejIuMiAgICAgIHx6Mi4xIHxObyAgICB8bWF5YmUgbW9yZSBsaWtlIDIuMj8gIEl0IGRvZXNuJ3QgbG9vayBsaWtlIGVpdGhlciB0byBtZSAgICB8CnxUTVJDMjAwMDkgfHoyLjIgICAgICB8ejIuMiB8WWVzICAgfCAgICB8CnxUTVJDMjAwMTAgfHoyLjMgICAgICB8ejIuMyB8IFllcyAgfCAgICB8CnxUTVJDMjAwMTYgfHoyLjMgICAgICB8ejIuMyB8IFllcyAgfHwKfFRNUkMyMDAxNCB8ejIuMnx6Mi4yfFllc3x8CnxUTVJDMjAwMTkgfHoyLjIgfHoyLjJ8WWVzfHwKfFRNUkMyMDA3MCB8ejIuMyB8ejIuM3xZZXN8fAp8VE1SQzIwMDIxIHx6Mi4zIHx6Mi4zfFllc3x8CnxUTVJDMjAwMjIgfHoyLjIgfHoyLjJ8WWVzfHwKfFRNUkMyMDAzNiB8ejIuMiB8ejIuMXxOb3wgSXQgaXMgZGVmaW5pdGVseSBhbiBvZGQgMi4yOyBJIGNhbiBzZWUgd2h5IGl0IHNhaWQgaXQgaXMgMi4xfAp8VE1SQzIwMDMxIHx6Mi4yIHx6Mi4yfFllc3x8CnxUTVJDMjAwNzYgfHoyLjJ8ejIuMnxZZXN8fAp8VE1SQzIwMDczIHx6Mi4zfHoyLjN8WWVzfHwKfFRNUkMyMDA3OCB8ejIuMnx6Mi4yfFllc3x8CnxUTVJDMjAwNDIgfHoyLjJ8ejIuMnxZZXN8fAp8VE1SQzIwMDU5IHx6Mi4zfHoyLjN8WWVzfHwKfFRNUkMyMDA0OCB8ejIuM3x6Mi4zfFllc3x8CnxUTVJDMjAwODggfHoyLjJ8ejIuMnxZZXN8fAp8VE1SQzIwMDc3IHx6Mi4yfHoyLjJ8WWVzfHwKfFRNUkMyMDA3NCB8ejIuMnx6Mi4yfFllc3x8CnxUTVJDMjAwNjMgfHoyLjJ8ejIuMnxZZXN8fAp8VE1SQzIwMDUyIHx6Mi4zfHoyLjN8WWVzfHwKfFRNUkMyMDA2NCB8ejIuM3x6Mi4zfFllc3x8CnxUTVJDMjAwNzUgfHoyLjN8ejIuM3xZZXN8fAp8VE1SQzIwMDUwIHx6Mi4yfHoyLjJ8WWVzfHwKfFRNUkMyMDA0OSB8ejIuMnx6Mi4yfFllc3x8CnxUTVJDMjAwNjIgfHoyLjN8ejIuM3xZZXN8fAp8VE1SQzIwMTEwIHx6Mi4yfHoyLjJ8WWVzfHwKfFRNUkMyMDA4MCB8ejIuM3x6Mi4zfFllc3x8CnxUTVJDMjAwNDMgfHoyLjN8ejIuM3xZZXN8fAp8VE1SQzIwMDg1IHx6Mi4zfHoyLjN8WWVzfHwKfFRNUkMyMDA0NiB8ejIuMnx6Mi4xfE5vfFRoaXMgbG9va3MgYWxtb3N0IGV4YWN0bHkgbGlrZSBUTVJDMjAwMzZ8CnxUTVJDMjAwOTMgfHoyLjF8ejIuMXxZZXN8SW50ZXJlc3RpbmdseSwgdGhpcyBkb2VzIG5vdCBsb29rIGxpa2UgVE1SQzIwMDQ2IG5vciBUTVJDMjAwMzYgaW4gYWxsIHBsYWNlc3wKfFRNUkMyMDA0NyB8ejIuNHx6Mi4zfE5vfEkgc2VlIHBsYWNlcyB3aGVyZSB0aGlzIGxvb2tzIGxpa2UgaXQgdW5pcXVlbHkgbWF0Y2hlcyBib3RoIHN0cmFpbnMsIHRoZXkgYXJlIGNyYXp5IHNpbWlsYXJ8CnxUTVJDMjAxMDkgfHoyLjJ8ejIuMnxZZXN8fAp8VE1SQzIwMDk2IHx6Mi4yfHoyLjJ8WWVzfHwKfFRNUkMyMDA5NyB8ejIuMnx6Mi4xfE5vfEkgYmV0IGl0IGxvb2tzIGxpa2UgVE1SQzIwMDQ2L1RNUkMyMDAzNiBiZWZvcmUgaXQgb3BlbnMsIGl0IGRvZXMhfAp8VE1SQzIwMTAxIHx6Mi4yfHoyLjJ8WWVzfHwKfFRNUkMyMDEwMiB8ejIuM3x6Mi4zfFllc3x8CnxUTVJDMjAwOTkgfHoyLjN8ejIuM3xZZXN8fAp8VE1SQzIwMTAwIHx6Mi4zfHoyLjN8WWVzfHwKfFRNUkMyMDA5MSB8ejIuMXx6Mi4yfE5vfFRoaXMgbG9va3MgbGlrZSB3aGF0IEkgdGhpbmsgb2YgYXMgdGhlICdjYW5vbmljYWwnIHoyLjIgYW5kIF9ub3RfIGxpa2UgVE1SQzIwMDQ3LnwKfFRNUkMyMDA4NCB8ejIuMXx6Mi4xfFllc3x8CnxUTVJDMjAwODcgfHoyLjJ8ejIuMnxZZXN8fAp8VE1SQzIwMTAzIHx6Mi4xfHoyLjF8WWVzfHwKfFRNUkMyMDEwNCB8ejIuM3x6Mi4zfFllc3x8CnxUTVJDMjAwODYgfHoyLjJ8ejIuMnxZZXN8fAp8VE1SQzIwMTA3IHx6Mi4zfHoyLjN8WWVzfHwKfFRNUkMyMDA5NSB8ejIuM3x6Mi4zfFllc3x8Cg==