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)
LS0tCnRpdGxlOiBBbiBleGFtcGxlIHNob3dpbmcgaG93IHRvIGFwcGx5IGFuIGV4aXN0aW5nIFBMU1IgbW9kZWwgdG8gbmV3IGRhdGEuIEluIHRoaXMgY2FzZSBhcHBseWluZyB0aGUgTE1BIG1vZGVsIGZyb20gU2VyYmluIGV0IGFsLiwgKDIwMTk7IERPSSAtIDEwLjExMTEvbnBoLjE2MTIzKSB0byBhIGRhdGFzZXQgY29sbGVjdGVkIGF0IENPTlVTIE5FT04gZmllbGQgc2l0ZXMgCmF1dGhvcjogIlNoYXduIFAuIFNlcmJpbiwgSnVsaWVuIExhbW91ciwgJiBKZXJlbWlhaCBBbmRlcnNvbiIKZGF0ZTogImByIFN5cy5EYXRlKClgIgpvdXRwdXQ6CiAgaHRtbF9ub3RlYm9vazogZGVmYXVsdAotLS0KCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFLCBlY2hvPUZBTFNFfQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IFRSVUUpCmBgYAoKIyMjIEdldHRpbmcgU3RhcnRlZAojIyMgTG9hZCBsaWJyYXJpZXMKYGBge3IsIGV2YWw9VFJVRSwgZWNobz1UUlVFfQpsaXN0Lm9mLnBhY2thZ2VzIDwtIGMoInBscyIsImRwbHlyIiwicmVzaGFwZTIiLCJoZXJlIiwicGxvdHJpeCIsImdncGxvdDIiLCJncmlkRXh0cmEiLAogICAgICAgICAgICAgICAgICAgICAgInNwZWN0cmF0cmFpdCIpCmludmlzaWJsZShsYXBwbHkobGlzdC5vZi5wYWNrYWdlcywgbGlicmFyeSwgY2hhcmFjdGVyLm9ubHkgPSBUUlVFKSkKYGBgCgojIyMgU2V0dXAgb3RoZXIgZnVuY3Rpb25zIGFuZCBvcHRpb25zCmBgYHtyLCBlY2hvPVRSVUV9CiMjIyBTZXR1cCBvcHRpb25zCgojIFNjcmlwdCBvcHRpb25zCnBsczo6cGxzLm9wdGlvbnMocGxzcmFsZyA9ICJvc2NvcmVzcGxzIikKcGxzOjpwbHMub3B0aW9ucygicGxzcmFsZyIpCgojIERlZmF1bHQgcGFyIG9wdGlvbnMKb3BhciA8LSBwYXIobm8ucmVhZG9ubHkgPSBUKQoKIyBXaGF0IGlzIHRoZSB0YXJnZXQgdmFyaWFibGU/CmluVmFyIDwtICJMTUFfZ0RXX20yIgoKIyBXaGF0IGlzIHRoZSBzb3VyY2UgZGF0YXNldCBmcm9tIEVjb1NJUz8KZWNvc2lzX2lkIDwtICI1NjE3ZGExNy1jOTI1LTQ5ZmItYjM5NS00NWE1MTI5MWJkMmQiCgojIFNwZWNpZnkgb3V0cHV0IGRpcmVjdG9yeSwgb3V0cHV0X2RpciAKIyBPcHRpb25zOiAKIyB0ZW1wZGlyIC0gdXNlIGEgT1Mtc3BlY2lmaWVkIHRlbXBvcmFyeSBkaXJlY3RvcnkgCiMgdXNlciBkZWZpbmVkIFBBVEggLSBlLmcuICJ+L3NjcmF0Y2gvUExTUiIKb3V0cHV0X2RpciA8LSAidGVtcGRpciIKYGBgCgojIyMgU2V0IHdvcmtpbmcgZGlyZWN0b3J5IChzY3JhdGNoIHNwYWNlKQpgYGB7ciwgZWNobz1GQUxTRX0KaWYgKG91dHB1dF9kaXI9PSJ0ZW1wZGlyIikgewogIG91dGRpciA8LSB0ZW1wZGlyKCkKfSBlbHNlIHsKICBpZiAoISBmaWxlLmV4aXN0cyhvdXRwdXRfZGlyKSkgZGlyLmNyZWF0ZShvdXRwdXRfZGlyLHJlY3Vyc2l2ZT1UUlVFKQogIG91dGRpciA8LSBmaWxlLnBhdGgocGF0aC5leHBhbmQob3V0cHV0X2RpcikpCn0Kc2V0d2Qob3V0ZGlyKSAjIHNldCB3b3JraW5nIGRpcmVjdG9yeQpnZXR3ZCgpICAjIGNoZWNrIHdkCmBgYAoKIyMjIEdyYWIgUExTUiBDb2VmZmljaWVudHMgZnJvbSBHaXRIdWIKYGBge3IsIGVjaG89VFJVRX0KZ2l0X3JlcG8gPC0gImh0dHBzOi8vcmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbS9zZXJiaW5zaC9TU2VyYmluX2V0YWxfMjAxOV9OZXdQaHl0b2xvZ2lzdC9tYXN0ZXIvIgpwcmludCgiKioqKiBEb3dubG9hZGluZyBQTFNSIGNvZWZmaWNpZW50cyAqKioqIikKZ2l0aHViVVJMIDwtIHBhc3RlMChnaXRfcmVwbywiU1NlcmJpbl9tdWx0aWJpb21lX2xtYV9wbHNyX21vZGVsL3NxcnRfTE1BX2dEV19tMl9QTFNSX0NvZWZmaWNpZW50c18xMGNvbXAuY3N2IikKTGVhZkxNQS5wbHNyLmNvZWZmcyA8LSBzcGVjdHJhdHJhaXQ6OnNvdXJjZV9HaXRIdWJEYXRhKGdpdGh1YlVSTCkKcm0oZ2l0aHViVVJMKQpnaXRodWJVUkwgPC0gcGFzdGUwKGdpdF9yZXBvLCJTU2VyYmluX211bHRpYmlvbWVfbG1hX3Bsc3JfbW9kZWwvc3FydF9MTUFfZ0RXX20yX0phY2traWZlX1BMU1JfQ29lZmZpY2llbnRzLmNzdiIpCkxlYWZMTUEucGxzci5qay5jb2VmZnMgPC0gc3BlY3RyYXRyYWl0Ojpzb3VyY2VfR2l0SHViRGF0YShnaXRodWJVUkwpCnJtKGdpdGh1YlVSTCkKYGBgCgojIyMgR2V0IHNvdXJjZSBkYXRhc2V0IGZyb20gRWNvU0lTCmBgYHtyLCBlY2hvPVRSVUV9CmRhdF9yYXcgPC0gc3BlY3RyYXRyYWl0OjpnZXRfZWNvc2lzX2RhdGEoZWNvc2lzX2lkID0gZWNvc2lzX2lkKQpoZWFkKGRhdF9yYXcpCm5hbWVzKGRhdF9yYXcpWzE6NDBdCmBgYAoKIyMjIFByZXBhcmUgbmV3IGRhdGEgZm9yIGVzdGltYXRpb24KYGBge3IsIGVjaG89VFJVRX0KU3RhcnQud2F2ZSA8LSA1MDAKRW5kLndhdmUgPC0gMjQwMAp3diA8LSBzZXEoU3RhcnQud2F2ZSxFbmQud2F2ZSwxKQpTcGVjdHJhIDwtIGFzLm1hdHJpeChkYXRfcmF3WyxuYW1lcyhkYXRfcmF3KSAlaW4lIHd2XSkKY29sbmFtZXMoU3BlY3RyYSkgPC0gYyhwYXN0ZTAoIldhdmVfIix3dikpCmhlYWQoU3BlY3RyYSlbMTo2LDE6MTBdCnNhbXBsZV9pbmZvIDwtIGRhdF9yYXdbLG5hbWVzKGRhdF9yYXcpICVub3RpbiUgc2VxKDM1MCwyNTAwLDEpXQpoZWFkKHNhbXBsZV9pbmZvKQoKc2FtcGxlX2luZm8yIDwtIHNhbXBsZV9pbmZvICU+JQogIHNlbGVjdChEb21haW4sRnVuY3Rpb25hbF90eXBlLFNhbXBsZV9JRCxVU0RBX1NwZWNpZXNfQ29kZT1gVVNEQSBTeW1ib2xgLExNQV9nRFdfbTI9TE1BKQpoZWFkKHNhbXBsZV9pbmZvMikKCnBsc3JfZGF0YSA8LSBkYXRhLmZyYW1lKHNhbXBsZV9pbmZvMixTcGVjdHJhKQpybShzYW1wbGVfaW5mbyxzYW1wbGVfaW5mbzIsU3BlY3RyYSkKYGBgCgojIyMjIEV4YW1wbGUgZGF0YSBjbGVhbmluZy4gCmBgYHtyLCBlY2hvPVRSVUV9CiMjIyMgRW5kIHVzZXIgbmVlZHMgdG8gZG8gd2hhdCdzIGFwcHJvcHJpYXRlIGZvciB0aGVpciBkYXRhLiAgVGhpcyBtYXkgYmUgYW4gaXRlcmF0aXZlIHByb2Nlc3MuCiMgS2VlcCBvbmx5IGNvbXBsZXRlIHJvd3Mgb2YgaW5WYXIgYW5kIHNwZWMgZGF0YSBiZWZvcmUgZml0dGluZwpwbHNyX2RhdGEgPC0gcGxzcl9kYXRhW2NvbXBsZXRlLmNhc2VzKHBsc3JfZGF0YVssbmFtZXMocGxzcl9kYXRhKSAlaW4lIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGMoaW5WYXIscGFzdGUwKCJXYXZlXyIsd3YpKV0pLF0KYGBgCgojIyMjIFByZXBhcmUgUExTUiBtb2RlbApgYGB7ciwgZWNobz1UUlVFfQpwcmludCgiKioqKiBBcHBseWluZyBQTFNSIG1vZGVsIHRvIGVzdGltYXRlIExNQSBmcm9tIHNwZWN0cmFsIG9ic2VydmF0aW9ucyAqKioqIikKIyBzZXR1cCBtb2RlbApkaW1zIDwtIGRpbShMZWFmTE1BLnBsc3IuY29lZmZzKQpMZWFmTE1BLnBsc3IuaW50ZXJjZXB0IDwtIExlYWZMTUEucGxzci5jb2VmZnNbMSxdCkxlYWZMTUEucGxzci5jb2VmZnMgPC0gZGF0YS5mcmFtZShMZWFmTE1BLnBsc3IuY29lZmZzWzI6ZGltc1sxXSxdKQpuYW1lcyhMZWFmTE1BLnBsc3IuY29lZmZzKSA8LSBjKCJ3YXZlbGVuZ3RoIiwiY29lZnMiKQpMZWFmTE1BLnBsc3IuY29lZmZzLnZlYyA8LSBhcy52ZWN0b3IoTGVhZkxNQS5wbHNyLmNvZWZmc1ssMl0pCnN1Yl9zcGVjIDwtIGRyb3BsZXZlbHMocGxzcl9kYXRhWyx3aGljaChuYW1lcyhwbHNyX2RhdGEpICVpbiUgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBhc3RlMCgiV2F2ZV8iLHNlcShTdGFydC53YXZlLEVuZC53YXZlLDEpKSldKQpgYGAKCiMjIyMgQXBwbHkgUExTUiBtb2RlbApgYGB7ciwgZWNobz1UUlVFfQpwbHNyX3ByZWQgPC0gYXMubWF0cml4KHN1Yl9zcGVjKSAlKiUgTGVhZkxNQS5wbHNyLmNvZWZmcy52ZWMgKyBMZWFmTE1BLnBsc3IuaW50ZXJjZXB0WywyXQpsZWFmTE1BIDwtIHBsc3JfcHJlZFssMV1eMiAgIyBjb252ZXJ0IHRvIHN0YW5kYXJkIExNQSB1bml0cyBmcm9tIHNxcnQoTE1BKQpuYW1lcyhsZWFmTE1BKSA8LSAiUExTUl9MTUFfZ0RXX20yIgoKIyBvcmdhbml6ZSBvdXRwdXQKTGVhZkxNQS5QTFNSLmRhdGFzZXQgPC0gZGF0YS5mcmFtZShwbHNyX2RhdGFbLHdoaWNoKG5hbWVzKHBsc3JfZGF0YSkgJW5vdGluJSAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcGFzdGUwKCJXYXZlXyIsc2VxKFN0YXJ0LndhdmUsRW5kLndhdmUsMSkpKV0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgUExTUl9MTUFfZ0RXX20yPWxlYWZMTUEsIFBMU1JfUmVzaWR1YWxzPWxlYWZMTUEtcGxzcl9kYXRhWyxpblZhcl0pCmhlYWQoTGVhZkxNQS5QTFNSLmRhdGFzZXQpCmBgYAoKIyMjIyBHZW5lcmF0ZSBQTFNSIHVuY2VydGFpbnR5IGVzdGltYXRlcwpgYGB7ciwgZWNobz1UUlVFfQpwcmludCgiKioqKiBHZW5lcmF0ZSBQTFNSIHVuY2VydGFpbnR5IGVzdGltYXRlcyAqKioqIikKamtfY29lZiA8LSBkYXRhLmZyYW1lKExlYWZMTUEucGxzci5qay5jb2VmZnNbLDM6ZGltKExlYWZMTUEucGxzci5qay5jb2VmZnMpWzJdXSkKamtfY29lZiA8LSB0KGprX2NvZWYpCmhlYWQoamtfY29lZilbLDE6Nl0KamtfaW50IDwtIHQoTGVhZkxNQS5wbHNyLmprLmNvZWZmc1ssMl0pCmhlYWQoamtfaW50KVssMTo2XQoKamtfcHJlZCA8LSBhcy5tYXRyaXgoc3ViX3NwZWMpICUqJSBqa19jb2VmICsgbWF0cml4KHJlcChqa19pbnQsIGxlbmd0aChwbHNyX2RhdGFbLGluVmFyXSkpLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJ5cm93PVRSVUUsIG5jb2w9bGVuZ3RoKGprX2ludCkpCmprX3ByZWQgPC0gamtfcHJlZF4yCmhlYWQoamtfcHJlZClbLDE6Nl0KZGltKGprX3ByZWQpCmludGVydmFsIDwtIGMoMC4wMjUsMC45NzUpCkludGVydmFsX0NvbmYgPC0gYXBwbHkoWCA9IGprX3ByZWQsIE1BUkdJTiA9IDEsIEZVTiA9IHF1YW50aWxlLCAKICAgICAgICAgICAgICAgICAgICAgICBwcm9icz1jKGludGVydmFsWzFdLCBpbnRlcnZhbFsyXSkpCnNkX21lYW4gPC0gYXBwbHkoWCA9IGprX3ByZWQsIE1BUkdJTiA9IDEsIEZVTiA9c2QpCnNkX3JlcyA8LSBzZChMZWFmTE1BLlBMU1IuZGF0YXNldCRQTFNSX1Jlc2lkdWFscykKc2RfdG90IDwtIHNxcnQoc2RfbWVhbl4yK3NkX3Jlc14yKQpMZWFmTE1BLlBMU1IuZGF0YXNldCRMQ0kgPC0gSW50ZXJ2YWxfQ29uZlsxLF0KTGVhZkxNQS5QTFNSLmRhdGFzZXQkVUNJIDwtIEludGVydmFsX0NvbmZbMixdCkxlYWZMTUEuUExTUi5kYXRhc2V0JExQSSA8LSBMZWFmTE1BLlBMU1IuZGF0YXNldCRQTFNSX0xNQV9nRFdfbTItMS45NipzZF90b3QKTGVhZkxNQS5QTFNSLmRhdGFzZXQkVVBJIDwtIExlYWZMTUEuUExTUi5kYXRhc2V0JFBMU1JfTE1BX2dEV19tMisxLjk2KnNkX3RvdApoZWFkKExlYWZMTUEuUExTUi5kYXRhc2V0KQpgYGAKCiMjIyMgR2VuZXJhdGUgUExTUiBlc3RpbWF0ZWQgTE1BIG9ic2VydmVkIHZzIHByZWRpY3RlZCBwbG90CmBgYHtyLCBmaWcuaGVpZ2h0ID0gNywgZmlnLndpZHRoID0gOCwgZWNobz1UUlVFfQpybXNlcF9wZXJjcm1zZXAgPC0gc3BlY3RyYXRyYWl0OjpwZXJjZW50X3Jtc2UocGxzcl9kYXRhc2V0ID0gTGVhZkxNQS5QTFNSLmRhdGFzZXQsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaW5WYXIgPSBpblZhciwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXNpZHVhbHMgPSBMZWFmTE1BLlBMU1IuZGF0YXNldCRQTFNSX1Jlc2lkdWFscywgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICByYW5nZT0iZnVsbCIpClJNU0VQIDwtIHJtc2VwX3BlcmNybXNlcCRybXNlCnBlcmNfUk1TRVAgPC0gcm1zZXBfcGVyY3Jtc2VwJHBlcmNfcm1zZQpyMiA8LSByb3VuZChzdW1tYXJ5KGxtKExlYWZMTUEuUExTUi5kYXRhc2V0JFBMU1JfTE1BX2dEV19tMn4KICAgICAgICAgICAgICAgICAgICAgICAgIExlYWZMTUEuUExTUi5kYXRhc2V0WyxpblZhcl0pKSRhZGouci5zcXVhcmVkLDIpCmV4cHIgPC0gdmVjdG9yKCJleHByZXNzaW9uIiwgMykKZXhwcltbMV1dIDwtIGJxdW90ZShSXjI9PS4ocjIpKQpleHByW1syXV0gPC0gYnF1b3RlKFJNU0VQPT0uKHJvdW5kKFJNU0VQLDIpKSkKZXhwcltbM11dIDwtIGJxdW90ZSgiJVJNU0VQIj09Lihyb3VuZChwZXJjX1JNU0VQLDIpKSkKcm5nX3ZhbHMgPC0gYyhtaW4oTGVhZkxNQS5QTFNSLmRhdGFzZXQkTFBJKSwgbWF4KExlYWZMTUEuUExTUi5kYXRhc2V0JFVQSSkpCnBhcihtZnJvdz1jKDEsMSksIG1hcj1jKDQuMiw1LjMsMSwwLjQpLCBvbWE9YygwLCAwLjEsIDAsIDAuMikpCnBsb3RyaXg6OnBsb3RDSShMZWFmTE1BLlBMU1IuZGF0YXNldCRQTFNSX0xNQV9nRFdfbTIsTGVhZkxNQS5QTFNSLmRhdGFzZXRbLGluVmFyXSwgCiAgICAgICAgICAgICAgICBsaT1MZWFmTE1BLlBMU1IuZGF0YXNldCRMUEksIHVpPUxlYWZMTUEuUExTUi5kYXRhc2V0JFVQSSwgZ2FwPTAuMDA5LHNmcmFjPTAuMDAwLCAKICAgICAgICAgICAgICAgIGx3ZD0xLjYsIHhsaW09YyhybmdfdmFsc1sxXSwgcm5nX3ZhbHNbMl0pLCB5bGltPWMocm5nX3ZhbHNbMV0sIHJuZ192YWxzWzJdKSwgCiAgICAgICAgICAgICAgICBlcnI9IngiLCBwY2g9MjEsIGNvbD0iYmxhY2siLCBwdC5iZz1zY2FsZXM6OmFscGhhKCJncmV5NzAiLDAuNyksIHNjb2w9ImdyZXk4MCIsCiAgICAgICAgICAgICAgICBjZXg9MiwgeGxhYj1wYXN0ZTAoIlByZWRpY3RlZCAiLCBwYXN0ZShpblZhciksICIgKHVuaXRzKSIpLAogICAgICAgICAgICAgICAgeWxhYj1wYXN0ZTAoIk9ic2VydmVkICIsIHBhc3RlKGluVmFyKSwgIiAodW5pdHMpIiksCiAgICAgICAgICAgICAgICBjZXguYXhpcz0xLjUsY2V4LmxhYj0xLjgpCmFibGluZSgwLDEsbHR5PTIsbHc9MikKcGxvdHJpeDo6cGxvdENJKExlYWZMTUEuUExTUi5kYXRhc2V0JFBMU1JfTE1BX2dEV19tMixMZWFmTE1BLlBMU1IuZGF0YXNldFssaW5WYXJdLCAKICAgICAgICAgICAgICAgIGxpPUxlYWZMTUEuUExTUi5kYXRhc2V0JExDSSwgdWk9TGVhZkxNQS5QTFNSLmRhdGFzZXQkVUNJLCBnYXA9MC4wMDksc2ZyYWM9MC4wMDQsIAogICAgICAgICAgICAgICAgbHdkPTEuNiwgeGxpbT1jKHJuZ192YWxzWzFdLCBybmdfdmFsc1syXSksIHlsaW09YyhybmdfdmFsc1sxXSwgcm5nX3ZhbHNbMl0pLCAKICAgICAgICAgICAgICAgIGVycj0ieCIsIHBjaD0yMSwgY29sPSJibGFjayIsIHB0LmJnPXNjYWxlczo6YWxwaGEoImdyZXk3MCIsMC43KSwgc2NvbD0iYmxhY2siLAogICAgICAgICAgICAgICAgY2V4PTIsIHhsYWI9cGFzdGUwKCJQcmVkaWN0ZWQgIiwgcGFzdGUoaW5WYXIpLCAiICh1bml0cykiKSwKICAgICAgICAgICAgICAgIHlsYWI9cGFzdGUwKCJPYnNlcnZlZCAiLCBwYXN0ZShpblZhciksICIgKHVuaXRzKSIpLAogICAgICAgICAgICAgICAgY2V4LmF4aXM9MS41LGNleC5sYWI9MS44LCBhZGQ9VCkKbGVnZW5kKCJ0b3BsZWZ0IiwgbGVnZW5kPWV4cHIsIGJ0eT0ibiIsIGNleD0xLjUpCmxlZ2VuZCgiYm90dG9tcmlnaHQiLCBsZWdlbmQ9YygiUHJlZGljdGlvbiBJbnRlcnZhbCIsIkNvbmZpZGVuY2UgSW50ZXJ2YWwiKSwgCiAgICAgICBsdHk9YygxLDEpLCBjb2wgPSBjKCJncmV5ODAiLCJibGFjayIpLCBsd2Q9MywgYnR5PSJuIiwgY2V4PTEuNSkKYm94KGx3ZD0yLjIpCmRldi5jb3B5KHBuZyxmaWxlLnBhdGgob3V0ZGlyLHBhc3RlMChpblZhciwiX1BMU1JfVmFsaWRhdGlvbl9TY2F0dGVycGxvdC5wbmciKSksIAogICAgICAgICBoZWlnaHQ9MjgwMCwgd2lkdGg9MzIwMCwgIHJlcz0zNDApCmRldi5vZmYoKTsKYGBgCmBgYHtyLCBlY2hvPVRSVUV9CnByaW50KHBhc3RlKCJPdXRwdXQgZGlyZWN0b3J5OiAiLCBvdXRkaXIpKQoKIyBPYnNlcnZlZCB2ZXJzdXMgcHJlZGljdGVkCndyaXRlLmNzdihMZWFmTE1BLlBMU1IuZGF0YXNldCxmaWxlPWZpbGUucGF0aChvdXRkaXIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcGFzdGUwKGluVmFyLCdfUExTUl9Fc3RpbWF0ZXMuY3N2JykpLAogICAgICAgICAgcm93Lm5hbWVzPUZBTFNFKQpgYGAKCiMjIyBDb25maXJtIGZpbGVzIHdlcmUgd3JpdHRlbiB0byB0ZW1wIHNwYWNlCmBgYHtyLCBlY2hvPVRSVUV9CnByaW50KCIqKioqIFBMU1Igb3V0cHV0IGZpbGVzOiAiKQpwcmludChsaXN0LmZpbGVzKG91dGRpcilbZ3JlcChwYXR0ZXJuID0gaW5WYXIsIGxpc3QuZmlsZXMob3V0ZGlyKSldKQpgYGAK