Overview

This is an R Markdown Notebook to illustrate how to retrieve a dataset from the EcoSIS spectral database, choose the “optimal” number of plsr components, and fit a plsr model for specific leaf area (SLA). In this example, the plants were cultivated in an outdoor setting in the botanical garden of the KIT using 40x40 cm pots with an standardized substrate. The data was measured on a weekly basis (the timestamp is included in the dataset).

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 <- "SLA_g_cm"

# What is the source dataset from EcoSIS?
ecosis_id <- "3cf6b27e-d80e-4bc7-b214-c95506e46daa"

# 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/RtmpF2OY3c 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] "Output directory: /private/var/folders/xp/h3k9vf3n2jx181ts786_yjrn9c2gjq/T/RtmpF2OY3c"

Grab data from EcoSIS

print(paste0("Output directory: ",getwd()))  # check wd
[1] "Output directory: /Users/sserbin/Data/GitHub/spectratrait/vignettes"
### Get source dataset from EcoSIS
dat_raw <- spectratrait::get_ecosis_data(ecosis_id = ecosis_id)
[1] "**** Downloading Ecosis data ****"
Downloading data...
Rows: 739 Columns: 2114── Column specification ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Delimiter: ","
chr    (3): growth form, species, timestamp
dbl (2111): Anthocyanin concentration (mg/g), Anthocyanin content ( g/cm ), Carotenoid concentration (mg/g), Carotenoid content ( g/cm ), Chlorophyl...
ℹ 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] "Anthocyanin concentration (mg/g)" "Anthocyanin content ( g/cm )"     "Carotenoid concentration (mg/g)"  "Carotenoid content ( g/cm )"     
 [5] "Chlorophyll concentration (mg/g)" "Chlorophyll content ( g/cm )"     "LDMC (g/g)"                       "LFA (mg/cm )"                    
 [9] "LWC (mg/cm )"                     "SLA (g/cm )"                      "growth form"                      "species"                         
[13] "timestamp"                        "400"                              "401"                              "402"                             
[17] "403"                              "404"                              "405"                              "406"                             
[21] "407"                              "408"                              "409"                              "410"                             
[25] "411"                              "412"                              "413"                              "414"                             
[29] "415"                              "416"                              "417"                              "418"                             
[33] "419"                              "420"                              "421"                              "422"                             
[37] "423"                              "424"                              "425"                              "426"                             

Create full plsr dataset

### Create plsr dataset
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))
sample_info <- dat_raw[,names(dat_raw) %notin% seq(350,2500,1)]
head(sample_info)

sample_info2 <- sample_info %>%
  select(Plant_Species=species,Growth_Form=`growth form`,timestamp,
         SLA_g_cm=`SLA (g/cm )`) %>%
  mutate(SLA_g_cm=as.numeric(SLA_g_cm)) # ensure SLA is numeric
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,wv)]),]
# Remove suspect high values
plsr_data <- plsr_data[ plsr_data[,inVar] <= 500, ]

Create cal/val datasets

### Create cal/val datasets
## Make a stratified random sampling in the strata USDA_Species_Code and Domain

method <- "base" #base/dplyr
# base R - a bit slow
# dplyr - much faster
split_data <- spectratrait::create_data_split(dataset=plsr_data, approach=method, split_seed=2356812, 
                                              prop=0.8, group_variables="Plant_Species")
Calamagrostis epigejos   Cal: 80%
Anthoxanthum odoratum   Cal: 80%
Alopecurus pratensis   Cal: 80%
Festuca ovina   Cal: 78.947%
Agrostis capillaris   Cal: 82.353%
Aegopodium podagraria   Cal: 80%
Arrhenatherum elatius   Cal: 82.353%
Arctium lappa   Cal: 83.333%
Urtica dioica   Cal: 78.947%
Cirsium arvense   Cal: 80%
Geranium pratense   Cal: 81.25%
Geum urbanum   Cal: 80%
Digitalis purpurea   Cal: 81.25%
Stellaria media   Cal: 77.778%
Trisetum flavescens   Cal: 80%
Trifolium pratense   Cal: 80.952%
Geranium robertianum   Cal: 78.571%
Plantago major   Cal: 85.714%
Nardus stricta   Cal: 78.947%
Lamium purpureum   Cal: 77.778%
Clinopodium vulgare   Cal: 78.571%
Poa annua   Cal: 75%
Campanula rotundifolia   Cal: 78.571%
Taraxacum spec.   Cal: 80%
Digitaria sanguinalis   Cal: 85.714%
Holcus lanatus   Cal: 82.353%
Lapsana communis   Cal: 75%
Apera spica-venti   Cal: 80%
Alopecurus geniculatus   Cal: 75%
Bromus hordeaceus   Cal: 80%
Phalaris arundinaceae   Cal: 81.25%
Thlaspi arvense Not enough observations
Origanum vulgare   Cal: 77.778%
Pulicaria dysenterica   Cal: 79.167%
Deschampsia cespitosa   Cal: 80%
Cirsium acaule   Cal: 80%
Brachypodium sylvaticum   Cal: 80%
Centaurium erythraea   Cal: 77.778%
Luzula multiflora   Cal: 78.571%
Filipendula ulmaria   Cal: 78.571%
Anthyllis vulneraria   Cal: 75%
Medicago lupulina   Cal: 75%
Succisa pratensis   Cal: 83.333%
Scirpus sylvaticus   Cal: 77.778%
Molinia caerulea   Cal: 83.333%
names(split_data)
[1] "cal_data" "val_data"
cal.plsr.data <- split_data$cal_data
val.plsr.data <- split_data$val_data
rm(split_data)

# Datasets:
print(paste("Cal observations: ",dim(cal.plsr.data)[1],sep=""))
[1] "Cal observations: 490"
print(paste("Val observations: ",dim(val.plsr.data)[1],sep=""))
[1] "Val observations: 124"
cal_hist_plot <- qplot(cal.plsr.data[,paste0(inVar)],geom="histogram",
                       main = paste0("Cal. Histogram for ",inVar),
                       xlab = paste0(inVar),ylab = "Count",fill=I("grey50"),
                       col=I("black"),alpha=I(.7))
val_hist_plot <- qplot(val.plsr.data[,paste0(inVar)],geom="histogram",
                       main = paste0("Val. Histogram for ",inVar),
                       xlab = paste0(inVar),ylab = "Count",fill=I("grey50"),
                       col=I("black"),alpha=I(.7))
histograms <- grid.arrange(cal_hist_plot, val_hist_plot, ncol=2)

ggsave(filename = file.path(outdir,paste0(inVar,"_Cal_Val_Histograms.png")), 
       plot = histograms, device="png", width = 30, height = 12, units = "cm",
       dpi = 300)
# output cal/val data
write.csv(cal.plsr.data,file=file.path(outdir,paste0(inVar,'_Cal_PLSR_Dataset.csv')),
          row.names=FALSE)
write.csv(val.plsr.data,file=file.path(outdir,paste0(inVar,'_Val_PLSR_Dataset.csv')),
          row.names=FALSE)

Create calibration and validation PLSR datasets

### Format PLSR data for model fitting 
cal_spec <- as.matrix(cal.plsr.data[, which(names(cal.plsr.data) %in% paste0("Wave_",wv))])
cal.plsr.data <- data.frame(cal.plsr.data[, which(names(cal.plsr.data) %notin% paste0("Wave_",wv))],
                            Spectra=I(cal_spec))
head(cal.plsr.data)[1:5]

val_spec <- as.matrix(val.plsr.data[, which(names(val.plsr.data) %in% paste0("Wave_",wv))])
val.plsr.data <- data.frame(val.plsr.data[, which(names(val.plsr.data) %notin% paste0("Wave_",wv))],
                            Spectra=I(val_spec))
head(val.plsr.data)[1:5]

plot cal and val spectra

par(mfrow=c(1,2)) # B, L, T, R
spectratrait::f.plot.spec(Z=cal.plsr.data$Spectra,wv=wv,plot_label="Calibration")
spectratrait::f.plot.spec(Z=val.plsr.data$Spectra,wv=wv,plot_label="Validation")

dev.copy(png,file.path(outdir,paste0(inVar,'_Cal_Val_Spectra.png')), 
         height=2500,width=4900, res=340)
quartz_off_screen 
                3 
dev.off();
quartz_off_screen 
                2 
par(mfrow=c(1,1))

Use Jackknife permutation to determine optimal number of components

### Use permutation to determine the optimal number of components
if(grepl("Windows", sessionInfo()$running)){
  pls.options(parallel = NULL)
} else {
  pls.options(parallel = parallel::detectCores()-1)
}

method <- "pls" #pls, firstPlateau, firstMin
random_seed <- 2356812
seg <- 100
maxComps <- 18
iterations <- 50
prop <- 0.70
if (method=="pls") {
  # pls package approach - faster but estimates more components....
  nComps <- spectratrait::find_optimal_components(dataset=cal.plsr.data, targetVariable=inVar, 
                                                  method=method, 
                                                  maxComps=maxComps, seg=seg, 
                                                  random_seed=random_seed)
  print(paste0("*** Optimal number of components: ", nComps))
} else {
  nComps <- spectratrait::find_optimal_components(dataset=cal.plsr.data, targetVariable=inVar, 
                                                  method=method, 
                                                  maxComps=maxComps, 
                                                  iterations=iterations, 
                                                  seg=seg, prop=prop, 
                                                  random_seed=random_seed)
}
[1] "*** Identifying optimal number of PLSR components ***"
[1] "*** Running PLS permutation test ***"
[1] "*** Optimal number of components: 10"
dev.copy(png,file.path(outdir,paste0(paste0(inVar,"_PLSR_Component_Selection.png"))), 
         height=2800, width=3400,  res=340)
quartz_off_screen 
                3 
dev.off();
quartz_off_screen 
                2 

Fit final model

segs <- 100
plsr.out <- plsr(as.formula(paste(inVar,"~","Spectra")),scale=FALSE,ncomp=nComps,validation="CV",
                 segments=segs, segment.type="interleaved",trace=FALSE,data=cal.plsr.data)
fit <- plsr.out$fitted.values[,1,nComps]
pls.options(parallel = NULL)

# External validation fit stats
par(mfrow=c(1,2)) # B, L, T, R
pls::RMSEP(plsr.out, newdata = val.plsr.data)
(Intercept)      1 comps      2 comps      3 comps      4 comps      5 comps      6 comps      7 comps      8 comps      9 comps     10 comps  
      86.06        82.60        81.55        78.54        74.40        69.32        66.16        63.13        61.74        61.53        60.73  
plot(pls::RMSEP(plsr.out,estimate=c("test"),newdata = val.plsr.data), main="MODEL RMSEP",
     xlab="Number of Components",ylab="Model Validation RMSEP",lty=1,col="black",cex=1.5,lwd=2)
box(lwd=2.2)

pls::R2(plsr.out, newdata = val.plsr.data)
(Intercept)      1 comps      2 comps      3 comps      4 comps      5 comps      6 comps      7 comps      8 comps      9 comps     10 comps  
   -0.01288      0.06681      0.09056      0.15636      0.24295      0.34288      0.40138      0.45499      0.47875      0.48216      0.49563  
plot(R2(plsr.out,estimate=c("test"),newdata = val.plsr.data), main="MODEL R2",
     xlab="Number of Components",ylab="Model Validation R2",lty=1,col="black",cex=1.5,lwd=2)
box(lwd=2.2)
dev.copy(png,file.path(outdir,paste0(paste0(inVar,"_Validation_RMSEP_R2_by_Component.png"))), 
         height=2800, width=4800,  res=340)
quartz_off_screen 
                3 
dev.off();
quartz_off_screen 
                2 
par(opar)

PLSR fit observed vs. predicted plot data

#calibration
cal.plsr.output <- data.frame(cal.plsr.data[, which(names(cal.plsr.data) %notin% "Spectra")],
                              PLSR_Predicted=fit,
                              PLSR_CV_Predicted=as.vector(plsr.out$validation$pred[,,nComps]))
cal.plsr.output <- cal.plsr.output %>%
  mutate(PLSR_CV_Residuals = PLSR_CV_Predicted-get(inVar))
head(cal.plsr.output)
cal.R2 <- round(pls::R2(plsr.out,intercept=F)[[1]][nComps],2)
cal.RMSEP <- round(sqrt(mean(cal.plsr.output$PLSR_CV_Residuals^2)),2)

val.plsr.output <- data.frame(val.plsr.data[, which(names(val.plsr.data) %notin% "Spectra")],
                              PLSR_Predicted=as.vector(predict(plsr.out, 
                                                               newdata = val.plsr.data, 
                                                               ncomp=nComps, type="response")[,,1]))
val.plsr.output <- val.plsr.output %>%
  mutate(PLSR_Residuals = PLSR_Predicted-get(inVar))
head(val.plsr.output)
val.R2 <- round(pls::R2(plsr.out,newdata=val.plsr.data,intercept=F)[[1]][nComps],2)
val.RMSEP <- round(sqrt(mean(val.plsr.output$PLSR_Residuals^2)),2)

rng_quant <- quantile(cal.plsr.output[,inVar], probs = c(0.001, 0.999))
cal_scatter_plot <- ggplot(cal.plsr.output, aes(x=PLSR_CV_Predicted, y=get(inVar))) + 
  theme_bw() + geom_point() + geom_abline(intercept = 0, slope = 1, color="dark grey", 
                                          linetype="dashed", size=1.5) + xlim(rng_quant[1], 
                                                                              rng_quant[2]) + 
  ylim(rng_quant[1], rng_quant[2]) +
  labs(x=paste0("Predicted ", paste(inVar), " (units)"),
       y=paste0("Observed ", paste(inVar), " (units)"),
       title=paste0("Calibration: ", paste0("Rsq = ", cal.R2), "; ", paste0("RMSEP = ", 
                                                                            cal.RMSEP))) +
  theme(axis.text=element_text(size=18), legend.position="none",
        axis.title=element_text(size=20, face="bold"), 
        axis.text.x = element_text(angle = 0,vjust = 0.5),
        panel.border = element_rect(linetype = "solid", fill = NA, size=1.5))

cal_resid_histogram <- ggplot(cal.plsr.output, aes(x=PLSR_CV_Residuals)) +
  geom_histogram(alpha=.5, position="identity") + 
  geom_vline(xintercept = 0, color="black", 
             linetype="dashed", size=1) + theme_bw() + 
  theme(axis.text=element_text(size=18), legend.position="none",
        axis.title=element_text(size=20, face="bold"), 
        axis.text.x = element_text(angle = 0,vjust = 0.5),
        panel.border = element_rect(linetype = "solid", fill = NA, size=1.5))

rng_quant <- quantile(val.plsr.output[,inVar], probs = c(0.001, 0.999))
val_scatter_plot <- ggplot(val.plsr.output, aes(x=PLSR_Predicted, y=get(inVar))) + 
  theme_bw() + geom_point() + geom_abline(intercept = 0, slope = 1, color="dark grey", 
                                          linetype="dashed", size=1.5) + xlim(rng_quant[1], 
                                                                              rng_quant[2]) + 
  ylim(rng_quant[1], rng_quant[2]) +
  labs(x=paste0("Predicted ", paste(inVar), " (units)"),
       y=paste0("Observed ", paste(inVar), " (units)"),
       title=paste0("Validation: ", paste0("Rsq = ", val.R2), "; ", paste0("RMSEP = ", 
                                                                           val.RMSEP))) +
  theme(axis.text=element_text(size=18), legend.position="none",
        axis.title=element_text(size=20, face="bold"), 
        axis.text.x = element_text(angle = 0,vjust = 0.5),
        panel.border = element_rect(linetype = "solid", fill = NA, size=1.5))

val_resid_histogram <- ggplot(val.plsr.output, aes(x=PLSR_Residuals)) +
  geom_histogram(alpha=.5, position="identity") + 
  geom_vline(xintercept = 0, color="black", 
             linetype="dashed", size=1) + theme_bw() + 
  theme(axis.text=element_text(size=18), legend.position="none",
        axis.title=element_text(size=20, face="bold"), 
        axis.text.x = element_text(angle = 0,vjust = 0.5),
        panel.border = element_rect(linetype = "solid", fill = NA, size=1.5))

# plot cal/val side-by-side
scatterplots <- grid.arrange(cal_scatter_plot, val_scatter_plot, cal_resid_histogram, 
                             val_resid_histogram, nrow=2, ncol=2)

ggsave(filename = file.path(outdir,paste0(inVar,"_Cal_Val_Scatterplots.png")), 
       plot = scatterplots, device="png", width = 32, height = 30, units = "cm",
       dpi = 300)

Generate Coefficient and VIP plots

vips <- spectratrait::VIP(plsr.out)[nComps,]

par(mfrow=c(2,1))
plot(plsr.out, plottype = "coef",xlab="Wavelength (nm)",
     ylab="Regression coefficients",legendpos = "bottomright",
     ncomp=nComps,lwd=2)
box(lwd=2.2)
plot(seq(Start.wave,End.wave,1),vips,xlab="Wavelength (nm)",ylab="VIP",cex=0.01)
lines(seq(Start.wave,End.wave,1),vips,lwd=3)
abline(h=0.8,lty=2,col="dark grey")
box(lwd=2.2)
dev.copy(png,file.path(outdir,paste0(inVar,'_Coefficient_VIP_plot.png')), 
         height=3100, width=4100, res=340)
quartz_off_screen 
                3 
dev.off();
quartz_off_screen 
                2 
par(opar)

Jackknife validation

if(grepl("Windows", sessionInfo()$running)){
  pls.options(parallel =NULL)
} else {
  pls.options(parallel = parallel::detectCores()-1)
}

seg <- 100
jk.plsr.out <- pls::plsr(as.formula(paste(inVar,"~","Spectra")), scale=FALSE, 
                         center=TRUE, ncomp=nComps, validation="CV", 
                         segments = seg, segment.type="interleaved", trace=FALSE, 
                         jackknife=TRUE, data=cal.plsr.data)
pls.options(parallel = NULL)

Jackknife_coef <- f.coef.valid(plsr.out = jk.plsr.out, data_plsr = cal.plsr.data, 
                               ncomp = nComps, inVar=inVar)
Jackknife_intercept <- Jackknife_coef[1,,,]
Jackknife_coef <- Jackknife_coef[2:dim(Jackknife_coef)[1],,,]

interval <- c(0.025,0.975)
Jackknife_Pred <- val.plsr.data$Spectra %*% Jackknife_coef + 
  matrix(rep(Jackknife_intercept, length(val.plsr.data[,inVar])), byrow=TRUE, 
         ncol=length(Jackknife_intercept))
Interval_Conf <- apply(X = Jackknife_Pred, MARGIN = 1, FUN = quantile, 
                       probs=c(interval[1], interval[2]))
sd_mean <- apply(X = Jackknife_Pred, MARGIN = 1, FUN =sd)
sd_res <- sd(val.plsr.output$PLSR_Residuals)
sd_tot <- sqrt(sd_mean^2+sd_res^2)
val.plsr.output$LCI <- Interval_Conf[1,]
val.plsr.output$UCI <- Interval_Conf[2,]
val.plsr.output$LPI <- val.plsr.output$PLSR_Predicted-1.96*sd_tot
val.plsr.output$UPI <- val.plsr.output$PLSR_Predicted+1.96*sd_tot
head(val.plsr.output)

Jackknife coefficient plot

spectratrait::f.plot.coef(Z = t(Jackknife_coef), wv = wv, 
            plot_label="Jackknife regression coefficients",position = 'bottomleft')
abline(h=0,lty=2,col="grey50")
box(lwd=2.2)
dev.copy(png,file.path(outdir,paste0(inVar,'_Jackknife_Regression_Coefficients.png')), 
         height=2100, width=3800, res=340)
quartz_off_screen 
                3 
dev.off();
quartz_off_screen 
                2 

Jackknife validation plot

rmsep_percrmsep <- spectratrait::percent_rmse(plsr_dataset = val.plsr.output, 
                                              inVar = inVar, 
                                              residuals = val.plsr.output$PLSR_Residuals, 
                                              range="full")
RMSEP <- rmsep_percrmsep$rmse
perc_RMSEP <- rmsep_percrmsep$perc_rmse
r2 <- round(pls::R2(plsr.out, newdata = val.plsr.data, intercept=F)$val[nComps],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(val.plsr.output$LPI), max(val.plsr.output$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(val.plsr.output$PLSR_Predicted,val.plsr.output[,inVar], 
       li=val.plsr.output$LPI, ui=val.plsr.output$UPI, 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="grey50",
       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)
legend("topleft", legend=expr, 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 

Output jackknife results

out.jk.coefs <- data.frame(Iteration=seq(1,seg,1),
                           Intercept=Jackknife_intercept,t(Jackknife_coef))
head(out.jk.coefs)[1:6]
write.csv(out.jk.coefs,file=file.path(outdir,
                                      paste0(inVar,
                                             '_Jackkife_PLSR_Coefficients.csv')),
          row.names=FALSE)

Create core PLSR outputs

print(paste("Output directory: ", getwd()))
[1] "Output directory:  /Users/sserbin/Data/GitHub/spectratrait/vignettes"
# Observed versus predicted
write.csv(cal.plsr.output,file=file.path(outdir,
                                         paste0(inVar,'_Observed_PLSR_CV_Pred_',
                                                nComps,'comp.csv')),
          row.names=FALSE)

# Validation data
write.csv(val.plsr.output,file=file.path(outdir,
                                         paste0(inVar,'_Validation_PLSR_Pred_',
                                                nComps,'comp.csv')),
          row.names=FALSE)

# Model coefficients
coefs <- coef(plsr.out,ncomp=nComps,intercept=TRUE)
write.csv(coefs,file=file.path(outdir,
                               paste0(inVar,'_PLSR_Coefficients_',
                                      nComps,'comp.csv')),
          row.names=TRUE)

# PLSR VIP
write.csv(vips,file=file.path(outdir,
                              paste0(inVar,'_PLSR_VIPs_',
                                     nComps,'comp.csv')))

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] "SLA_g_cm_Cal_PLSR_Dataset.csv"                  "SLA_g_cm_Cal_Val_Histograms.png"                "SLA_g_cm_Cal_Val_Scatterplots.png"             
 [4] "SLA_g_cm_Cal_Val_Spectra.png"                   "SLA_g_cm_Coefficient_VIP_plot.png"              "SLA_g_cm_Jackkife_PLSR_Coefficients.csv"       
 [7] "SLA_g_cm_Jackknife_Regression_Coefficients.png" "SLA_g_cm_Observed_PLSR_CV_Pred_10comp.csv"      "SLA_g_cm_PLSR_Coefficients_10comp.csv"         
[10] "SLA_g_cm_PLSR_Component_Selection.png"          "SLA_g_cm_PLSR_Validation_Scatterplot.png"       "SLA_g_cm_PLSR_VIPs_10comp.csv"                 
[13] "SLA_g_cm_Val_PLSR_Dataset.csv"                  "SLA_g_cm_Validation_PLSR_Pred_10comp.csv"       "SLA_g_cm_Validation_RMSEP_R2_by_Component.png" 
LS0tCnRpdGxlOiBTcGVjdHJhLXRyYWl0IFBMU1IgZXhhbXBsZSB1c2luZyBsZWFmLWxldmVsIHNwZWN0cmEgYW5kIHNwZWNpZmljIGxlYWYgYXJlYSAoU0xBKSBkYXRhIGZyb20gbW9yZSB0aGFuIDQwIHNwZWNpZXMgZ3Jhc3NsYW5kIHNwZWNpZXMgY29tcHJpc2luZyBib3RoIGhlcmJzIGFuZCBncmFtaW5vaWRzCmF1dGhvcjogIlNoYXduIFAuIFNlcmJpbiwgSnVsaWVuIExhbW91ciwgJiBKZXJlbWlhaCBBbmRlcnNvbiIKZGF0ZTogImByIFN5cy5EYXRlKClgIgpvdXRwdXQ6CiAgaHRtbF9ub3RlYm9vazogZGVmYXVsdAogIHBkZl9kb2N1bWVudDogZGVmYXVsdAogIGdpdGh1Yl9kb2N1bWVudDogZGVmYXVsdAogIGh0bWxfZG9jdW1lbnQ6CiAgICBkZl9wcmludDogcGFnZWQKICBybWFya2Rvd246IGh0bWxfdmlnbmV0dGUKdmlnbmV0dGU6ID4KICAlXFZpZ25ldHRlSW5kZXhFbnRyeXtTcGVjdHJhLXRyYWl0IFBMU1IgZXhhbXBsZSB1c2luZyBsZWFmLWxldmVsIHNwZWN0cmEgYW5kIHNwZWNpZmljIGxlYWYgYXJlYSAoU0xBKSBkYXRhIGZyb20gbW9yZSB0aGFuIDQwIHNwZWNpZXMgZ3Jhc3NsYW5kIHNwZWNpZXMgY29tcHJpc2luZyBib3RoIGhlcmJzIGFuZCBncmFtaW5vaWRzfQogICVcdXNlcGFja2FnZVt1dGY4XXtpbnB1dGVuY30KICAlXFZpZ25ldHRlRW5naW5le2tuaXRyOjprbml0cn0KLS0tCgpgYGB7ciBzZXR1cCwgaW5jbHVkZT1GQUxTRSwgZWNobz1GQUxTRX0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFKQpgYGAKCiMjIyBPdmVydmlldwpUaGlzIGlzIGFuIFtSIE1hcmtkb3duXShodHRwOi8vcm1hcmtkb3duLnJzdHVkaW8uY29tKSBOb3RlYm9vayB0byBpbGx1c3RyYXRlIGhvdyB0byByZXRyaWV2ZSBhIGRhdGFzZXQgZnJvbSB0aGUgRWNvU0lTIHNwZWN0cmFsIGRhdGFiYXNlLCBjaG9vc2UgdGhlICJvcHRpbWFsIiBudW1iZXIgb2YgcGxzciBjb21wb25lbnRzLCBhbmQgZml0IGEgcGxzciBtb2RlbCBmb3Igc3BlY2lmaWMgbGVhZiBhcmVhIChTTEEpLiBJbiB0aGlzIGV4YW1wbGUsIHRoZSBwbGFudHMgd2VyZSBjdWx0aXZhdGVkIGluIGFuIG91dGRvb3Igc2V0dGluZyBpbiB0aGUgYm90YW5pY2FsIGdhcmRlbiBvZiB0aGUgS0lUIHVzaW5nIDQweDQwIGNtIHBvdHMgd2l0aCBhbiBzdGFuZGFyZGl6ZWQgc3Vic3RyYXRlLiBUaGUgZGF0YSB3YXMgbWVhc3VyZWQgb24gYSB3ZWVrbHkgYmFzaXMgKHRoZSB0aW1lc3RhbXAgaXMgaW5jbHVkZWQgaW4gdGhlIGRhdGFzZXQpLiAKCiMjIyBHZXR0aW5nIFN0YXJ0ZWQKIyMjIExvYWQgbGlicmFyaWVzCmBgYHtyLCBldmFsPVRSVUUsIGVjaG89VFJVRX0KbGlzdC5vZi5wYWNrYWdlcyA8LSBjKCJwbHMiLCJkcGx5ciIsInJlc2hhcGUyIiwiaGVyZSIsInBsb3RyaXgiLCJnZ3Bsb3QyIiwiZ3JpZEV4dHJhIiwKICAgICAgICAgICAgICAgICAgICAgICJzcGVjdHJhdHJhaXQiKQppbnZpc2libGUobGFwcGx5KGxpc3Qub2YucGFja2FnZXMsIGxpYnJhcnksIGNoYXJhY3Rlci5vbmx5ID0gVFJVRSkpCmBgYAoKIyMjIFNldHVwIG90aGVyIGZ1bmN0aW9ucyBhbmQgb3B0aW9ucwpgYGB7ciwgZWNobz1UUlVFfQojIyMgU2V0dXAgb3B0aW9ucwoKIyBTY3JpcHQgb3B0aW9ucwpwbHM6OnBscy5vcHRpb25zKHBsc3JhbGcgPSAib3Njb3Jlc3BscyIpCnBsczo6cGxzLm9wdGlvbnMoInBsc3JhbGciKQoKIyBEZWZhdWx0IHBhciBvcHRpb25zCm9wYXIgPC0gcGFyKG5vLnJlYWRvbmx5ID0gVCkKCiMgV2hhdCBpcyB0aGUgdGFyZ2V0IHZhcmlhYmxlPwppblZhciA8LSAiU0xBX2dfY20iCgojIFdoYXQgaXMgdGhlIHNvdXJjZSBkYXRhc2V0IGZyb20gRWNvU0lTPwplY29zaXNfaWQgPC0gIjNjZjZiMjdlLWQ4MGUtNGJjNy1iMjE0LWM5NTUwNmU0NmRhYSIKCiMgU3BlY2lmeSBvdXRwdXQgZGlyZWN0b3J5LCBvdXRwdXRfZGlyIAojIE9wdGlvbnM6IAojIHRlbXBkaXIgLSB1c2UgYSBPUy1zcGVjaWZpZWQgdGVtcG9yYXJ5IGRpcmVjdG9yeSAKIyB1c2VyIGRlZmluZWQgUEFUSCAtIGUuZy4gIn4vc2NyYXRjaC9QTFNSIgpvdXRwdXRfZGlyIDwtICJ0ZW1wZGlyIgpgYGAKCiMjIyBTZXQgd29ya2luZyBkaXJlY3RvcnkgKHNjcmF0Y2ggc3BhY2UpCmBgYHtyLCBlY2hvPUZBTFNFfQpvdXRkaXIgPC0gdGVtcGRpcigpCnNldHdkKG91dGRpcikgIyBzZXQgd29ya2luZyBkaXJlY3RvcnkKcHJpbnQocGFzdGUwKCJPdXRwdXQgZGlyZWN0b3J5OiAiLGdldHdkKCkpKQpgYGAKCiMjIyBHcmFiIGRhdGEgZnJvbSBFY29TSVMKYGBge3IsIGVjaG89VFJVRX0KcHJpbnQocGFzdGUwKCJPdXRwdXQgZGlyZWN0b3J5OiAiLGdldHdkKCkpKSAgIyBjaGVjayB3ZAojIyMgR2V0IHNvdXJjZSBkYXRhc2V0IGZyb20gRWNvU0lTCmRhdF9yYXcgPC0gc3BlY3RyYXRyYWl0OjpnZXRfZWNvc2lzX2RhdGEoZWNvc2lzX2lkID0gZWNvc2lzX2lkKQpoZWFkKGRhdF9yYXcpCm5hbWVzKGRhdF9yYXcpWzE6NDBdCmBgYAoKIyMjIENyZWF0ZSBmdWxsIHBsc3IgZGF0YXNldApgYGB7ciwgZWNobz1UUlVFfQojIyMgQ3JlYXRlIHBsc3IgZGF0YXNldApTdGFydC53YXZlIDwtIDUwMApFbmQud2F2ZSA8LSAyNDAwCnd2IDwtIHNlcShTdGFydC53YXZlLEVuZC53YXZlLDEpClNwZWN0cmEgPC0gYXMubWF0cml4KGRhdF9yYXdbLG5hbWVzKGRhdF9yYXcpICVpbiUgd3ZdKQpjb2xuYW1lcyhTcGVjdHJhKSA8LSBjKHBhc3RlMCgiV2F2ZV8iLHd2KSkKc2FtcGxlX2luZm8gPC0gZGF0X3Jhd1ssbmFtZXMoZGF0X3JhdykgJW5vdGluJSBzZXEoMzUwLDI1MDAsMSldCmhlYWQoc2FtcGxlX2luZm8pCgpzYW1wbGVfaW5mbzIgPC0gc2FtcGxlX2luZm8gJT4lCiAgc2VsZWN0KFBsYW50X1NwZWNpZXM9c3BlY2llcyxHcm93dGhfRm9ybT1gZ3Jvd3RoIGZvcm1gLHRpbWVzdGFtcCwKICAgICAgICAgU0xBX2dfY209YFNMQSAoZy9jbSApYCkgJT4lCiAgbXV0YXRlKFNMQV9nX2NtPWFzLm51bWVyaWMoU0xBX2dfY20pKSAjIGVuc3VyZSBTTEEgaXMgbnVtZXJpYwpoZWFkKHNhbXBsZV9pbmZvMikKCnBsc3JfZGF0YSA8LSBkYXRhLmZyYW1lKHNhbXBsZV9pbmZvMixTcGVjdHJhKQpybShzYW1wbGVfaW5mbyxzYW1wbGVfaW5mbzIsU3BlY3RyYSkKYGBgCgojIyMgRXhhbXBsZSBkYXRhIGNsZWFuaW5nCmBgYHtyfQojIyMjIEVuZCB1c2VyIG5lZWRzIHRvIGRvIHdoYXQncyBhcHByb3ByaWF0ZSBmb3IgdGhlaXIgZGF0YS4gIFRoaXMgbWF5IGJlIGFuIGl0ZXJhdGl2ZSBwcm9jZXNzLgojIEtlZXAgb25seSBjb21wbGV0ZSByb3dzIG9mIGluVmFyIGFuZCBzcGVjIGRhdGEgYmVmb3JlIGZpdHRpbmcKcGxzcl9kYXRhIDwtIHBsc3JfZGF0YVtjb21wbGV0ZS5jYXNlcyhwbHNyX2RhdGFbLG5hbWVzKHBsc3JfZGF0YSkgJWluJSBjKGluVmFyLHd2KV0pLF0KIyBSZW1vdmUgc3VzcGVjdCBoaWdoIHZhbHVlcwpwbHNyX2RhdGEgPC0gcGxzcl9kYXRhWyBwbHNyX2RhdGFbLGluVmFyXSA8PSA1MDAsIF0KYGBgCgojIyMgQ3JlYXRlIGNhbC92YWwgZGF0YXNldHMKYGBge3IsIGZpZy5oZWlnaHQgPSA1LCBmaWcud2lkdGggPSAxMiwgZWNobz1UUlVFfQojIyMgQ3JlYXRlIGNhbC92YWwgZGF0YXNldHMKIyMgTWFrZSBhIHN0cmF0aWZpZWQgcmFuZG9tIHNhbXBsaW5nIGluIHRoZSBzdHJhdGEgVVNEQV9TcGVjaWVzX0NvZGUgYW5kIERvbWFpbgoKbWV0aG9kIDwtICJiYXNlIiAjYmFzZS9kcGx5cgojIGJhc2UgUiAtIGEgYml0IHNsb3cKIyBkcGx5ciAtIG11Y2ggZmFzdGVyCnNwbGl0X2RhdGEgPC0gc3BlY3RyYXRyYWl0OjpjcmVhdGVfZGF0YV9zcGxpdChkYXRhc2V0PXBsc3JfZGF0YSwgYXBwcm9hY2g9bWV0aG9kLCBzcGxpdF9zZWVkPTIzNTY4MTIsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcHJvcD0wLjgsIGdyb3VwX3ZhcmlhYmxlcz0iUGxhbnRfU3BlY2llcyIpCm5hbWVzKHNwbGl0X2RhdGEpCmNhbC5wbHNyLmRhdGEgPC0gc3BsaXRfZGF0YSRjYWxfZGF0YQp2YWwucGxzci5kYXRhIDwtIHNwbGl0X2RhdGEkdmFsX2RhdGEKcm0oc3BsaXRfZGF0YSkKCiMgRGF0YXNldHM6CnByaW50KHBhc3RlKCJDYWwgb2JzZXJ2YXRpb25zOiAiLGRpbShjYWwucGxzci5kYXRhKVsxXSxzZXA9IiIpKQpwcmludChwYXN0ZSgiVmFsIG9ic2VydmF0aW9uczogIixkaW0odmFsLnBsc3IuZGF0YSlbMV0sc2VwPSIiKSkKCmNhbF9oaXN0X3Bsb3QgPC0gcXBsb3QoY2FsLnBsc3IuZGF0YVsscGFzdGUwKGluVmFyKV0sZ2VvbT0iaGlzdG9ncmFtIiwKICAgICAgICAgICAgICAgICAgICAgICBtYWluID0gcGFzdGUwKCJDYWwuIEhpc3RvZ3JhbSBmb3IgIixpblZhciksCiAgICAgICAgICAgICAgICAgICAgICAgeGxhYiA9IHBhc3RlMChpblZhcikseWxhYiA9ICJDb3VudCIsZmlsbD1JKCJncmV5NTAiKSwKICAgICAgICAgICAgICAgICAgICAgICBjb2w9SSgiYmxhY2siKSxhbHBoYT1JKC43KSkKdmFsX2hpc3RfcGxvdCA8LSBxcGxvdCh2YWwucGxzci5kYXRhWyxwYXN0ZTAoaW5WYXIpXSxnZW9tPSJoaXN0b2dyYW0iLAogICAgICAgICAgICAgICAgICAgICAgIG1haW4gPSBwYXN0ZTAoIlZhbC4gSGlzdG9ncmFtIGZvciAiLGluVmFyKSwKICAgICAgICAgICAgICAgICAgICAgICB4bGFiID0gcGFzdGUwKGluVmFyKSx5bGFiID0gIkNvdW50IixmaWxsPUkoImdyZXk1MCIpLAogICAgICAgICAgICAgICAgICAgICAgIGNvbD1JKCJibGFjayIpLGFscGhhPUkoLjcpKQpoaXN0b2dyYW1zIDwtIGdyaWQuYXJyYW5nZShjYWxfaGlzdF9wbG90LCB2YWxfaGlzdF9wbG90LCBuY29sPTIpCmdnc2F2ZShmaWxlbmFtZSA9IGZpbGUucGF0aChvdXRkaXIscGFzdGUwKGluVmFyLCJfQ2FsX1ZhbF9IaXN0b2dyYW1zLnBuZyIpKSwgCiAgICAgICBwbG90ID0gaGlzdG9ncmFtcywgZGV2aWNlPSJwbmciLCB3aWR0aCA9IDMwLCBoZWlnaHQgPSAxMiwgdW5pdHMgPSAiY20iLAogICAgICAgZHBpID0gMzAwKQojIG91dHB1dCBjYWwvdmFsIGRhdGEKd3JpdGUuY3N2KGNhbC5wbHNyLmRhdGEsZmlsZT1maWxlLnBhdGgob3V0ZGlyLHBhc3RlMChpblZhciwnX0NhbF9QTFNSX0RhdGFzZXQuY3N2JykpLAogICAgICAgICAgcm93Lm5hbWVzPUZBTFNFKQp3cml0ZS5jc3YodmFsLnBsc3IuZGF0YSxmaWxlPWZpbGUucGF0aChvdXRkaXIscGFzdGUwKGluVmFyLCdfVmFsX1BMU1JfRGF0YXNldC5jc3YnKSksCiAgICAgICAgICByb3cubmFtZXM9RkFMU0UpCmBgYAoKIyMjIENyZWF0ZSBjYWxpYnJhdGlvbiBhbmQgdmFsaWRhdGlvbiBQTFNSIGRhdGFzZXRzCmBgYHtyLCBlY2hvPVRSVUV9CiMjIyBGb3JtYXQgUExTUiBkYXRhIGZvciBtb2RlbCBmaXR0aW5nIApjYWxfc3BlYyA8LSBhcy5tYXRyaXgoY2FsLnBsc3IuZGF0YVssIHdoaWNoKG5hbWVzKGNhbC5wbHNyLmRhdGEpICVpbiUgcGFzdGUwKCJXYXZlXyIsd3YpKV0pCmNhbC5wbHNyLmRhdGEgPC0gZGF0YS5mcmFtZShjYWwucGxzci5kYXRhWywgd2hpY2gobmFtZXMoY2FsLnBsc3IuZGF0YSkgJW5vdGluJSBwYXN0ZTAoIldhdmVfIix3dikpXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIFNwZWN0cmE9SShjYWxfc3BlYykpCmhlYWQoY2FsLnBsc3IuZGF0YSlbMTo1XQoKdmFsX3NwZWMgPC0gYXMubWF0cml4KHZhbC5wbHNyLmRhdGFbLCB3aGljaChuYW1lcyh2YWwucGxzci5kYXRhKSAlaW4lIHBhc3RlMCgiV2F2ZV8iLHd2KSldKQp2YWwucGxzci5kYXRhIDwtIGRhdGEuZnJhbWUodmFsLnBsc3IuZGF0YVssIHdoaWNoKG5hbWVzKHZhbC5wbHNyLmRhdGEpICVub3RpbiUgcGFzdGUwKCJXYXZlXyIsd3YpKV0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBTcGVjdHJhPUkodmFsX3NwZWMpKQpoZWFkKHZhbC5wbHNyLmRhdGEpWzE6NV0KYGBgCgojIyMgcGxvdCBjYWwgYW5kIHZhbCBzcGVjdHJhCmBgYHtyLCBmaWcuaGVpZ2h0ID0gNSwgZmlnLndpZHRoID0gMTIsIGVjaG89VFJVRX0KcGFyKG1mcm93PWMoMSwyKSkgIyBCLCBMLCBULCBSCnNwZWN0cmF0cmFpdDo6Zi5wbG90LnNwZWMoWj1jYWwucGxzci5kYXRhJFNwZWN0cmEsd3Y9d3YscGxvdF9sYWJlbD0iQ2FsaWJyYXRpb24iKQpzcGVjdHJhdHJhaXQ6OmYucGxvdC5zcGVjKFo9dmFsLnBsc3IuZGF0YSRTcGVjdHJhLHd2PXd2LHBsb3RfbGFiZWw9IlZhbGlkYXRpb24iKQoKZGV2LmNvcHkocG5nLGZpbGUucGF0aChvdXRkaXIscGFzdGUwKGluVmFyLCdfQ2FsX1ZhbF9TcGVjdHJhLnBuZycpKSwgCiAgICAgICAgIGhlaWdodD0yNTAwLHdpZHRoPTQ5MDAsIHJlcz0zNDApCmRldi5vZmYoKTsKcGFyKG1mcm93PWMoMSwxKSkKYGBgCgojIyMgVXNlIEphY2trbmlmZSBwZXJtdXRhdGlvbiB0byBkZXRlcm1pbmUgb3B0aW1hbCBudW1iZXIgb2YgY29tcG9uZW50cwpgYGB7ciwgZmlnLmhlaWdodCA9IDYsIGZpZy53aWR0aCA9IDEwLCBlY2hvPVRSVUV9CiMjIyBVc2UgcGVybXV0YXRpb24gdG8gZGV0ZXJtaW5lIHRoZSBvcHRpbWFsIG51bWJlciBvZiBjb21wb25lbnRzCmlmKGdyZXBsKCJXaW5kb3dzIiwgc2Vzc2lvbkluZm8oKSRydW5uaW5nKSl7CiAgcGxzLm9wdGlvbnMocGFyYWxsZWwgPSBOVUxMKQp9IGVsc2UgewogIHBscy5vcHRpb25zKHBhcmFsbGVsID0gcGFyYWxsZWw6OmRldGVjdENvcmVzKCktMSkKfQoKbWV0aG9kIDwtICJwbHMiICNwbHMsIGZpcnN0UGxhdGVhdSwgZmlyc3RNaW4KcmFuZG9tX3NlZWQgPC0gMjM1NjgxMgpzZWcgPC0gMTAwCm1heENvbXBzIDwtIDE4Cml0ZXJhdGlvbnMgPC0gNTAKcHJvcCA8LSAwLjcwCmlmIChtZXRob2Q9PSJwbHMiKSB7CiAgIyBwbHMgcGFja2FnZSBhcHByb2FjaCAtIGZhc3RlciBidXQgZXN0aW1hdGVzIG1vcmUgY29tcG9uZW50cy4uLi4KICBuQ29tcHMgPC0gc3BlY3RyYXRyYWl0OjpmaW5kX29wdGltYWxfY29tcG9uZW50cyhkYXRhc2V0PWNhbC5wbHNyLmRhdGEsIHRhcmdldFZhcmlhYmxlPWluVmFyLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtZXRob2Q9bWV0aG9kLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtYXhDb21wcz1tYXhDb21wcywgc2VnPXNlZywgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmFuZG9tX3NlZWQ9cmFuZG9tX3NlZWQpCiAgcHJpbnQocGFzdGUwKCIqKiogT3B0aW1hbCBudW1iZXIgb2YgY29tcG9uZW50czogIiwgbkNvbXBzKSkKfSBlbHNlIHsKICBuQ29tcHMgPC0gc3BlY3RyYXRyYWl0OjpmaW5kX29wdGltYWxfY29tcG9uZW50cyhkYXRhc2V0PWNhbC5wbHNyLmRhdGEsIHRhcmdldFZhcmlhYmxlPWluVmFyLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtZXRob2Q9bWV0aG9kLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtYXhDb21wcz1tYXhDb21wcywgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaXRlcmF0aW9ucz1pdGVyYXRpb25zLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzZWc9c2VnLCBwcm9wPXByb3AsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJhbmRvbV9zZWVkPXJhbmRvbV9zZWVkKQp9CmRldi5jb3B5KHBuZyxmaWxlLnBhdGgob3V0ZGlyLHBhc3RlMChwYXN0ZTAoaW5WYXIsIl9QTFNSX0NvbXBvbmVudF9TZWxlY3Rpb24ucG5nIikpKSwgCiAgICAgICAgIGhlaWdodD0yODAwLCB3aWR0aD0zNDAwLCAgcmVzPTM0MCkKZGV2Lm9mZigpOwpgYGAKCiMjIyBGaXQgZmluYWwgbW9kZWwKYGBge3IsIGZpZy5oZWlnaHQgPSA1LCBmaWcud2lkdGggPSAxMiwgZWNobz1UUlVFfQpzZWdzIDwtIDEwMApwbHNyLm91dCA8LSBwbHNyKGFzLmZvcm11bGEocGFzdGUoaW5WYXIsIn4iLCJTcGVjdHJhIikpLHNjYWxlPUZBTFNFLG5jb21wPW5Db21wcyx2YWxpZGF0aW9uPSJDViIsCiAgICAgICAgICAgICAgICAgc2VnbWVudHM9c2Vncywgc2VnbWVudC50eXBlPSJpbnRlcmxlYXZlZCIsdHJhY2U9RkFMU0UsZGF0YT1jYWwucGxzci5kYXRhKQpmaXQgPC0gcGxzci5vdXQkZml0dGVkLnZhbHVlc1ssMSxuQ29tcHNdCnBscy5vcHRpb25zKHBhcmFsbGVsID0gTlVMTCkKCiMgRXh0ZXJuYWwgdmFsaWRhdGlvbiBmaXQgc3RhdHMKcGFyKG1mcm93PWMoMSwyKSkgIyBCLCBMLCBULCBSCnBsczo6Uk1TRVAocGxzci5vdXQsIG5ld2RhdGEgPSB2YWwucGxzci5kYXRhKQpwbG90KHBsczo6Uk1TRVAocGxzci5vdXQsZXN0aW1hdGU9YygidGVzdCIpLG5ld2RhdGEgPSB2YWwucGxzci5kYXRhKSwgbWFpbj0iTU9ERUwgUk1TRVAiLAogICAgIHhsYWI9Ik51bWJlciBvZiBDb21wb25lbnRzIix5bGFiPSJNb2RlbCBWYWxpZGF0aW9uIFJNU0VQIixsdHk9MSxjb2w9ImJsYWNrIixjZXg9MS41LGx3ZD0yKQpib3gobHdkPTIuMikKCnBsczo6UjIocGxzci5vdXQsIG5ld2RhdGEgPSB2YWwucGxzci5kYXRhKQpwbG90KFIyKHBsc3Iub3V0LGVzdGltYXRlPWMoInRlc3QiKSxuZXdkYXRhID0gdmFsLnBsc3IuZGF0YSksIG1haW49Ik1PREVMIFIyIiwKICAgICB4bGFiPSJOdW1iZXIgb2YgQ29tcG9uZW50cyIseWxhYj0iTW9kZWwgVmFsaWRhdGlvbiBSMiIsbHR5PTEsY29sPSJibGFjayIsY2V4PTEuNSxsd2Q9MikKYm94KGx3ZD0yLjIpCmRldi5jb3B5KHBuZyxmaWxlLnBhdGgob3V0ZGlyLHBhc3RlMChwYXN0ZTAoaW5WYXIsIl9WYWxpZGF0aW9uX1JNU0VQX1IyX2J5X0NvbXBvbmVudC5wbmciKSkpLCAKICAgICAgICAgaGVpZ2h0PTI4MDAsIHdpZHRoPTQ4MDAsICByZXM9MzQwKQpkZXYub2ZmKCk7CnBhcihvcGFyKQpgYGAKCiMjIyBQTFNSIGZpdCBvYnNlcnZlZCB2cy4gcHJlZGljdGVkIHBsb3QgZGF0YQpgYGB7ciwgZmlnLmhlaWdodCA9IDE1LCBmaWcud2lkdGggPSAxNSwgZWNobz1UUlVFfSAgCiNjYWxpYnJhdGlvbgpjYWwucGxzci5vdXRwdXQgPC0gZGF0YS5mcmFtZShjYWwucGxzci5kYXRhWywgd2hpY2gobmFtZXMoY2FsLnBsc3IuZGF0YSkgJW5vdGluJSAiU3BlY3RyYSIpXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgUExTUl9QcmVkaWN0ZWQ9Zml0LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBQTFNSX0NWX1ByZWRpY3RlZD1hcy52ZWN0b3IocGxzci5vdXQkdmFsaWRhdGlvbiRwcmVkWywsbkNvbXBzXSkpCmNhbC5wbHNyLm91dHB1dCA8LSBjYWwucGxzci5vdXRwdXQgJT4lCiAgbXV0YXRlKFBMU1JfQ1ZfUmVzaWR1YWxzID0gUExTUl9DVl9QcmVkaWN0ZWQtZ2V0KGluVmFyKSkKaGVhZChjYWwucGxzci5vdXRwdXQpCmNhbC5SMiA8LSByb3VuZChwbHM6OlIyKHBsc3Iub3V0LGludGVyY2VwdD1GKVtbMV1dW25Db21wc10sMikKY2FsLlJNU0VQIDwtIHJvdW5kKHNxcnQobWVhbihjYWwucGxzci5vdXRwdXQkUExTUl9DVl9SZXNpZHVhbHNeMikpLDIpCgp2YWwucGxzci5vdXRwdXQgPC0gZGF0YS5mcmFtZSh2YWwucGxzci5kYXRhWywgd2hpY2gobmFtZXModmFsLnBsc3IuZGF0YSkgJW5vdGluJSAiU3BlY3RyYSIpXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgUExTUl9QcmVkaWN0ZWQ9YXMudmVjdG9yKHByZWRpY3QocGxzci5vdXQsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuZXdkYXRhID0gdmFsLnBsc3IuZGF0YSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG5jb21wPW5Db21wcywgdHlwZT0icmVzcG9uc2UiKVssLDFdKSkKdmFsLnBsc3Iub3V0cHV0IDwtIHZhbC5wbHNyLm91dHB1dCAlPiUKICBtdXRhdGUoUExTUl9SZXNpZHVhbHMgPSBQTFNSX1ByZWRpY3RlZC1nZXQoaW5WYXIpKQpoZWFkKHZhbC5wbHNyLm91dHB1dCkKdmFsLlIyIDwtIHJvdW5kKHBsczo6UjIocGxzci5vdXQsbmV3ZGF0YT12YWwucGxzci5kYXRhLGludGVyY2VwdD1GKVtbMV1dW25Db21wc10sMikKdmFsLlJNU0VQIDwtIHJvdW5kKHNxcnQobWVhbih2YWwucGxzci5vdXRwdXQkUExTUl9SZXNpZHVhbHNeMikpLDIpCgpybmdfcXVhbnQgPC0gcXVhbnRpbGUoY2FsLnBsc3Iub3V0cHV0WyxpblZhcl0sIHByb2JzID0gYygwLjAwMSwgMC45OTkpKQpjYWxfc2NhdHRlcl9wbG90IDwtIGdncGxvdChjYWwucGxzci5vdXRwdXQsIGFlcyh4PVBMU1JfQ1ZfUHJlZGljdGVkLCB5PWdldChpblZhcikpKSArIAogIHRoZW1lX2J3KCkgKyBnZW9tX3BvaW50KCkgKyBnZW9tX2FibGluZShpbnRlcmNlcHQgPSAwLCBzbG9wZSA9IDEsIGNvbG9yPSJkYXJrIGdyZXkiLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbGluZXR5cGU9ImRhc2hlZCIsIHNpemU9MS41KSArIHhsaW0ocm5nX3F1YW50WzFdLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcm5nX3F1YW50WzJdKSArIAogIHlsaW0ocm5nX3F1YW50WzFdLCBybmdfcXVhbnRbMl0pICsKICBsYWJzKHg9cGFzdGUwKCJQcmVkaWN0ZWQgIiwgcGFzdGUoaW5WYXIpLCAiICh1bml0cykiKSwKICAgICAgIHk9cGFzdGUwKCJPYnNlcnZlZCAiLCBwYXN0ZShpblZhciksICIgKHVuaXRzKSIpLAogICAgICAgdGl0bGU9cGFzdGUwKCJDYWxpYnJhdGlvbjogIiwgcGFzdGUwKCJSc3EgPSAiLCBjYWwuUjIpLCAiOyAiLCBwYXN0ZTAoIlJNU0VQID0gIiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjYWwuUk1TRVApKSkgKwogIHRoZW1lKGF4aXMudGV4dD1lbGVtZW50X3RleHQoc2l6ZT0xOCksIGxlZ2VuZC5wb3NpdGlvbj0ibm9uZSIsCiAgICAgICAgYXhpcy50aXRsZT1lbGVtZW50X3RleHQoc2l6ZT0yMCwgZmFjZT0iYm9sZCIpLCAKICAgICAgICBheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChhbmdsZSA9IDAsdmp1c3QgPSAwLjUpLAogICAgICAgIHBhbmVsLmJvcmRlciA9IGVsZW1lbnRfcmVjdChsaW5ldHlwZSA9ICJzb2xpZCIsIGZpbGwgPSBOQSwgc2l6ZT0xLjUpKQoKY2FsX3Jlc2lkX2hpc3RvZ3JhbSA8LSBnZ3Bsb3QoY2FsLnBsc3Iub3V0cHV0LCBhZXMoeD1QTFNSX0NWX1Jlc2lkdWFscykpICsKICBnZW9tX2hpc3RvZ3JhbShhbHBoYT0uNSwgcG9zaXRpb249ImlkZW50aXR5IikgKyAKICBnZW9tX3ZsaW5lKHhpbnRlcmNlcHQgPSAwLCBjb2xvcj0iYmxhY2siLCAKICAgICAgICAgICAgIGxpbmV0eXBlPSJkYXNoZWQiLCBzaXplPTEpICsgdGhlbWVfYncoKSArIAogIHRoZW1lKGF4aXMudGV4dD1lbGVtZW50X3RleHQoc2l6ZT0xOCksIGxlZ2VuZC5wb3NpdGlvbj0ibm9uZSIsCiAgICAgICAgYXhpcy50aXRsZT1lbGVtZW50X3RleHQoc2l6ZT0yMCwgZmFjZT0iYm9sZCIpLCAKICAgICAgICBheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChhbmdsZSA9IDAsdmp1c3QgPSAwLjUpLAogICAgICAgIHBhbmVsLmJvcmRlciA9IGVsZW1lbnRfcmVjdChsaW5ldHlwZSA9ICJzb2xpZCIsIGZpbGwgPSBOQSwgc2l6ZT0xLjUpKQoKcm5nX3F1YW50IDwtIHF1YW50aWxlKHZhbC5wbHNyLm91dHB1dFssaW5WYXJdLCBwcm9icyA9IGMoMC4wMDEsIDAuOTk5KSkKdmFsX3NjYXR0ZXJfcGxvdCA8LSBnZ3Bsb3QodmFsLnBsc3Iub3V0cHV0LCBhZXMoeD1QTFNSX1ByZWRpY3RlZCwgeT1nZXQoaW5WYXIpKSkgKyAKICB0aGVtZV9idygpICsgZ2VvbV9wb2ludCgpICsgZ2VvbV9hYmxpbmUoaW50ZXJjZXB0ID0gMCwgc2xvcGUgPSAxLCBjb2xvcj0iZGFyayBncmV5IiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGxpbmV0eXBlPSJkYXNoZWQiLCBzaXplPTEuNSkgKyB4bGltKHJuZ19xdWFudFsxXSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJuZ19xdWFudFsyXSkgKyAKICB5bGltKHJuZ19xdWFudFsxXSwgcm5nX3F1YW50WzJdKSArCiAgbGFicyh4PXBhc3RlMCgiUHJlZGljdGVkICIsIHBhc3RlKGluVmFyKSwgIiAodW5pdHMpIiksCiAgICAgICB5PXBhc3RlMCgiT2JzZXJ2ZWQgIiwgcGFzdGUoaW5WYXIpLCAiICh1bml0cykiKSwKICAgICAgIHRpdGxlPXBhc3RlMCgiVmFsaWRhdGlvbjogIiwgcGFzdGUwKCJSc3EgPSAiLCB2YWwuUjIpLCAiOyAiLCBwYXN0ZTAoIlJNU0VQID0gIiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHZhbC5STVNFUCkpKSArCiAgdGhlbWUoYXhpcy50ZXh0PWVsZW1lbnRfdGV4dChzaXplPTE4KSwgbGVnZW5kLnBvc2l0aW9uPSJub25lIiwKICAgICAgICBheGlzLnRpdGxlPWVsZW1lbnRfdGV4dChzaXplPTIwLCBmYWNlPSJib2xkIiksIAogICAgICAgIGF4aXMudGV4dC54ID0gZWxlbWVudF90ZXh0KGFuZ2xlID0gMCx2anVzdCA9IDAuNSksCiAgICAgICAgcGFuZWwuYm9yZGVyID0gZWxlbWVudF9yZWN0KGxpbmV0eXBlID0gInNvbGlkIiwgZmlsbCA9IE5BLCBzaXplPTEuNSkpCgp2YWxfcmVzaWRfaGlzdG9ncmFtIDwtIGdncGxvdCh2YWwucGxzci5vdXRwdXQsIGFlcyh4PVBMU1JfUmVzaWR1YWxzKSkgKwogIGdlb21faGlzdG9ncmFtKGFscGhhPS41LCBwb3NpdGlvbj0iaWRlbnRpdHkiKSArIAogIGdlb21fdmxpbmUoeGludGVyY2VwdCA9IDAsIGNvbG9yPSJibGFjayIsIAogICAgICAgICAgICAgbGluZXR5cGU9ImRhc2hlZCIsIHNpemU9MSkgKyB0aGVtZV9idygpICsgCiAgdGhlbWUoYXhpcy50ZXh0PWVsZW1lbnRfdGV4dChzaXplPTE4KSwgbGVnZW5kLnBvc2l0aW9uPSJub25lIiwKICAgICAgICBheGlzLnRpdGxlPWVsZW1lbnRfdGV4dChzaXplPTIwLCBmYWNlPSJib2xkIiksIAogICAgICAgIGF4aXMudGV4dC54ID0gZWxlbWVudF90ZXh0KGFuZ2xlID0gMCx2anVzdCA9IDAuNSksCiAgICAgICAgcGFuZWwuYm9yZGVyID0gZWxlbWVudF9yZWN0KGxpbmV0eXBlID0gInNvbGlkIiwgZmlsbCA9IE5BLCBzaXplPTEuNSkpCgojIHBsb3QgY2FsL3ZhbCBzaWRlLWJ5LXNpZGUKc2NhdHRlcnBsb3RzIDwtIGdyaWQuYXJyYW5nZShjYWxfc2NhdHRlcl9wbG90LCB2YWxfc2NhdHRlcl9wbG90LCBjYWxfcmVzaWRfaGlzdG9ncmFtLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICB2YWxfcmVzaWRfaGlzdG9ncmFtLCBucm93PTIsIG5jb2w9MikKZ2dzYXZlKGZpbGVuYW1lID0gZmlsZS5wYXRoKG91dGRpcixwYXN0ZTAoaW5WYXIsIl9DYWxfVmFsX1NjYXR0ZXJwbG90cy5wbmciKSksIAogICAgICAgcGxvdCA9IHNjYXR0ZXJwbG90cywgZGV2aWNlPSJwbmciLCB3aWR0aCA9IDMyLCBoZWlnaHQgPSAzMCwgdW5pdHMgPSAiY20iLAogICAgICAgZHBpID0gMzAwKQpgYGAKCiMjIyBHZW5lcmF0ZSBDb2VmZmljaWVudCBhbmQgVklQIHBsb3RzCmBgYHtyLCBmaWcuaGVpZ2h0ID0gOSwgZmlnLndpZHRoID0gMTAsIGVjaG89VFJVRX0KdmlwcyA8LSBzcGVjdHJhdHJhaXQ6OlZJUChwbHNyLm91dClbbkNvbXBzLF0KCnBhcihtZnJvdz1jKDIsMSkpCnBsb3QocGxzci5vdXQsIHBsb3R0eXBlID0gImNvZWYiLHhsYWI9IldhdmVsZW5ndGggKG5tKSIsCiAgICAgeWxhYj0iUmVncmVzc2lvbiBjb2VmZmljaWVudHMiLGxlZ2VuZHBvcyA9ICJib3R0b21yaWdodCIsCiAgICAgbmNvbXA9bkNvbXBzLGx3ZD0yKQpib3gobHdkPTIuMikKcGxvdChzZXEoU3RhcnQud2F2ZSxFbmQud2F2ZSwxKSx2aXBzLHhsYWI9IldhdmVsZW5ndGggKG5tKSIseWxhYj0iVklQIixjZXg9MC4wMSkKbGluZXMoc2VxKFN0YXJ0LndhdmUsRW5kLndhdmUsMSksdmlwcyxsd2Q9MykKYWJsaW5lKGg9MC44LGx0eT0yLGNvbD0iZGFyayBncmV5IikKYm94KGx3ZD0yLjIpCmRldi5jb3B5KHBuZyxmaWxlLnBhdGgob3V0ZGlyLHBhc3RlMChpblZhciwnX0NvZWZmaWNpZW50X1ZJUF9wbG90LnBuZycpKSwgCiAgICAgICAgIGhlaWdodD0zMTAwLCB3aWR0aD00MTAwLCByZXM9MzQwKQpkZXYub2ZmKCk7CnBhcihvcGFyKQpgYGAKCiMjIyBKYWNra25pZmUgdmFsaWRhdGlvbgpgYGB7ciwgZWNobz1UUlVFfQppZihncmVwbCgiV2luZG93cyIsIHNlc3Npb25JbmZvKCkkcnVubmluZykpewogIHBscy5vcHRpb25zKHBhcmFsbGVsID1OVUxMKQp9IGVsc2UgewogIHBscy5vcHRpb25zKHBhcmFsbGVsID0gcGFyYWxsZWw6OmRldGVjdENvcmVzKCktMSkKfQoKc2VnIDwtIDEwMApqay5wbHNyLm91dCA8LSBwbHM6OnBsc3IoYXMuZm9ybXVsYShwYXN0ZShpblZhciwifiIsIlNwZWN0cmEiKSksIHNjYWxlPUZBTFNFLCAKICAgICAgICAgICAgICAgICAgICAgICAgIGNlbnRlcj1UUlVFLCBuY29tcD1uQ29tcHMsIHZhbGlkYXRpb249IkNWIiwgCiAgICAgICAgICAgICAgICAgICAgICAgICBzZWdtZW50cyA9IHNlZywgc2VnbWVudC50eXBlPSJpbnRlcmxlYXZlZCIsIHRyYWNlPUZBTFNFLCAKICAgICAgICAgICAgICAgICAgICAgICAgIGphY2trbmlmZT1UUlVFLCBkYXRhPWNhbC5wbHNyLmRhdGEpCnBscy5vcHRpb25zKHBhcmFsbGVsID0gTlVMTCkKCkphY2trbmlmZV9jb2VmIDwtIGYuY29lZi52YWxpZChwbHNyLm91dCA9IGprLnBsc3Iub3V0LCBkYXRhX3Bsc3IgPSBjYWwucGxzci5kYXRhLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG5jb21wID0gbkNvbXBzLCBpblZhcj1pblZhcikKSmFja2tuaWZlX2ludGVyY2VwdCA8LSBKYWNra25pZmVfY29lZlsxLCwsXQpKYWNra25pZmVfY29lZiA8LSBKYWNra25pZmVfY29lZlsyOmRpbShKYWNra25pZmVfY29lZilbMV0sLCxdCgppbnRlcnZhbCA8LSBjKDAuMDI1LDAuOTc1KQpKYWNra25pZmVfUHJlZCA8LSB2YWwucGxzci5kYXRhJFNwZWN0cmEgJSolIEphY2trbmlmZV9jb2VmICsgCiAgbWF0cml4KHJlcChKYWNra25pZmVfaW50ZXJjZXB0LCBsZW5ndGgodmFsLnBsc3IuZGF0YVssaW5WYXJdKSksIGJ5cm93PVRSVUUsIAogICAgICAgICBuY29sPWxlbmd0aChKYWNra25pZmVfaW50ZXJjZXB0KSkKSW50ZXJ2YWxfQ29uZiA8LSBhcHBseShYID0gSmFja2tuaWZlX1ByZWQsIE1BUkdJTiA9IDEsIEZVTiA9IHF1YW50aWxlLCAKICAgICAgICAgICAgICAgICAgICAgICBwcm9icz1jKGludGVydmFsWzFdLCBpbnRlcnZhbFsyXSkpCnNkX21lYW4gPC0gYXBwbHkoWCA9IEphY2trbmlmZV9QcmVkLCBNQVJHSU4gPSAxLCBGVU4gPXNkKQpzZF9yZXMgPC0gc2QodmFsLnBsc3Iub3V0cHV0JFBMU1JfUmVzaWR1YWxzKQpzZF90b3QgPC0gc3FydChzZF9tZWFuXjIrc2RfcmVzXjIpCnZhbC5wbHNyLm91dHB1dCRMQ0kgPC0gSW50ZXJ2YWxfQ29uZlsxLF0KdmFsLnBsc3Iub3V0cHV0JFVDSSA8LSBJbnRlcnZhbF9Db25mWzIsXQp2YWwucGxzci5vdXRwdXQkTFBJIDwtIHZhbC5wbHNyLm91dHB1dCRQTFNSX1ByZWRpY3RlZC0xLjk2KnNkX3RvdAp2YWwucGxzci5vdXRwdXQkVVBJIDwtIHZhbC5wbHNyLm91dHB1dCRQTFNSX1ByZWRpY3RlZCsxLjk2KnNkX3RvdApoZWFkKHZhbC5wbHNyLm91dHB1dCkKYGBgCgojIyMgSmFja2tuaWZlIGNvZWZmaWNpZW50IHBsb3QKYGBge3IsIGZpZy5oZWlnaHQgPSA2LCBmaWcud2lkdGggPSAxMCwgZWNobz1UUlVFfQpzcGVjdHJhdHJhaXQ6OmYucGxvdC5jb2VmKFogPSB0KEphY2trbmlmZV9jb2VmKSwgd3YgPSB3diwgCiAgICAgICAgICAgIHBsb3RfbGFiZWw9IkphY2trbmlmZSByZWdyZXNzaW9uIGNvZWZmaWNpZW50cyIscG9zaXRpb24gPSAnYm90dG9tbGVmdCcpCmFibGluZShoPTAsbHR5PTIsY29sPSJncmV5NTAiKQpib3gobHdkPTIuMikKZGV2LmNvcHkocG5nLGZpbGUucGF0aChvdXRkaXIscGFzdGUwKGluVmFyLCdfSmFja2tuaWZlX1JlZ3Jlc3Npb25fQ29lZmZpY2llbnRzLnBuZycpKSwgCiAgICAgICAgIGhlaWdodD0yMTAwLCB3aWR0aD0zODAwLCByZXM9MzQwKQpkZXYub2ZmKCk7CmBgYAoKIyMjIEphY2trbmlmZSB2YWxpZGF0aW9uIHBsb3QKYGBge3IsIGZpZy5oZWlnaHQgPSA3LCBmaWcud2lkdGggPSA4LCBlY2hvPVRSVUV9CnJtc2VwX3BlcmNybXNlcCA8LSBzcGVjdHJhdHJhaXQ6OnBlcmNlbnRfcm1zZShwbHNyX2RhdGFzZXQgPSB2YWwucGxzci5vdXRwdXQsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaW5WYXIgPSBpblZhciwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXNpZHVhbHMgPSB2YWwucGxzci5vdXRwdXQkUExTUl9SZXNpZHVhbHMsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmFuZ2U9ImZ1bGwiKQpSTVNFUCA8LSBybXNlcF9wZXJjcm1zZXAkcm1zZQpwZXJjX1JNU0VQIDwtIHJtc2VwX3BlcmNybXNlcCRwZXJjX3Jtc2UKcjIgPC0gcm91bmQocGxzOjpSMihwbHNyLm91dCwgbmV3ZGF0YSA9IHZhbC5wbHNyLmRhdGEsIGludGVyY2VwdD1GKSR2YWxbbkNvbXBzXSwyKQpleHByIDwtIHZlY3RvcigiZXhwcmVzc2lvbiIsIDMpCmV4cHJbWzFdXSA8LSBicXVvdGUoUl4yPT0uKHIyKSkKZXhwcltbMl1dIDwtIGJxdW90ZShSTVNFUD09Lihyb3VuZChSTVNFUCwyKSkpCmV4cHJbWzNdXSA8LSBicXVvdGUoIiVSTVNFUCI9PS4ocm91bmQocGVyY19STVNFUCwyKSkpCnJuZ192YWxzIDwtIGMobWluKHZhbC5wbHNyLm91dHB1dCRMUEkpLCBtYXgodmFsLnBsc3Iub3V0cHV0JFVQSSkpCnBhcihtZnJvdz1jKDEsMSksIG1hcj1jKDQuMiw1LjMsMSwwLjQpLCBvbWE9YygwLCAwLjEsIDAsIDAuMikpCnBsb3RyaXg6OnBsb3RDSSh2YWwucGxzci5vdXRwdXQkUExTUl9QcmVkaWN0ZWQsdmFsLnBsc3Iub3V0cHV0WyxpblZhcl0sIAogICAgICAgbGk9dmFsLnBsc3Iub3V0cHV0JExQSSwgdWk9dmFsLnBsc3Iub3V0cHV0JFVQSSwgZ2FwPTAuMDA5LHNmcmFjPTAuMDA0LCAKICAgICAgIGx3ZD0xLjYsIHhsaW09YyhybmdfdmFsc1sxXSwgcm5nX3ZhbHNbMl0pLCB5bGltPWMocm5nX3ZhbHNbMV0sIHJuZ192YWxzWzJdKSwgCiAgICAgICBlcnI9IngiLCBwY2g9MjEsIGNvbD0iYmxhY2siLCBwdC5iZz1zY2FsZXM6OmFscGhhKCJncmV5NzAiLDAuNyksIHNjb2w9ImdyZXk1MCIsCiAgICAgICBjZXg9MiwgeGxhYj1wYXN0ZTAoIlByZWRpY3RlZCAiLCBwYXN0ZShpblZhciksICIgKHVuaXRzKSIpLAogICAgICAgeWxhYj1wYXN0ZTAoIk9ic2VydmVkICIsIHBhc3RlKGluVmFyKSwgIiAodW5pdHMpIiksCiAgICAgICBjZXguYXhpcz0xLjUsY2V4LmxhYj0xLjgpCmFibGluZSgwLDEsbHR5PTIsbHc9MikKbGVnZW5kKCJ0b3BsZWZ0IiwgbGVnZW5kPWV4cHIsIGJ0eT0ibiIsIGNleD0xLjUpCmJveChsd2Q9Mi4yKQpkZXYuY29weShwbmcsZmlsZS5wYXRoKG91dGRpcixwYXN0ZTAoaW5WYXIsIl9QTFNSX1ZhbGlkYXRpb25fU2NhdHRlcnBsb3QucG5nIikpLCAKICAgICAgICAgaGVpZ2h0PTI4MDAsIHdpZHRoPTMyMDAsICByZXM9MzQwKQpkZXYub2ZmKCk7CmBgYAoKIyMjIE91dHB1dCBqYWNra25pZmUgcmVzdWx0cwpgYGB7ciwgZWNobz1UUlVFfQpvdXQuamsuY29lZnMgPC0gZGF0YS5mcmFtZShJdGVyYXRpb249c2VxKDEsc2VnLDEpLAogICAgICAgICAgICAgICAgICAgICAgICAgICBJbnRlcmNlcHQ9SmFja2tuaWZlX2ludGVyY2VwdCx0KEphY2trbmlmZV9jb2VmKSkKaGVhZChvdXQuamsuY29lZnMpWzE6Nl0Kd3JpdGUuY3N2KG91dC5qay5jb2VmcyxmaWxlPWZpbGUucGF0aChvdXRkaXIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcGFzdGUwKGluVmFyLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAnX0phY2traWZlX1BMU1JfQ29lZmZpY2llbnRzLmNzdicpKSwKICAgICAgICAgIHJvdy5uYW1lcz1GQUxTRSkKYGBgCgojIyMgQ3JlYXRlIGNvcmUgUExTUiBvdXRwdXRzCmBgYHtyLCBlY2hvPVRSVUV9CnByaW50KHBhc3RlKCJPdXRwdXQgZGlyZWN0b3J5OiAiLCBnZXR3ZCgpKSkKCiMgT2JzZXJ2ZWQgdmVyc3VzIHByZWRpY3RlZAp3cml0ZS5jc3YoY2FsLnBsc3Iub3V0cHV0LGZpbGU9ZmlsZS5wYXRoKG91dGRpciwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwYXN0ZTAoaW5WYXIsJ19PYnNlcnZlZF9QTFNSX0NWX1ByZWRfJywKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbkNvbXBzLCdjb21wLmNzdicpKSwKICAgICAgICAgIHJvdy5uYW1lcz1GQUxTRSkKCiMgVmFsaWRhdGlvbiBkYXRhCndyaXRlLmNzdih2YWwucGxzci5vdXRwdXQsZmlsZT1maWxlLnBhdGgob3V0ZGlyLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBhc3RlMChpblZhciwnX1ZhbGlkYXRpb25fUExTUl9QcmVkXycsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG5Db21wcywnY29tcC5jc3YnKSksCiAgICAgICAgICByb3cubmFtZXM9RkFMU0UpCgojIE1vZGVsIGNvZWZmaWNpZW50cwpjb2VmcyA8LSBjb2VmKHBsc3Iub3V0LG5jb21wPW5Db21wcyxpbnRlcmNlcHQ9VFJVRSkKd3JpdGUuY3N2KGNvZWZzLGZpbGU9ZmlsZS5wYXRoKG91dGRpciwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBhc3RlMChpblZhciwnX1BMU1JfQ29lZmZpY2llbnRzXycsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbkNvbXBzLCdjb21wLmNzdicpKSwKICAgICAgICAgIHJvdy5uYW1lcz1UUlVFKQoKIyBQTFNSIFZJUAp3cml0ZS5jc3YodmlwcyxmaWxlPWZpbGUucGF0aChvdXRkaXIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBhc3RlMChpblZhciwnX1BMU1JfVklQc18nLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbkNvbXBzLCdjb21wLmNzdicpKSkKYGBgCgojIyMgQ29uZmlybSBmaWxlcyB3ZXJlIHdyaXR0ZW4gdG8gdGVtcCBzcGFjZQpgYGB7ciwgZWNobz1UUlVFfQpwcmludCgiKioqKiBQTFNSIG91dHB1dCBmaWxlczogIikKcHJpbnQobGlzdC5maWxlcyhvdXRkaXIpW2dyZXAocGF0dGVybiA9IGluVmFyLCBsaXN0LmZpbGVzKG91dGRpcikpXSkKYGBgCg==