Getting Started

Load libraries

list.of.packages <- c("pls","dplyr","reshape2","here","plotrix","ggplot2","gridExtra",
                      "spectratrait")
invisible(lapply(list.of.packages, library, character.only = TRUE))

Attaching package: ‘pls’

The following object is masked from ‘package:stats’:

    loadings


Attaching package: ‘dplyr’

The following objects are masked from ‘package:stats’:

    filter, lag

The following objects are masked from ‘package:base’:

    intersect, setdiff, setequal, union

here() starts at /Users/sserbin/Data/GitHub/spectratrait

Attaching package: ‘gridExtra’

The following object is masked from ‘package:dplyr’:

    combine

Setup other functions and options

### Setup options

# Script options
pls::pls.options(plsralg = "oscorespls")
pls::pls.options("plsralg")
$plsralg
[1] "oscorespls"
# Default par options
opar <- par(no.readonly = T)

# What is the target variable?
inVar <- "LMA_gDW_m2"

# What is the source dataset from EcoSIS?
ecosis_id <- "5617da17-c925-49fb-b395-45a51291bd2d"

# Specify output directory, output_dir 
# Options: 
# tempdir - use a OS-specified temporary directory 
# user defined PATH - e.g. "~/scratch/PLSR"
output_dir <- "tempdir"

Set working directory (scratch space)

The working directory was changed to /private/var/folders/xp/h3k9vf3n2jx181ts786_yjrn9c2gjq/T/RtmpvH9zex inside a notebook chunk. The working directory will be reset when the chunk is finished running. Use the knitr root.dir option in the setup chunk to change the working directory for notebook chunks.
[1] "/private/var/folders/xp/h3k9vf3n2jx181ts786_yjrn9c2gjq/T/RtmpvH9zex"

Grab PLSR Coefficients from GitHub

git_repo <- "https://raw.githubusercontent.com/serbinsh/SSerbin_etal_2019_NewPhytologist/master/"
print("**** Downloading PLSR coefficients ****")
[1] "**** Downloading PLSR coefficients ****"
githubURL <- paste0(git_repo,"SSerbin_multibiome_lma_plsr_model/sqrt_LMA_gDW_m2_PLSR_Coefficients_10comp.csv")
LeafLMA.plsr.coeffs <- spectratrait::source_GitHubData(githubURL)
rm(githubURL)
githubURL <- paste0(git_repo,"SSerbin_multibiome_lma_plsr_model/sqrt_LMA_gDW_m2_Jackkife_PLSR_Coefficients.csv")
LeafLMA.plsr.jk.coeffs <- spectratrait::source_GitHubData(githubURL)
rm(githubURL)

Get source dataset from EcoSIS

dat_raw <- spectratrait::get_ecosis_data(ecosis_id = ecosis_id)
[1] "**** Downloading Ecosis data ****"
Downloading data...
Rows: 6312 Columns: 2162── Column specification ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Delimiter: ","
chr   (10): Affiliation, Common Name, Domain, Functional_type, Latin Genus, Latin Species, PI, Project, Sample_ID, USDA Symbol
dbl (2152): LMA, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, ...
ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.Download complete!
head(dat_raw)
names(dat_raw)[1:40]
 [1] "Affiliation"     "Common Name"     "Domain"          "Functional_type" "LMA"             "Latin Genus"     "Latin Species"   "PI"             
 [9] "Project"         "Sample_ID"       "USDA Symbol"     "350"             "351"             "352"             "353"             "354"            
[17] "355"             "356"             "357"             "358"             "359"             "360"             "361"             "362"            
[25] "363"             "364"             "365"             "366"             "367"             "368"             "369"             "370"            
[33] "371"             "372"             "373"             "374"             "375"             "376"             "377"             "378"            

Prepare new data for estimation

Start.wave <- 500
End.wave <- 2400
wv <- seq(Start.wave,End.wave,1)
Spectra <- as.matrix(dat_raw[,names(dat_raw) %in% wv])
colnames(Spectra) <- c(paste0("Wave_",wv))
head(Spectra)[1:6,1:10]
     Wave_500 Wave_501 Wave_502 Wave_503 Wave_504 Wave_505 Wave_506 Wave_507 Wave_508 Wave_509
[1,] 0.044226 0.044605 0.044927 0.045473 0.046241 0.046878 0.047826 0.049090 0.050268 0.051525
[2,] 0.046855 0.047601 0.047944 0.048478 0.049381 0.050235 0.051161 0.052191 0.053322 0.054357
[3,] 0.043758 0.044171 0.044869 0.045465 0.045984 0.046933 0.047993 0.049090 0.050168 0.051441
[4,] 0.041154 0.041603 0.042088 0.042408 0.042639 0.043260 0.044140 0.045058 0.045700 0.046476
[5,] 0.037296 0.037944 0.038209 0.038677 0.039388 0.039948 0.040630 0.041501 0.042613 0.043731
[6,] 0.043878 0.044257 0.044723 0.045295 0.045949 0.046575 0.047378 0.048357 0.049392 0.050387
sample_info <- dat_raw[,names(dat_raw) %notin% seq(350,2500,1)]
head(sample_info)

sample_info2 <- sample_info %>%
  select(Domain,Functional_type,Sample_ID,USDA_Species_Code=`USDA Symbol`,LMA_gDW_m2=LMA)
head(sample_info2)

plsr_data <- data.frame(sample_info2,Spectra)
rm(sample_info,sample_info2,Spectra)

Example data cleaning.

#### End user needs to do what's appropriate for their data.  This may be an iterative process.
# Keep only complete rows of inVar and spec data before fitting
plsr_data <- plsr_data[complete.cases(plsr_data[,names(plsr_data) %in% 
                                                  c(inVar,paste0("Wave_",wv))]),]

Prepare PLSR model

print("**** Applying PLSR model to estimate LMA from spectral observations ****")
[1] "**** Applying PLSR model to estimate LMA from spectral observations ****"
# setup model
dims <- dim(LeafLMA.plsr.coeffs)
LeafLMA.plsr.intercept <- LeafLMA.plsr.coeffs[1,]
LeafLMA.plsr.coeffs <- data.frame(LeafLMA.plsr.coeffs[2:dims[1],])
names(LeafLMA.plsr.coeffs) <- c("wavelength","coefs")
LeafLMA.plsr.coeffs.vec <- as.vector(LeafLMA.plsr.coeffs[,2])
sub_spec <- droplevels(plsr_data[,which(names(plsr_data) %in% 
                                                   paste0("Wave_",seq(Start.wave,End.wave,1)))])

Apply PLSR model

plsr_pred <- as.matrix(sub_spec) %*% LeafLMA.plsr.coeffs.vec + LeafLMA.plsr.intercept[,2]
leafLMA <- plsr_pred[,1]^2  # convert to standard LMA units from sqrt(LMA)
names(leafLMA) <- "PLSR_LMA_gDW_m2"

# organize output
LeafLMA.PLSR.dataset <- data.frame(plsr_data[,which(names(plsr_data) %notin% 
                                                      paste0("Wave_",seq(Start.wave,End.wave,1)))],
                                   PLSR_LMA_gDW_m2=leafLMA, PLSR_Residuals=leafLMA-plsr_data[,inVar])
head(LeafLMA.PLSR.dataset)

Generate PLSR uncertainty estimates

print("**** Generate PLSR uncertainty estimates ****")
[1] "**** Generate PLSR uncertainty estimates ****"
jk_coef <- data.frame(LeafLMA.plsr.jk.coeffs[,3:dim(LeafLMA.plsr.jk.coeffs)[2]])
jk_coef <- t(jk_coef)
head(jk_coef)[,1:6]
              [,1]      [,2]      [,3]      [,4]      [,5]      [,6]
Wave_500 1.0005875 0.9952840 0.5652908 0.9793160 1.1052207 0.9370473
Wave_501 0.9584235 0.9631434 0.5230544 0.9330803 1.0477469 0.9042780
Wave_502 0.8960202 0.9065954 0.4597413 0.8710298 0.9658130 0.8628370
Wave_503 0.8722135 0.8936197 0.4420696 0.8456098 0.9272967 0.8513741
Wave_504 0.8452831 0.8644923 0.4159567 0.8110004 0.8903192 0.8320347
Wave_505 0.8240743 0.8378399 0.3902871 0.7829891 0.8570048 0.8150339
jk_int <- t(LeafLMA.plsr.jk.coeffs[,2])
head(jk_int)[,1:6]
[1] 7.787098 7.959443 8.015161 8.018586 7.658080 7.998432
jk_pred <- as.matrix(sub_spec) %*% jk_coef + matrix(rep(jk_int, length(plsr_data[,inVar])), 
                                                    byrow=TRUE, ncol=length(jk_int))
jk_pred <- jk_pred^2
head(jk_pred)[,1:6]
      [,1]      [,2]      [,3]     [,4]      [,5]      [,6]
1 94.28721  96.77712  96.44452 95.11992  96.72830  95.33877
2 90.36051  90.57120  90.77562 89.77821  90.24826  89.61806
3 75.71088  77.91861  76.42730 76.11473  77.67179  76.68756
4 61.37001  61.30963  60.56606 60.72330  61.63712  60.69649
5 99.24456 101.75948 101.22916 99.96305 101.70397 100.16758
6 97.40414  97.65463  97.52687 97.00817  97.33677  96.08535
dim(jk_pred)
[1] 6312 1000
interval <- c(0.025,0.975)
Interval_Conf <- apply(X = jk_pred, MARGIN = 1, FUN = quantile, 
                       probs=c(interval[1], interval[2]))
sd_mean <- apply(X = jk_pred, MARGIN = 1, FUN =sd)
sd_res <- sd(LeafLMA.PLSR.dataset$PLSR_Residuals)
sd_tot <- sqrt(sd_mean^2+sd_res^2)
LeafLMA.PLSR.dataset$LCI <- Interval_Conf[1,]
LeafLMA.PLSR.dataset$UCI <- Interval_Conf[2,]
LeafLMA.PLSR.dataset$LPI <- LeafLMA.PLSR.dataset$PLSR_LMA_gDW_m2-1.96*sd_tot
LeafLMA.PLSR.dataset$UPI <- LeafLMA.PLSR.dataset$PLSR_LMA_gDW_m2+1.96*sd_tot
head(LeafLMA.PLSR.dataset)

Generate PLSR estimated LMA observed vs predicted plot

rmsep_percrmsep <- spectratrait::percent_rmse(plsr_dataset = LeafLMA.PLSR.dataset, 
                                              inVar = inVar, 
                                              residuals = LeafLMA.PLSR.dataset$PLSR_Residuals, 
                                              range="full")
RMSEP <- rmsep_percrmsep$rmse
perc_RMSEP <- rmsep_percrmsep$perc_rmse
r2 <- round(summary(lm(LeafLMA.PLSR.dataset$PLSR_LMA_gDW_m2~
                         LeafLMA.PLSR.dataset[,inVar]))$adj.r.squared,2)
expr <- vector("expression", 3)
expr[[1]] <- bquote(R^2==.(r2))
expr[[2]] <- bquote(RMSEP==.(round(RMSEP,2)))
expr[[3]] <- bquote("%RMSEP"==.(round(perc_RMSEP,2)))
rng_vals <- c(min(LeafLMA.PLSR.dataset$LPI), max(LeafLMA.PLSR.dataset$UPI))
par(mfrow=c(1,1), mar=c(4.2,5.3,1,0.4), oma=c(0, 0.1, 0, 0.2))
plotrix::plotCI(LeafLMA.PLSR.dataset$PLSR_LMA_gDW_m2,LeafLMA.PLSR.dataset[,inVar], 
                li=LeafLMA.PLSR.dataset$LPI, ui=LeafLMA.PLSR.dataset$UPI, gap=0.009,sfrac=0.000, 
                lwd=1.6, xlim=c(rng_vals[1], rng_vals[2]), ylim=c(rng_vals[1], rng_vals[2]), 
                err="x", pch=21, col="black", pt.bg=scales::alpha("grey70",0.7), scol="grey80",
                cex=2, xlab=paste0("Predicted ", paste(inVar), " (units)"),
                ylab=paste0("Observed ", paste(inVar), " (units)"),
                cex.axis=1.5,cex.lab=1.8)
abline(0,1,lty=2,lw=2)
plotrix::plotCI(LeafLMA.PLSR.dataset$PLSR_LMA_gDW_m2,LeafLMA.PLSR.dataset[,inVar], 
                li=LeafLMA.PLSR.dataset$LCI, ui=LeafLMA.PLSR.dataset$UCI, gap=0.009,sfrac=0.004, 
                lwd=1.6, xlim=c(rng_vals[1], rng_vals[2]), ylim=c(rng_vals[1], rng_vals[2]), 
                err="x", pch=21, col="black", pt.bg=scales::alpha("grey70",0.7), scol="black",
                cex=2, xlab=paste0("Predicted ", paste(inVar), " (units)"),
                ylab=paste0("Observed ", paste(inVar), " (units)"),
                cex.axis=1.5,cex.lab=1.8, add=T)
legend("topleft", legend=expr, bty="n", cex=1.5)
legend("bottomright", legend=c("Prediction Interval","Confidence Interval"), 
       lty=c(1,1), col = c("grey80","black"), lwd=3, bty="n", cex=1.5)
box(lwd=2.2)
dev.copy(png,file.path(outdir,paste0(inVar,"_PLSR_Validation_Scatterplot.png")), 
         height=2800, width=3200,  res=340)
quartz_off_screen 
                3 
dev.off();
quartz_off_screen 
                2 

print(paste("Output directory: ", outdir))
[1] "Output directory:  /var/folders/xp/h3k9vf3n2jx181ts786_yjrn9c2gjq/T//RtmpvH9zex"
# Observed versus predicted
write.csv(LeafLMA.PLSR.dataset,file=file.path(outdir,
                                         paste0(inVar,'_PLSR_Estimates.csv')),
          row.names=FALSE)

Confirm files were written to temp space

print("**** PLSR output files: ")
[1] "**** PLSR output files: "
print(list.files(outdir)[grep(pattern = inVar, list.files(outdir))])
[1] "LMA_gDW_m2_PLSR_Estimates.csv"              "LMA_gDW_m2_PLSR_Validation_Scatterplot.png"
LS0tCnRpdGxlOiBBbiBleGFtcGxlIHNob3dpbmcgaG93IHRvIGFwcGx5IGFuIGV4aXN0aW5nIFBMU1IgbW9kZWwgdG8gbmV3IGRhdGEuIEluIHRoaXMgY2FzZSBhcHBseWluZyB0aGUgTE1BIG1vZGVsIGZyb20gU2VyYmluIGV0IGFsLiwgKDIwMTk7IERPSSAtIDEwLjExMTEvbnBoLjE2MTIzKSB0byBhIGRhdGFzZXQgY29sbGVjdGVkIGF0IENPTlVTIE5FT04gZmllbGQgc2l0ZXMgCmF1dGhvcjogIlNoYXduIFAuIFNlcmJpbiwgSnVsaWVuIExhbW91ciwgJiBKZXJlbWlhaCBBbmRlcnNvbiIKZGF0ZTogImByIFN5cy5EYXRlKClgIgpvdXRwdXQ6CiAgaHRtbF9ub3RlYm9vazogZGVmYXVsdAotLS0KCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFLCBlY2hvPUZBTFNFfQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IFRSVUUpCmBgYAoKIyMjIEdldHRpbmcgU3RhcnRlZAojIyMgTG9hZCBsaWJyYXJpZXMKYGBge3IsIGV2YWw9VFJVRSwgZWNobz1UUlVFfQpsaXN0Lm9mLnBhY2thZ2VzIDwtIGMoInBscyIsImRwbHlyIiwicmVzaGFwZTIiLCJoZXJlIiwicGxvdHJpeCIsImdncGxvdDIiLCJncmlkRXh0cmEiLAogICAgICAgICAgICAgICAgICAgICAgInNwZWN0cmF0cmFpdCIpCmludmlzaWJsZShsYXBwbHkobGlzdC5vZi5wYWNrYWdlcywgbGlicmFyeSwgY2hhcmFjdGVyLm9ubHkgPSBUUlVFKSkKYGBgCgojIyMgU2V0dXAgb3RoZXIgZnVuY3Rpb25zIGFuZCBvcHRpb25zCmBgYHtyLCBlY2hvPVRSVUV9CiMjIyBTZXR1cCBvcHRpb25zCgojIFNjcmlwdCBvcHRpb25zCnBsczo6cGxzLm9wdGlvbnMocGxzcmFsZyA9ICJvc2NvcmVzcGxzIikKcGxzOjpwbHMub3B0aW9ucygicGxzcmFsZyIpCgojIERlZmF1bHQgcGFyIG9wdGlvbnMKb3BhciA8LSBwYXIobm8ucmVhZG9ubHkgPSBUKQoKIyBXaGF0IGlzIHRoZSB0YXJnZXQgdmFyaWFibGU/CmluVmFyIDwtICJMTUFfZ0RXX20yIgoKIyBXaGF0IGlzIHRoZSBzb3VyY2UgZGF0YXNldCBmcm9tIEVjb1NJUz8KZWNvc2lzX2lkIDwtICI1NjE3ZGExNy1jOTI1LTQ5ZmItYjM5NS00NWE1MTI5MWJkMmQiCgojIFNwZWNpZnkgb3V0cHV0IGRpcmVjdG9yeSwgb3V0cHV0X2RpciAKIyBPcHRpb25zOiAKIyB0ZW1wZGlyIC0gdXNlIGEgT1Mtc3BlY2lmaWVkIHRlbXBvcmFyeSBkaXJlY3RvcnkgCiMgdXNlciBkZWZpbmVkIFBBVEggLSBlLmcuICJ+L3NjcmF0Y2gvUExTUiIKb3V0cHV0X2RpciA8LSAidGVtcGRpciIKYGBgCgojIyMgU2V0IHdvcmtpbmcgZGlyZWN0b3J5IChzY3JhdGNoIHNwYWNlKQpgYGB7ciwgZWNobz1GQUxTRX0KaWYgKG91dHB1dF9kaXI9PSJ0ZW1wZGlyIikgewogIG91dGRpciA8LSB0ZW1wZGlyKCkKfSBlbHNlIHsKICBpZiAoISBmaWxlLmV4aXN0cyhvdXRwdXRfZGlyKSkgZGlyLmNyZWF0ZShvdXRwdXRfZGlyLHJlY3Vyc2l2ZT1UUlVFKQogIG91dGRpciA8LSBmaWxlLnBhdGgocGF0aC5leHBhbmQob3V0cHV0X2RpcikpCn0Kc2V0d2Qob3V0ZGlyKSAjIHNldCB3b3JraW5nIGRpcmVjdG9yeQpnZXR3ZCgpICAjIGNoZWNrIHdkCmBgYAoKIyMjIEdyYWIgUExTUiBDb2VmZmljaWVudHMgZnJvbSBHaXRIdWIKYGBge3IsIGVjaG89VFJVRX0KZ2l0X3JlcG8gPC0gImh0dHBzOi8vcmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbS9zZXJiaW5zaC9TU2VyYmluX2V0YWxfMjAxOV9OZXdQaHl0b2xvZ2lzdC9tYXN0ZXIvIgpwcmludCgiKioqKiBEb3dubG9hZGluZyBQTFNSIGNvZWZmaWNpZW50cyAqKioqIikKZ2l0aHViVVJMIDwtIHBhc3RlMChnaXRfcmVwbywiU1NlcmJpbl9tdWx0aWJpb21lX2xtYV9wbHNyX21vZGVsL3NxcnRfTE1BX2dEV19tMl9QTFNSX0NvZWZmaWNpZW50c18xMGNvbXAuY3N2IikKTGVhZkxNQS5wbHNyLmNvZWZmcyA8LSBzcGVjdHJhdHJhaXQ6OnNvdXJjZV9HaXRIdWJEYXRhKGdpdGh1YlVSTCkKcm0oZ2l0aHViVVJMKQpnaXRodWJVUkwgPC0gcGFzdGUwKGdpdF9yZXBvLCJTU2VyYmluX211bHRpYmlvbWVfbG1hX3Bsc3JfbW9kZWwvc3FydF9MTUFfZ0RXX20yX0phY2traWZlX1BMU1JfQ29lZmZpY2llbnRzLmNzdiIpCkxlYWZMTUEucGxzci5qay5jb2VmZnMgPC0gc3BlY3RyYXRyYWl0Ojpzb3VyY2VfR2l0SHViRGF0YShnaXRodWJVUkwpCnJtKGdpdGh1YlVSTCkKYGBgCgojIyMgR2V0IHNvdXJjZSBkYXRhc2V0IGZyb20gRWNvU0lTCmBgYHtyLCBlY2hvPVRSVUV9CmRhdF9yYXcgPC0gc3BlY3RyYXRyYWl0OjpnZXRfZWNvc2lzX2RhdGEoZWNvc2lzX2lkID0gZWNvc2lzX2lkKQpoZWFkKGRhdF9yYXcpCm5hbWVzKGRhdF9yYXcpWzE6NDBdCmBgYAoKIyMjIFByZXBhcmUgbmV3IGRhdGEgZm9yIGVzdGltYXRpb24KYGBge3IsIGVjaG89VFJVRX0KU3RhcnQud2F2ZSA8LSA1MDAKRW5kLndhdmUgPC0gMjQwMAp3diA8LSBzZXEoU3RhcnQud2F2ZSxFbmQud2F2ZSwxKQpTcGVjdHJhIDwtIGFzLm1hdHJpeChkYXRfcmF3WyxuYW1lcyhkYXRfcmF3KSAlaW4lIHd2XSkKY29sbmFtZXMoU3BlY3RyYSkgPC0gYyhwYXN0ZTAoIldhdmVfIix3dikpCmhlYWQoU3BlY3RyYSlbMTo2LDE6MTBdCnNhbXBsZV9pbmZvIDwtIGRhdF9yYXdbLG5hbWVzKGRhdF9yYXcpICVub3RpbiUgc2VxKDM1MCwyNTAwLDEpXQpoZWFkKHNhbXBsZV9pbmZvKQoKc2FtcGxlX2luZm8yIDwtIHNhbXBsZV9pbmZvICU+JQogIHNlbGVjdChEb21haW4sRnVuY3Rpb25hbF90eXBlLFNhbXBsZV9JRCxVU0RBX1NwZWNpZXNfQ29kZT1gVVNEQSBTeW1ib2xgLExNQV9nRFdfbTI9TE1BKQpoZWFkKHNhbXBsZV9pbmZvMikKCnBsc3JfZGF0YSA8LSBkYXRhLmZyYW1lKHNhbXBsZV9pbmZvMixTcGVjdHJhKQpybShzYW1wbGVfaW5mbyxzYW1wbGVfaW5mbzIsU3BlY3RyYSkKYGBgCgojIyMjIEV4YW1wbGUgZGF0YSBjbGVhbmluZy4gCmBgYHtyLCBlY2hvPVRSVUV9CiMjIyMgRW5kIHVzZXIgbmVlZHMgdG8gZG8gd2hhdCdzIGFwcHJvcHJpYXRlIGZvciB0aGVpciBkYXRhLiAgVGhpcyBtYXkgYmUgYW4gaXRlcmF0aXZlIHByb2Nlc3MuCiMgS2VlcCBvbmx5IGNvbXBsZXRlIHJvd3Mgb2YgaW5WYXIgYW5kIHNwZWMgZGF0YSBiZWZvcmUgZml0dGluZwpwbHNyX2RhdGEgPC0gcGxzcl9kYXRhW2NvbXBsZXRlLmNhc2VzKHBsc3JfZGF0YVssbmFtZXMocGxzcl9kYXRhKSAlaW4lIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGMoaW5WYXIscGFzdGUwKCJXYXZlXyIsd3YpKV0pLF0KYGBgCgojIyMjIFByZXBhcmUgUExTUiBtb2RlbApgYGB7ciwgZWNobz1UUlVFfQpwcmludCgiKioqKiBBcHBseWluZyBQTFNSIG1vZGVsIHRvIGVzdGltYXRlIExNQSBmcm9tIHNwZWN0cmFsIG9ic2VydmF0aW9ucyAqKioqIikKIyBzZXR1cCBtb2RlbApkaW1zIDwtIGRpbShMZWFmTE1BLnBsc3IuY29lZmZzKQpMZWFmTE1BLnBsc3IuaW50ZXJjZXB0IDwtIExlYWZMTUEucGxzci5jb2VmZnNbMSxdCkxlYWZMTUEucGxzci5jb2VmZnMgPC0gZGF0YS5mcmFtZShMZWFmTE1BLnBsc3IuY29lZmZzWzI6ZGltc1sxXSxdKQpuYW1lcyhMZWFmTE1BLnBsc3IuY29lZmZzKSA8LSBjKCJ3YXZlbGVuZ3RoIiwiY29lZnMiKQpMZWFmTE1BLnBsc3IuY29lZmZzLnZlYyA8LSBhcy52ZWN0b3IoTGVhZkxNQS5wbHNyLmNvZWZmc1ssMl0pCnN1Yl9zcGVjIDwtIGRyb3BsZXZlbHMocGxzcl9kYXRhWyx3aGljaChuYW1lcyhwbHNyX2RhdGEpICVpbiUgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBhc3RlMCgiV2F2ZV8iLHNlcShTdGFydC53YXZlLEVuZC53YXZlLDEpKSldKQpgYGAKCiMjIyMgQXBwbHkgUExTUiBtb2RlbApgYGB7ciwgZWNobz1UUlVFfQpwbHNyX3ByZWQgPC0gYXMubWF0cml4KHN1Yl9zcGVjKSAlKiUgTGVhZkxNQS5wbHNyLmNvZWZmcy52ZWMgKyBMZWFmTE1BLnBsc3IuaW50ZXJjZXB0WywyXQpsZWFmTE1BIDwtIHBsc3JfcHJlZFssMV1eMiAgIyBjb252ZXJ0IHRvIHN0YW5kYXJkIExNQSB1bml0cyBmcm9tIHNxcnQoTE1BKQpuYW1lcyhsZWFmTE1BKSA8LSAiUExTUl9MTUFfZ0RXX20yIgoKIyBvcmdhbml6ZSBvdXRwdXQKTGVhZkxNQS5QTFNSLmRhdGFzZXQgPC0gZGF0YS5mcmFtZShwbHNyX2RhdGFbLHdoaWNoKG5hbWVzKHBsc3JfZGF0YSkgJW5vdGluJSAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcGFzdGUwKCJXYXZlXyIsc2VxKFN0YXJ0LndhdmUsRW5kLndhdmUsMSkpKV0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgUExTUl9MTUFfZ0RXX20yPWxlYWZMTUEsIFBMU1JfUmVzaWR1YWxzPWxlYWZMTUEtcGxzcl9kYXRhWyxpblZhcl0pCmhlYWQoTGVhZkxNQS5QTFNSLmRhdGFzZXQpCmBgYAoKIyMjIyBHZW5lcmF0ZSBQTFNSIHVuY2VydGFpbnR5IGVzdGltYXRlcwpgYGB7ciwgZWNobz1UUlVFfQpwcmludCgiKioqKiBHZW5lcmF0ZSBQTFNSIHVuY2VydGFpbnR5IGVzdGltYXRlcyAqKioqIikKamtfY29lZiA8LSBkYXRhLmZyYW1lKExlYWZMTUEucGxzci5qay5jb2VmZnNbLDM6ZGltKExlYWZMTUEucGxzci5qay5jb2VmZnMpWzJdXSkKamtfY29lZiA8LSB0KGprX2NvZWYpCmhlYWQoamtfY29lZilbLDE6Nl0KamtfaW50IDwtIHQoTGVhZkxNQS5wbHNyLmprLmNvZWZmc1ssMl0pCmhlYWQoamtfaW50KVssMTo2XQoKamtfcHJlZCA8LSBhcy5tYXRyaXgoc3ViX3NwZWMpICUqJSBqa19jb2VmICsgbWF0cml4KHJlcChqa19pbnQsIGxlbmd0aChwbHNyX2RhdGFbLGluVmFyXSkpLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJ5cm93PVRSVUUsIG5jb2w9bGVuZ3RoKGprX2ludCkpCmprX3ByZWQgPC0gamtfcHJlZF4yCmhlYWQoamtfcHJlZClbLDE6Nl0KZGltKGprX3ByZWQpCmludGVydmFsIDwtIGMoMC4wMjUsMC45NzUpCkludGVydmFsX0NvbmYgPC0gYXBwbHkoWCA9IGprX3ByZWQsIE1BUkdJTiA9IDEsIEZVTiA9IHF1YW50aWxlLCAKICAgICAgICAgICAgICAgICAgICAgICBwcm9icz1jKGludGVydmFsWzFdLCBpbnRlcnZhbFsyXSkpCnNkX21lYW4gPC0gYXBwbHkoWCA9IGprX3ByZWQsIE1BUkdJTiA9IDEsIEZVTiA9c2QpCnNkX3JlcyA8LSBzZChMZWFmTE1BLlBMU1IuZGF0YXNldCRQTFNSX1Jlc2lkdWFscykKc2RfdG90IDwtIHNxcnQoc2RfbWVhbl4yK3NkX3Jlc14yKQpMZWFmTE1BLlBMU1IuZGF0YXNldCRMQ0kgPC0gSW50ZXJ2YWxfQ29uZlsxLF0KTGVhZkxNQS5QTFNSLmRhdGFzZXQkVUNJIDwtIEludGVydmFsX0NvbmZbMixdCkxlYWZMTUEuUExTUi5kYXRhc2V0JExQSSA8LSBMZWFmTE1BLlBMU1IuZGF0YXNldCRQTFNSX0xNQV9nRFdfbTItMS45NipzZF90b3QKTGVhZkxNQS5QTFNSLmRhdGFzZXQkVVBJIDwtIExlYWZMTUEuUExTUi5kYXRhc2V0JFBMU1JfTE1BX2dEV19tMisxLjk2KnNkX3RvdApoZWFkKExlYWZMTUEuUExTUi5kYXRhc2V0KQpgYGAKCiMjIyMgR2VuZXJhdGUgUExTUiBlc3RpbWF0ZWQgTE1BIG9ic2VydmVkIHZzIHByZWRpY3RlZCBwbG90CmBgYHtyLCBmaWcuaGVpZ2h0ID0gNywgZmlnLndpZHRoID0gOCwgZWNobz1UUlVFfQpybXNlcF9wZXJjcm1zZXAgPC0gc3BlY3RyYXRyYWl0OjpwZXJjZW50X3Jtc2UocGxzcl9kYXRhc2V0ID0gTGVhZkxNQS5QTFNSLmRhdGFzZXQsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaW5WYXIgPSBpblZhciwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXNpZHVhbHMgPSBMZWFmTE1BLlBMU1IuZGF0YXNldCRQTFNSX1Jlc2lkdWFscywgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICByYW5nZT0iZnVsbCIpClJNU0VQIDwtIHJtc2VwX3BlcmNybXNlcCRybXNlCnBlcmNfUk1TRVAgPC0gcm1zZXBfcGVyY3Jtc2VwJHBlcmNfcm1zZQpyMiA8LSByb3VuZChzdW1tYXJ5KGxtKExlYWZMTUEuUExTUi5kYXRhc2V0JFBMU1JfTE1BX2dEV19tMn4KICAgICAgICAgICAgICAgICAgICAgICAgIExlYWZMTUEuUExTUi5kYXRhc2V0WyxpblZhcl0pKSRhZGouci5zcXVhcmVkLDIpCmV4cHIgPC0gdmVjdG9yKCJleHByZXNzaW9uIiwgMykKZXhwcltbMV1dIDwtIGJxdW90ZShSXjI9PS4ocjIpKQpleHByW1syXV0gPC0gYnF1b3RlKFJNU0VQPT0uKHJvdW5kKFJNU0VQLDIpKSkKZXhwcltbM11dIDwtIGJxdW90ZSgiJVJNU0VQIj09Lihyb3VuZChwZXJjX1JNU0VQLDIpKSkKcm5nX3ZhbHMgPC0gYyhtaW4oTGVhZkxNQS5QTFNSLmRhdGFzZXQkTFBJKSwgbWF4KExlYWZMTUEuUExTUi5kYXRhc2V0JFVQSSkpCnBhcihtZnJvdz1jKDEsMSksIG1hcj1jKDQuMiw1LjMsMSwwLjQpLCBvbWE9YygwLCAwLjEsIDAsIDAuMikpCnBsb3RyaXg6OnBsb3RDSShMZWFmTE1BLlBMU1IuZGF0YXNldCRQTFNSX0xNQV9nRFdfbTIsTGVhZkxNQS5QTFNSLmRhdGFzZXRbLGluVmFyXSwgCiAgICAgICAgICAgICAgICBsaT1MZWFmTE1BLlBMU1IuZGF0YXNldCRMUEksIHVpPUxlYWZMTUEuUExTUi5kYXRhc2V0JFVQSSwgZ2FwPTAuMDA5LHNmcmFjPTAuMDAwLCAKICAgICAgICAgICAgICAgIGx3ZD0xLjYsIHhsaW09YyhybmdfdmFsc1sxXSwgcm5nX3ZhbHNbMl0pLCB5bGltPWMocm5nX3ZhbHNbMV0sIHJuZ192YWxzWzJdKSwgCiAgICAgICAgICAgICAgICBlcnI9IngiLCBwY2g9MjEsIGNvbD0iYmxhY2siLCBwdC5iZz1zY2FsZXM6OmFscGhhKCJncmV5NzAiLDAuNyksIHNjb2w9ImdyZXk4MCIsCiAgICAgICAgICAgICAgICBjZXg9MiwgeGxhYj1wYXN0ZTAoIlByZWRpY3RlZCAiLCBwYXN0ZShpblZhciksICIgKHVuaXRzKSIpLAogICAgICAgICAgICAgICAgeWxhYj1wYXN0ZTAoIk9ic2VydmVkICIsIHBhc3RlKGluVmFyKSwgIiAodW5pdHMpIiksCiAgICAgICAgICAgICAgICBjZXguYXhpcz0xLjUsY2V4LmxhYj0xLjgpCmFibGluZSgwLDEsbHR5PTIsbHc9MikKcGxvdHJpeDo6cGxvdENJKExlYWZMTUEuUExTUi5kYXRhc2V0JFBMU1JfTE1BX2dEV19tMixMZWFmTE1BLlBMU1IuZGF0YXNldFssaW5WYXJdLCAKICAgICAgICAgICAgICAgIGxpPUxlYWZMTUEuUExTUi5kYXRhc2V0JExDSSwgdWk9TGVhZkxNQS5QTFNSLmRhdGFzZXQkVUNJLCBnYXA9MC4wMDksc2ZyYWM9MC4wMDQsIAogICAgICAgICAgICAgICAgbHdkPTEuNiwgeGxpbT1jKHJuZ192YWxzWzFdLCBybmdfdmFsc1syXSksIHlsaW09YyhybmdfdmFsc1sxXSwgcm5nX3ZhbHNbMl0pLCAKICAgICAgICAgICAgICAgIGVycj0ieCIsIHBjaD0yMSwgY29sPSJibGFjayIsIHB0LmJnPXNjYWxlczo6YWxwaGEoImdyZXk3MCIsMC43KSwgc2NvbD0iYmxhY2siLAogICAgICAgICAgICAgICAgY2V4PTIsIHhsYWI9cGFzdGUwKCJQcmVkaWN0ZWQgIiwgcGFzdGUoaW5WYXIpLCAiICh1bml0cykiKSwKICAgICAgICAgICAgICAgIHlsYWI9cGFzdGUwKCJPYnNlcnZlZCAiLCBwYXN0ZShpblZhciksICIgKHVuaXRzKSIpLAogICAgICAgICAgICAgICAgY2V4LmF4aXM9MS41LGNleC5sYWI9MS44LCBhZGQ9VCkKbGVnZW5kKCJ0b3BsZWZ0IiwgbGVnZW5kPWV4cHIsIGJ0eT0ibiIsIGNleD0xLjUpCmxlZ2VuZCgiYm90dG9tcmlnaHQiLCBsZWdlbmQ9YygiUHJlZGljdGlvbiBJbnRlcnZhbCIsIkNvbmZpZGVuY2UgSW50ZXJ2YWwiKSwgCiAgICAgICBsdHk9YygxLDEpLCBjb2wgPSBjKCJncmV5ODAiLCJibGFjayIpLCBsd2Q9MywgYnR5PSJuIiwgY2V4PTEuNSkKYm94KGx3ZD0yLjIpCmRldi5jb3B5KHBuZyxmaWxlLnBhdGgob3V0ZGlyLHBhc3RlMChpblZhciwiX1BMU1JfVmFsaWRhdGlvbl9TY2F0dGVycGxvdC5wbmciKSksIAogICAgICAgICBoZWlnaHQ9MjgwMCwgd2lkdGg9MzIwMCwgIHJlcz0zNDApCmRldi5vZmYoKTsKYGBgCmBgYHtyLCBlY2hvPVRSVUV9CnByaW50KHBhc3RlKCJPdXRwdXQgZGlyZWN0b3J5OiAiLCBvdXRkaXIpKQoKIyBPYnNlcnZlZCB2ZXJzdXMgcHJlZGljdGVkCndyaXRlLmNzdihMZWFmTE1BLlBMU1IuZGF0YXNldCxmaWxlPWZpbGUucGF0aChvdXRkaXIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcGFzdGUwKGluVmFyLCdfUExTUl9Fc3RpbWF0ZXMuY3N2JykpLAogICAgICAgICAgcm93Lm5hbWVzPUZBTFNFKQpgYGAKCiMjIyBDb25maXJtIGZpbGVzIHdlcmUgd3JpdHRlbiB0byB0ZW1wIHNwYWNlCmBgYHtyLCBlY2hvPVRSVUV9CnByaW50KCIqKioqIFBMU1Igb3V0cHV0IGZpbGVzOiAiKQpwcmludChsaXN0LmZpbGVzKG91dGRpcilbZ3JlcChwYXR0ZXJuID0gaW5WYXIsIGxpc3QuZmlsZXMob3V0ZGlyKSldKQpgYGAK