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 leaf nitrogen content (Narea, g/m2)

Getting Started

Load libraries

list.of.packages <- c("pls","dplyr","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 <- "Narea_g_m2"

# What is the source dataset from EcoSIS?
ecosis_id <- "9db4c5a2-7eac-4e1e-8859-009233648e89"

# 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/RtmpdFG9hz 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/RtmpdFG9hz"

Grab data from EcoSIS

print(paste0("Output directory: ",getwd()))  # check wd
[1] "Output directory: /Users/sserbin/Data/GitHub/spectratrait/vignettes"
dat_raw <- spectratrait::get_ecosis_data(ecosis_id = ecosis_id)
[1] "**** Downloading Ecosis data ****"
Downloading data...
Rows: 256 Columns: 2164── Column specification ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Delimiter: ","
chr    (4): Latin Species, ids, plot code, species code
dbl (2160): Cw/EWT (cm3/cm2), Leaf area (mm2), Leaf calcium content per leaf area (mg/mm2), Leaf magnesium content per leaf area (mg/mm2), Leaf ...
ℹ 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] "Cw/EWT (cm3/cm2)"                               "Latin Species"                                 
 [3] "Leaf area (mm2)"                                "Leaf calcium content per leaf area (mg/mm2)"   
 [5] "Leaf magnesium content per leaf area (mg/mm2)"  "Leaf mass per area (g/cm2)"                    
 [7] "Leaf nitrogen content per leaf area (mg/mm2)"   "Leaf phosphorus content per leaf area (mg/mm2)"
 [9] "Leaf potassium content per leaf area (mg/mm2)"  "Plant height vegetative (cm)"                  
[11] "ids"                                            "plot code"                                     
[13] "species code"                                   "350"                                           
[15] "351"                                            "352"                                           
[17] "353"                                            "354"                                           
[19] "355"                                            "356"                                           
[21] "357"                                            "358"                                           
[23] "359"                                            "360"                                           
[25] "361"                                            "362"                                           
[27] "363"                                            "364"                                           
[29] "365"                                            "366"                                           
[31] "367"                                            "368"                                           
[33] "369"                                            "370"                                           
[35] "371"                                            "372"                                           
[37] "373"                                            "374"                                           
[39] "375"                                            "376"                                           

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=`Latin Species`,Species_Code=`species code`,Plot=`plot code`,
         Narea_mg_mm2=`Leaf nitrogen content per leaf area (mg/mm2)`)
sample_info2 <- sample_info2 %>%
#  mutate(Narea_g_m2=Narea_mg_mm2*(0.001/1e-6)) # based on orig units should be this but conversion wrong
  mutate(Narea_g_m2=Narea_mg_mm2*100) # this assumes orig units were g/mm2 or mg/cm2
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))]),]

Create cal/val datasets

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

method <- "dplyr" #base/dplyr
# base R - a bit slow
# dplyr - much faster
split_data <- spectratrait::create_data_split(dataset=plsr_data, approach=method, split_seed=1245565, 
                                              prop=0.8, group_variables="Species_Code")
names(split_data)
[1] "cal_data" "val_data"
cal.plsr.data <- split_data$cal_data
head(cal.plsr.data)[1:8]
val.plsr.data <- split_data$val_data
head(val.plsr.data)[1:8]
rm(split_data)

# Datasets:
print(paste("Cal observations: ",dim(cal.plsr.data)[1],sep=""))
[1] "Cal observations: 183"
print(paste("Val observations: ",dim(val.plsr.data)[1],sep=""))
[1] "Val observations: 73"
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 <- 1245565
seg <- 50
maxComps <- 16
iterations <- 80
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

plsr.out <- plsr(as.formula(paste(inVar,"~","Spectra")),scale=FALSE,ncomp=nComps,validation="LOO",
                 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  
     0.5594       0.6034       0.5448       0.3842       0.3481       0.3027       0.2429       0.2268       0.2852       0.2818       0.2780  
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.007544    -0.172296     0.044153     0.524579     0.609920     0.704963     0.809962     0.834383     0.738093     0.744325     0.751224  
plot(pls::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 

Jackknife validation

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

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

Jackknife_coef <- spectratrait::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)
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,length(Jackknife_intercept),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: ", outdir))
[1] "Output directory:  /var/folders/xp/h3k9vf3n2jx181ts786_yjrn9c2gjq/T//RtmpdFG9hz"
# 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] "Narea_g_m2_Cal_PLSR_Dataset.csv"                  "Narea_g_m2_Cal_Val_Histograms.png"               
 [3] "Narea_g_m2_Cal_Val_Scatterplots.png"              "Narea_g_m2_Cal_Val_Spectra.png"                  
 [5] "Narea_g_m2_Coefficient_VIP_plot.png"              "Narea_g_m2_Jackkife_PLSR_Coefficients.csv"       
 [7] "Narea_g_m2_Jackknife_Regression_Coefficients.png" "Narea_g_m2_Observed_PLSR_CV_Pred_10comp.csv"     
 [9] "Narea_g_m2_PLSR_Coefficients_10comp.csv"          "Narea_g_m2_PLSR_Component_Selection.png"         
[11] "Narea_g_m2_PLSR_Validation_Scatterplot.png"       "Narea_g_m2_PLSR_VIPs_10comp.csv"                 
[13] "Narea_g_m2_Val_PLSR_Dataset.csv"                  "Narea_g_m2_Validation_PLSR_Pred_10comp.csv"      
[15] "Narea_g_m2_Validation_RMSEP_R2_by_Component.png" 
LS0tCnRpdGxlOiBTcGVjdHJhLXRyYWl0IFBMU1IgZXhhbXBsZSB1c2luZyBsZWFmLWxldmVsIHNwZWN0cmEgYW5kIGxlYWYgbml0cm9nZW4gY29udGVudCAoTmFyZWEsIGcvbTIpIGRhdGEgZnJvbSAzNiBzcGVjaWVzIGdyb3dpbmcgaW4gUm9zYSBydWdvc2EgaW52YWRlZCBjb2FzdGFsIGdyYXNzbGFuZCBjb21tdW5pdGllcyBpbiBCZWxnaXVtCmF1dGhvcjogIlNoYXduIFAuIFNlcmJpbiwgSnVsaWVuIExhbW91ciwgJiBKZXJlbWlhaCBBbmRlcnNvbiIKZGF0ZTogImByIFN5cy5EYXRlKClgIgpvdXRwdXQ6CiAgaHRtbF9ub3RlYm9vazogZGVmYXVsdAogIGdpdGh1Yl9kb2N1bWVudDogZGVmYXVsdAogIGh0bWxfZG9jdW1lbnQ6CiAgICBkZl9wcmludDogcGFnZWQKICBybWFya2Rvd246IGh0bWxfdmlnbmV0dGUKICBwZGZfZG9jdW1lbnQ6IGRlZmF1bHQKdmlnbmV0dGU6ID4KICAlXFZpZ25ldHRlSW5kZXhFbnRyeXtTcGVjdHJhLXRyYWl0IFBMU1IgZXhhbXBsZSB1c2luZyBsZWFmLWxldmVsIHNwZWN0cmEgYW5kIGxlYWYgbml0cm9nZW4gY29udGVudCAoTmFyZWEsIGcvbTIpIGRhdGEgZnJvbSAzNiBzcGVjaWVzIGdyb3dpbmcgaW4gUm9zYSBydWdvc2EgaW52YWRlZCBjb2FzdGFsIGdyYXNzbGFuZCBjb21tdW5pdGllcyBpbiBCZWxnaXVtfQogICVcdXNlcGFja2FnZVt1dGY4XXtpbnB1dGVuY30KICAlXFZpZ25ldHRlRW5naW5le2tuaXRyOjprbml0cn0KLS0tCgpgYGB7ciBzZXR1cCwgaW5jbHVkZT1GQUxTRSwgZWNobz1GQUxTRX0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFKQpgYGAKCiMjIyBPdmVydmlldwpUaGlzIGlzIGFuIFtSIE1hcmtkb3duXShodHRwOi8vcm1hcmtkb3duLnJzdHVkaW8uY29tKSBOb3RlYm9vayB0byBpbGx1c3RyYXRlIGhvdyB0byByZXRyaWV2ZSBhIGRhdGFzZXQgZnJvbSB0aGUgRWNvU0lTIHNwZWN0cmFsIGRhdGFiYXNlLCBjaG9vc2UgdGhlICJvcHRpbWFsIiBudW1iZXIgb2YgcGxzciBjb21wb25lbnRzLCBhbmQgZml0IGEgcGxzciBtb2RlbCBmb3IgbGVhZiBuaXRyb2dlbiBjb250ZW50IChOYXJlYSwgZy9tMikKCiMjIyBHZXR0aW5nIFN0YXJ0ZWQKIyMjIExvYWQgbGlicmFyaWVzCmBgYHtyLCBldmFsPVRSVUUsIGVjaG89VFJVRX0KbGlzdC5vZi5wYWNrYWdlcyA8LSBjKCJwbHMiLCJkcGx5ciIsImhlcmUiLCJwbG90cml4IiwiZ2dwbG90MiIsImdyaWRFeHRyYSIsInNwZWN0cmF0cmFpdCIpCmludmlzaWJsZShsYXBwbHkobGlzdC5vZi5wYWNrYWdlcywgbGlicmFyeSwgY2hhcmFjdGVyLm9ubHkgPSBUUlVFKSkKYGBgCgojIyMgU2V0dXAgb3RoZXIgZnVuY3Rpb25zIGFuZCBvcHRpb25zCmBgYHtyLCBlY2hvPVRSVUV9CiMjIyBTZXR1cCBvcHRpb25zCgojIFNjcmlwdCBvcHRpb25zCnBsczo6cGxzLm9wdGlvbnMocGxzcmFsZyA9ICJvc2NvcmVzcGxzIikKcGxzOjpwbHMub3B0aW9ucygicGxzcmFsZyIpCgojIERlZmF1bHQgcGFyIG9wdGlvbnMKb3BhciA8LSBwYXIobm8ucmVhZG9ubHkgPSBUKQoKIyBXaGF0IGlzIHRoZSB0YXJnZXQgdmFyaWFibGU/CmluVmFyIDwtICJOYXJlYV9nX20yIgoKIyBXaGF0IGlzIHRoZSBzb3VyY2UgZGF0YXNldCBmcm9tIEVjb1NJUz8KZWNvc2lzX2lkIDwtICI5ZGI0YzVhMi03ZWFjLTRlMWUtODg1OS0wMDkyMzM2NDhlODkiCgojIFNwZWNpZnkgb3V0cHV0IGRpcmVjdG9yeSwgb3V0cHV0X2RpciAKIyBPcHRpb25zOiAKIyB0ZW1wZGlyIC0gdXNlIGEgT1Mtc3BlY2lmaWVkIHRlbXBvcmFyeSBkaXJlY3RvcnkgCiMgdXNlciBkZWZpbmVkIFBBVEggLSBlLmcuICJ+L3NjcmF0Y2gvUExTUiIKb3V0cHV0X2RpciA8LSAidGVtcGRpciIKYGBgCgojIyMgU2V0IHdvcmtpbmcgZGlyZWN0b3J5IChzY3JhdGNoIHNwYWNlKQpgYGB7ciwgZWNobz1GQUxTRX0KaWYgKG91dHB1dF9kaXI9PSJ0ZW1wZGlyIikgewogIG91dGRpciA8LSB0ZW1wZGlyKCkKfSBlbHNlIHsKICBpZiAoISBmaWxlLmV4aXN0cyhvdXRwdXRfZGlyKSkgZGlyLmNyZWF0ZShvdXRwdXRfZGlyLHJlY3Vyc2l2ZT1UUlVFKQogIG91dGRpciA8LSBmaWxlLnBhdGgocGF0aC5leHBhbmQob3V0cHV0X2RpcikpCn0Kc2V0d2Qob3V0ZGlyKSAjIHNldCB3b3JraW5nIGRpcmVjdG9yeQpnZXR3ZCgpICAjIGNoZWNrIHdkCmBgYAoKIyMjIEdyYWIgZGF0YSBmcm9tIEVjb1NJUwpgYGB7ciwgZWNobz1UUlVFfQpwcmludChwYXN0ZTAoIk91dHB1dCBkaXJlY3Rvcnk6ICIsZ2V0d2QoKSkpICAjIGNoZWNrIHdkCmRhdF9yYXcgPC0gc3BlY3RyYXRyYWl0OjpnZXRfZWNvc2lzX2RhdGEoZWNvc2lzX2lkID0gZWNvc2lzX2lkKQpoZWFkKGRhdF9yYXcpCm5hbWVzKGRhdF9yYXcpWzE6NDBdCmBgYAoKIyMjIENyZWF0ZSBmdWxsIHBsc3IgZGF0YXNldApgYGB7ciwgZWNobz1UUlVFfQojIyMgQ3JlYXRlIHBsc3IgZGF0YXNldApTdGFydC53YXZlIDwtIDUwMApFbmQud2F2ZSA8LSAyNDAwCnd2IDwtIHNlcShTdGFydC53YXZlLEVuZC53YXZlLDEpClNwZWN0cmEgPC0gYXMubWF0cml4KGRhdF9yYXdbLG5hbWVzKGRhdF9yYXcpICVpbiUgd3ZdKQpjb2xuYW1lcyhTcGVjdHJhKSA8LSBjKHBhc3RlMCgiV2F2ZV8iLHd2KSkKc2FtcGxlX2luZm8gPC0gZGF0X3Jhd1ssbmFtZXMoZGF0X3JhdykgJW5vdGluJSBzZXEoMzUwLDI1MDAsMSldCmhlYWQoc2FtcGxlX2luZm8pCgpzYW1wbGVfaW5mbzIgPC0gc2FtcGxlX2luZm8gJT4lCiAgc2VsZWN0KFBsYW50X1NwZWNpZXM9YExhdGluIFNwZWNpZXNgLFNwZWNpZXNfQ29kZT1gc3BlY2llcyBjb2RlYCxQbG90PWBwbG90IGNvZGVgLAogICAgICAgICBOYXJlYV9tZ19tbTI9YExlYWYgbml0cm9nZW4gY29udGVudCBwZXIgbGVhZiBhcmVhIChtZy9tbTIpYCkKc2FtcGxlX2luZm8yIDwtIHNhbXBsZV9pbmZvMiAlPiUKIyAgbXV0YXRlKE5hcmVhX2dfbTI9TmFyZWFfbWdfbW0yKigwLjAwMS8xZS02KSkgIyBiYXNlZCBvbiBvcmlnIHVuaXRzIHNob3VsZCBiZSB0aGlzIGJ1dCBjb252ZXJzaW9uIHdyb25nCiAgbXV0YXRlKE5hcmVhX2dfbTI9TmFyZWFfbWdfbW0yKjEwMCkgIyB0aGlzIGFzc3VtZXMgb3JpZyB1bml0cyB3ZXJlIGcvbW0yIG9yIG1nL2NtMgpoZWFkKHNhbXBsZV9pbmZvMikKCnBsc3JfZGF0YSA8LSBkYXRhLmZyYW1lKHNhbXBsZV9pbmZvMixTcGVjdHJhKQpybShzYW1wbGVfaW5mbyxzYW1wbGVfaW5mbzIsU3BlY3RyYSkKYGBgCgojIyMjIEV4YW1wbGUgZGF0YSBjbGVhbmluZy4gCmBgYHtyLCBlY2hvPVRSVUV9CiMjIyMgRW5kIHVzZXIgbmVlZHMgdG8gZG8gd2hhdCdzIGFwcHJvcHJpYXRlIGZvciB0aGVpciBkYXRhLiAgVGhpcyBtYXkgYmUgYW4gaXRlcmF0aXZlIHByb2Nlc3MuCiMgS2VlcCBvbmx5IGNvbXBsZXRlIHJvd3Mgb2YgaW5WYXIgYW5kIHNwZWMgZGF0YSBiZWZvcmUgZml0dGluZwpwbHNyX2RhdGEgPC0gcGxzcl9kYXRhW2NvbXBsZXRlLmNhc2VzKHBsc3JfZGF0YVssbmFtZXMocGxzcl9kYXRhKSAlaW4lIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGMoaW5WYXIscGFzdGUwKCJXYXZlXyIsd3YpKV0pLF0KYGBgCgojIyMgQ3JlYXRlIGNhbC92YWwgZGF0YXNldHMKYGBge3IsIGZpZy5oZWlnaHQgPSA1LCBmaWcud2lkdGggPSAxMiwgZWNobz1UUlVFfQojIyMgQ3JlYXRlIGNhbC92YWwgZGF0YXNldHMKIyMgTWFrZSBhIHN0cmF0aWZpZWQgcmFuZG9tIHNhbXBsaW5nIGluIHRoZSBzdHJhdGEgVVNEQV9TcGVjaWVzX0NvZGUgYW5kIERvbWFpbgoKbWV0aG9kIDwtICJkcGx5ciIgI2Jhc2UvZHBseXIKIyBiYXNlIFIgLSBhIGJpdCBzbG93CiMgZHBseXIgLSBtdWNoIGZhc3RlcgpzcGxpdF9kYXRhIDwtIHNwZWN0cmF0cmFpdDo6Y3JlYXRlX2RhdGFfc3BsaXQoZGF0YXNldD1wbHNyX2RhdGEsIGFwcHJvYWNoPW1ldGhvZCwgc3BsaXRfc2VlZD0xMjQ1NTY1LCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHByb3A9MC44LCBncm91cF92YXJpYWJsZXM9IlNwZWNpZXNfQ29kZSIpCm5hbWVzKHNwbGl0X2RhdGEpCmNhbC5wbHNyLmRhdGEgPC0gc3BsaXRfZGF0YSRjYWxfZGF0YQpoZWFkKGNhbC5wbHNyLmRhdGEpWzE6OF0KdmFsLnBsc3IuZGF0YSA8LSBzcGxpdF9kYXRhJHZhbF9kYXRhCmhlYWQodmFsLnBsc3IuZGF0YSlbMTo4XQpybShzcGxpdF9kYXRhKQoKIyBEYXRhc2V0czoKcHJpbnQocGFzdGUoIkNhbCBvYnNlcnZhdGlvbnM6ICIsZGltKGNhbC5wbHNyLmRhdGEpWzFdLHNlcD0iIikpCnByaW50KHBhc3RlKCJWYWwgb2JzZXJ2YXRpb25zOiAiLGRpbSh2YWwucGxzci5kYXRhKVsxXSxzZXA9IiIpKQoKY2FsX2hpc3RfcGxvdCA8LSBxcGxvdChjYWwucGxzci5kYXRhWyxwYXN0ZTAoaW5WYXIpXSxnZW9tPSJoaXN0b2dyYW0iLAogICAgICAgICAgICAgICAgICAgICAgIG1haW4gPSBwYXN0ZTAoIkNhbC4gSGlzdG9ncmFtIGZvciAiLGluVmFyKSwKICAgICAgICAgICAgICAgICAgICAgICB4bGFiID0gcGFzdGUwKGluVmFyKSx5bGFiID0gIkNvdW50IixmaWxsPUkoImdyZXk1MCIpLGNvbD1JKCJibGFjayIpLAogICAgICAgICAgICAgICAgICAgICAgIGFscGhhPUkoLjcpKQp2YWxfaGlzdF9wbG90IDwtIHFwbG90KHZhbC5wbHNyLmRhdGFbLHBhc3RlMChpblZhcildLGdlb209Imhpc3RvZ3JhbSIsCiAgICAgICAgICAgICAgICAgICAgICAgbWFpbiA9IHBhc3RlMCgiVmFsLiBIaXN0b2dyYW0gZm9yICIsaW5WYXIpLAogICAgICAgICAgICAgICAgICAgICAgIHhsYWIgPSBwYXN0ZTAoaW5WYXIpLHlsYWIgPSAiQ291bnQiLGZpbGw9SSgiZ3JleTUwIiksY29sPUkoImJsYWNrIiksCiAgICAgICAgICAgICAgICAgICAgICAgYWxwaGE9SSguNykpCmhpc3RvZ3JhbXMgPC0gZ3JpZC5hcnJhbmdlKGNhbF9oaXN0X3Bsb3QsIHZhbF9oaXN0X3Bsb3QsIG5jb2w9MikKZ2dzYXZlKGZpbGVuYW1lID0gZmlsZS5wYXRoKG91dGRpcixwYXN0ZTAoaW5WYXIsIl9DYWxfVmFsX0hpc3RvZ3JhbXMucG5nIikpLCBwbG90ID0gaGlzdG9ncmFtcywgCiAgICAgICBkZXZpY2U9InBuZyIsIHdpZHRoID0gMzAsIAogICAgICAgaGVpZ2h0ID0gMTIsIHVuaXRzID0gImNtIiwKICAgICAgIGRwaSA9IDMwMCkKIyBvdXRwdXQgY2FsL3ZhbCBkYXRhCndyaXRlLmNzdihjYWwucGxzci5kYXRhLGZpbGU9ZmlsZS5wYXRoKG91dGRpcixwYXN0ZTAoaW5WYXIsJ19DYWxfUExTUl9EYXRhc2V0LmNzdicpKSwKICAgICAgICAgIHJvdy5uYW1lcz1GQUxTRSkKd3JpdGUuY3N2KHZhbC5wbHNyLmRhdGEsZmlsZT1maWxlLnBhdGgob3V0ZGlyLHBhc3RlMChpblZhciwnX1ZhbF9QTFNSX0RhdGFzZXQuY3N2JykpLAogICAgICAgICAgcm93Lm5hbWVzPUZBTFNFKQpgYGAKCiMjIyBDcmVhdGUgY2FsaWJyYXRpb24gYW5kIHZhbGlkYXRpb24gUExTUiBkYXRhc2V0cwpgYGB7ciwgZWNobz1UUlVFfQojIyMgRm9ybWF0IFBMU1IgZGF0YSBmb3IgbW9kZWwgZml0dGluZyAKY2FsX3NwZWMgPC0gYXMubWF0cml4KGNhbC5wbHNyLmRhdGFbLCB3aGljaChuYW1lcyhjYWwucGxzci5kYXRhKSAlaW4lIHBhc3RlMCgiV2F2ZV8iLHd2KSldKQpjYWwucGxzci5kYXRhIDwtIGRhdGEuZnJhbWUoY2FsLnBsc3IuZGF0YVssIHdoaWNoKG5hbWVzKGNhbC5wbHNyLmRhdGEpICVub3RpbiUgcGFzdGUwKCJXYXZlXyIsd3YpKV0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBTcGVjdHJhPUkoY2FsX3NwZWMpKQpoZWFkKGNhbC5wbHNyLmRhdGEpWzE6NV0KCnZhbF9zcGVjIDwtIGFzLm1hdHJpeCh2YWwucGxzci5kYXRhWywgd2hpY2gobmFtZXModmFsLnBsc3IuZGF0YSkgJWluJSBwYXN0ZTAoIldhdmVfIix3dikpXSkKdmFsLnBsc3IuZGF0YSA8LSBkYXRhLmZyYW1lKHZhbC5wbHNyLmRhdGFbLCB3aGljaChuYW1lcyh2YWwucGxzci5kYXRhKSAlbm90aW4lIHBhc3RlMCgiV2F2ZV8iLHd2KSldLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgU3BlY3RyYT1JKHZhbF9zcGVjKSkKaGVhZCh2YWwucGxzci5kYXRhKVsxOjVdCmBgYAoKIyMjIHBsb3QgY2FsIGFuZCB2YWwgc3BlY3RyYQpgYGB7ciwgZmlnLmhlaWdodCA9IDUsIGZpZy53aWR0aCA9IDEyLCBlY2hvPVRSVUV9CnBhcihtZnJvdz1jKDEsMikpICMgQiwgTCwgVCwgUgpzcGVjdHJhdHJhaXQ6OmYucGxvdC5zcGVjKFo9Y2FsLnBsc3IuZGF0YSRTcGVjdHJhLHd2PXd2LHBsb3RfbGFiZWw9IkNhbGlicmF0aW9uIikKc3BlY3RyYXRyYWl0OjpmLnBsb3Quc3BlYyhaPXZhbC5wbHNyLmRhdGEkU3BlY3RyYSx3dj13dixwbG90X2xhYmVsPSJWYWxpZGF0aW9uIikKCmRldi5jb3B5KHBuZyxmaWxlLnBhdGgob3V0ZGlyLHBhc3RlMChpblZhciwnX0NhbF9WYWxfU3BlY3RyYS5wbmcnKSksIAogICAgICAgICBoZWlnaHQ9MjUwMCx3aWR0aD00OTAwLCByZXM9MzQwKQpkZXYub2ZmKCk7CnBhcihtZnJvdz1jKDEsMSkpCmBgYAoKIyMjIFVzZSBKYWNra25pZmUgcGVybXV0YXRpb24gdG8gZGV0ZXJtaW5lIG9wdGltYWwgbnVtYmVyIG9mIGNvbXBvbmVudHMKYGBge3IsIGZpZy5oZWlnaHQgPSA2LCBmaWcud2lkdGggPSAxMCwgZWNobz1UUlVFfQojIyMgVXNlIHBlcm11dGF0aW9uIHRvIGRldGVybWluZSB0aGUgb3B0aW1hbCBudW1iZXIgb2YgY29tcG9uZW50cwppZihncmVwbCgiV2luZG93cyIsIHNlc3Npb25JbmZvKCkkcnVubmluZykpewogIHBscy5vcHRpb25zKHBhcmFsbGVsID0gTlVMTCkKfSBlbHNlIHsKICBwbHMub3B0aW9ucyhwYXJhbGxlbCA9IHBhcmFsbGVsOjpkZXRlY3RDb3JlcygpLTEpCn0KCm1ldGhvZCA8LSAicGxzIiAjcGxzLCBmaXJzdFBsYXRlYXUsIGZpcnN0TWluCnJhbmRvbV9zZWVkIDwtIDEyNDU1NjUKc2VnIDwtIDUwCm1heENvbXBzIDwtIDE2Cml0ZXJhdGlvbnMgPC0gODAKcHJvcCA8LSAwLjcwCmlmIChtZXRob2Q9PSJwbHMiKSB7CiAgIyBwbHMgcGFja2FnZSBhcHByb2FjaCAtIGZhc3RlciBidXQgZXN0aW1hdGVzIG1vcmUgY29tcG9uZW50cy4uLi4KICBuQ29tcHMgPC0gc3BlY3RyYXRyYWl0OjpmaW5kX29wdGltYWxfY29tcG9uZW50cyhkYXRhc2V0PWNhbC5wbHNyLmRhdGEsIHRhcmdldFZhcmlhYmxlPWluVmFyLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtZXRob2Q9bWV0aG9kLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtYXhDb21wcz1tYXhDb21wcywgc2VnPXNlZywgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmFuZG9tX3NlZWQ9cmFuZG9tX3NlZWQpCiAgcHJpbnQocGFzdGUwKCIqKiogT3B0aW1hbCBudW1iZXIgb2YgY29tcG9uZW50czogIiwgbkNvbXBzKSkKfSBlbHNlIHsKICBuQ29tcHMgPC0gc3BlY3RyYXRyYWl0OjpmaW5kX29wdGltYWxfY29tcG9uZW50cyhkYXRhc2V0PWNhbC5wbHNyLmRhdGEsIHRhcmdldFZhcmlhYmxlPWluVmFyLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1ldGhvZD1tZXRob2QsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1heENvbXBzPW1heENvbXBzLCBpdGVyYXRpb25zPWl0ZXJhdGlvbnMsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNlZz1zZWcsIHByb3A9cHJvcCwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmFuZG9tX3NlZWQ9cmFuZG9tX3NlZWQpCn0KZGV2LmNvcHkocG5nLGZpbGUucGF0aChvdXRkaXIscGFzdGUwKHBhc3RlMChpblZhciwiX1BMU1JfQ29tcG9uZW50X1NlbGVjdGlvbi5wbmciKSkpLCAKICAgICAgICAgaGVpZ2h0PTI4MDAsIHdpZHRoPTM0MDAsICByZXM9MzQwKQpkZXYub2ZmKCk7CmBgYAoKIyMjIEZpdCBmaW5hbCBtb2RlbApgYGB7ciwgZmlnLmhlaWdodCA9IDUsIGZpZy53aWR0aCA9IDEyLCBlY2hvPVRSVUV9CnBsc3Iub3V0IDwtIHBsc3IoYXMuZm9ybXVsYShwYXN0ZShpblZhciwifiIsIlNwZWN0cmEiKSksc2NhbGU9RkFMU0UsbmNvbXA9bkNvbXBzLHZhbGlkYXRpb249IkxPTyIsCiAgICAgICAgICAgICAgICAgdHJhY2U9RkFMU0UsZGF0YT1jYWwucGxzci5kYXRhKQpmaXQgPC0gcGxzci5vdXQkZml0dGVkLnZhbHVlc1ssMSxuQ29tcHNdCnBscy5vcHRpb25zKHBhcmFsbGVsID0gTlVMTCkKCiMgRXh0ZXJuYWwgdmFsaWRhdGlvbiBmaXQgc3RhdHMKcGFyKG1mcm93PWMoMSwyKSkgIyBCLCBMLCBULCBSCnBsczo6Uk1TRVAocGxzci5vdXQsIG5ld2RhdGEgPSB2YWwucGxzci5kYXRhKQpwbG90KHBsczo6Uk1TRVAocGxzci5vdXQsZXN0aW1hdGU9YygidGVzdCIpLG5ld2RhdGEgPSB2YWwucGxzci5kYXRhKSwgbWFpbj0iTU9ERUwgUk1TRVAiLAogICAgIHhsYWI9Ik51bWJlciBvZiBDb21wb25lbnRzIix5bGFiPSJNb2RlbCBWYWxpZGF0aW9uIFJNU0VQIixsdHk9MSxjb2w9ImJsYWNrIixjZXg9MS41LGx3ZD0yKQpib3gobHdkPTIuMikKCnBsczo6UjIocGxzci5vdXQsIG5ld2RhdGEgPSB2YWwucGxzci5kYXRhKQpwbG90KHBsczo6UjIocGxzci5vdXQsZXN0aW1hdGU9YygidGVzdCIpLG5ld2RhdGEgPSB2YWwucGxzci5kYXRhKSwgbWFpbj0iTU9ERUwgUjIiLAogICAgIHhsYWI9Ik51bWJlciBvZiBDb21wb25lbnRzIix5bGFiPSJNb2RlbCBWYWxpZGF0aW9uIFIyIixsdHk9MSxjb2w9ImJsYWNrIixjZXg9MS41LGx3ZD0yKQpib3gobHdkPTIuMikKZGV2LmNvcHkocG5nLGZpbGUucGF0aChvdXRkaXIscGFzdGUwKHBhc3RlMChpblZhciwiX1ZhbGlkYXRpb25fUk1TRVBfUjJfYnlfQ29tcG9uZW50LnBuZyIpKSksIAogICAgICAgICBoZWlnaHQ9MjgwMCwgd2lkdGg9NDgwMCwgIHJlcz0zNDApCmRldi5vZmYoKTsKcGFyKG9wYXIpCmBgYAoKIyMjIFBMU1IgZml0IG9ic2VydmVkIHZzLiBwcmVkaWN0ZWQgcGxvdCBkYXRhCmBgYHtyLCBmaWcuaGVpZ2h0ID0gMTUsIGZpZy53aWR0aCA9IDE1LCBlY2hvPVRSVUV9ICAKI2NhbGlicmF0aW9uCmNhbC5wbHNyLm91dHB1dCA8LSBkYXRhLmZyYW1lKGNhbC5wbHNyLmRhdGFbLCB3aGljaChuYW1lcyhjYWwucGxzci5kYXRhKSAlbm90aW4lICJTcGVjdHJhIildLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBQTFNSX1ByZWRpY3RlZD1maXQsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIFBMU1JfQ1ZfUHJlZGljdGVkPWFzLnZlY3RvcihwbHNyLm91dCR2YWxpZGF0aW9uJHByZWRbLCxuQ29tcHNdKSkKY2FsLnBsc3Iub3V0cHV0IDwtIGNhbC5wbHNyLm91dHB1dCAlPiUKICBtdXRhdGUoUExTUl9DVl9SZXNpZHVhbHMgPSBQTFNSX0NWX1ByZWRpY3RlZC1nZXQoaW5WYXIpKQpoZWFkKGNhbC5wbHNyLm91dHB1dCkKY2FsLlIyIDwtIHJvdW5kKHBsczo6UjIocGxzci5vdXQsaW50ZXJjZXB0PUYpW1sxXV1bbkNvbXBzXSwyKQpjYWwuUk1TRVAgPC0gcm91bmQoc3FydChtZWFuKGNhbC5wbHNyLm91dHB1dCRQTFNSX0NWX1Jlc2lkdWFsc14yKSksMikKCnZhbC5wbHNyLm91dHB1dCA8LSBkYXRhLmZyYW1lKHZhbC5wbHNyLmRhdGFbLCB3aGljaChuYW1lcyh2YWwucGxzci5kYXRhKSAlbm90aW4lICJTcGVjdHJhIildLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBQTFNSX1ByZWRpY3RlZD1hcy52ZWN0b3IocHJlZGljdChwbHNyLm91dCwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG5ld2RhdGEgPSB2YWwucGxzci5kYXRhLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbmNvbXA9bkNvbXBzLCB0eXBlPSJyZXNwb25zZSIpWywsMV0pKQp2YWwucGxzci5vdXRwdXQgPC0gdmFsLnBsc3Iub3V0cHV0ICU+JQogIG11dGF0ZShQTFNSX1Jlc2lkdWFscyA9IFBMU1JfUHJlZGljdGVkLWdldChpblZhcikpCmhlYWQodmFsLnBsc3Iub3V0cHV0KQp2YWwuUjIgPC0gcm91bmQocGxzOjpSMihwbHNyLm91dCxuZXdkYXRhPXZhbC5wbHNyLmRhdGEsaW50ZXJjZXB0PUYpW1sxXV1bbkNvbXBzXSwyKQp2YWwuUk1TRVAgPC0gcm91bmQoc3FydChtZWFuKHZhbC5wbHNyLm91dHB1dCRQTFNSX1Jlc2lkdWFsc14yKSksMikKCnJuZ19xdWFudCA8LSBxdWFudGlsZShjYWwucGxzci5vdXRwdXRbLGluVmFyXSwgcHJvYnMgPSBjKDAuMDAxLCAwLjk5OSkpCmNhbF9zY2F0dGVyX3Bsb3QgPC0gZ2dwbG90KGNhbC5wbHNyLm91dHB1dCwgYWVzKHg9UExTUl9DVl9QcmVkaWN0ZWQsIHk9Z2V0KGluVmFyKSkpICsgCiAgdGhlbWVfYncoKSArIGdlb21fcG9pbnQoKSArIGdlb21fYWJsaW5lKGludGVyY2VwdCA9IDAsIHNsb3BlID0gMSwgY29sb3I9ImRhcmsgZ3JleSIsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBsaW5ldHlwZT0iZGFzaGVkIiwgc2l6ZT0xLjUpICsgeGxpbShybmdfcXVhbnRbMV0sIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBybmdfcXVhbnRbMl0pICsgCiAgeWxpbShybmdfcXVhbnRbMV0sIHJuZ19xdWFudFsyXSkgKwogIGxhYnMoeD1wYXN0ZTAoIlByZWRpY3RlZCAiLCBwYXN0ZShpblZhciksICIgKHVuaXRzKSIpLAogICAgICAgeT1wYXN0ZTAoIk9ic2VydmVkICIsIHBhc3RlKGluVmFyKSwgIiAodW5pdHMpIiksCiAgICAgICB0aXRsZT1wYXN0ZTAoIkNhbGlicmF0aW9uOiAiLCBwYXN0ZTAoIlJzcSA9ICIsIGNhbC5SMiksICI7ICIsIHBhc3RlMCgiUk1TRVAgPSAiLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNhbC5STVNFUCkpKSArCiAgdGhlbWUoYXhpcy50ZXh0PWVsZW1lbnRfdGV4dChzaXplPTE4KSwgbGVnZW5kLnBvc2l0aW9uPSJub25lIiwKICAgICAgICBheGlzLnRpdGxlPWVsZW1lbnRfdGV4dChzaXplPTIwLCBmYWNlPSJib2xkIiksIAogICAgICAgIGF4aXMudGV4dC54ID0gZWxlbWVudF90ZXh0KGFuZ2xlID0gMCx2anVzdCA9IDAuNSksCiAgICAgICAgcGFuZWwuYm9yZGVyID0gZWxlbWVudF9yZWN0KGxpbmV0eXBlID0gInNvbGlkIiwgZmlsbCA9IE5BLCBzaXplPTEuNSkpCgpjYWxfcmVzaWRfaGlzdG9ncmFtIDwtIGdncGxvdChjYWwucGxzci5vdXRwdXQsIGFlcyh4PVBMU1JfQ1ZfUmVzaWR1YWxzKSkgKwogIGdlb21faGlzdG9ncmFtKGFscGhhPS41LCBwb3NpdGlvbj0iaWRlbnRpdHkiKSArIAogIGdlb21fdmxpbmUoeGludGVyY2VwdCA9IDAsIGNvbG9yPSJibGFjayIsIAogICAgICAgICAgICAgbGluZXR5cGU9ImRhc2hlZCIsIHNpemU9MSkgKyB0aGVtZV9idygpICsgCiAgdGhlbWUoYXhpcy50ZXh0PWVsZW1lbnRfdGV4dChzaXplPTE4KSwgbGVnZW5kLnBvc2l0aW9uPSJub25lIiwKICAgICAgICBheGlzLnRpdGxlPWVsZW1lbnRfdGV4dChzaXplPTIwLCBmYWNlPSJib2xkIiksIAogICAgICAgIGF4aXMudGV4dC54ID0gZWxlbWVudF90ZXh0KGFuZ2xlID0gMCx2anVzdCA9IDAuNSksCiAgICAgICAgcGFuZWwuYm9yZGVyID0gZWxlbWVudF9yZWN0KGxpbmV0eXBlID0gInNvbGlkIiwgZmlsbCA9IE5BLCBzaXplPTEuNSkpCgpybmdfcXVhbnQgPC0gcXVhbnRpbGUodmFsLnBsc3Iub3V0cHV0WyxpblZhcl0sIHByb2JzID0gYygwLjAwMSwgMC45OTkpKQp2YWxfc2NhdHRlcl9wbG90IDwtIGdncGxvdCh2YWwucGxzci5vdXRwdXQsIGFlcyh4PVBMU1JfUHJlZGljdGVkLCB5PWdldChpblZhcikpKSArIAogIHRoZW1lX2J3KCkgKyBnZW9tX3BvaW50KCkgKyBnZW9tX2FibGluZShpbnRlcmNlcHQgPSAwLCBzbG9wZSA9IDEsIGNvbG9yPSJkYXJrIGdyZXkiLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbGluZXR5cGU9ImRhc2hlZCIsIHNpemU9MS41KSArIHhsaW0ocm5nX3F1YW50WzFdLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcm5nX3F1YW50WzJdKSArIAogIHlsaW0ocm5nX3F1YW50WzFdLCBybmdfcXVhbnRbMl0pICsKICBsYWJzKHg9cGFzdGUwKCJQcmVkaWN0ZWQgIiwgcGFzdGUoaW5WYXIpLCAiICh1bml0cykiKSwKICAgICAgIHk9cGFzdGUwKCJPYnNlcnZlZCAiLCBwYXN0ZShpblZhciksICIgKHVuaXRzKSIpLAogICAgICAgdGl0bGU9cGFzdGUwKCJWYWxpZGF0aW9uOiAiLCBwYXN0ZTAoIlJzcSA9ICIsIHZhbC5SMiksICI7ICIsIHBhc3RlMCgiUk1TRVAgPSAiLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdmFsLlJNU0VQKSkpICsKICB0aGVtZShheGlzLnRleHQ9ZWxlbWVudF90ZXh0KHNpemU9MTgpLCBsZWdlbmQucG9zaXRpb249Im5vbmUiLAogICAgICAgIGF4aXMudGl0bGU9ZWxlbWVudF90ZXh0KHNpemU9MjAsIGZhY2U9ImJvbGQiKSwgCiAgICAgICAgYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoYW5nbGUgPSAwLHZqdXN0ID0gMC41KSwKICAgICAgICBwYW5lbC5ib3JkZXIgPSBlbGVtZW50X3JlY3QobGluZXR5cGUgPSAic29saWQiLCBmaWxsID0gTkEsIHNpemU9MS41KSkKCnZhbF9yZXNpZF9oaXN0b2dyYW0gPC0gZ2dwbG90KHZhbC5wbHNyLm91dHB1dCwgYWVzKHg9UExTUl9SZXNpZHVhbHMpKSArCiAgZ2VvbV9oaXN0b2dyYW0oYWxwaGE9LjUsIHBvc2l0aW9uPSJpZGVudGl0eSIpICsgCiAgZ2VvbV92bGluZSh4aW50ZXJjZXB0ID0gMCwgY29sb3I9ImJsYWNrIiwgCiAgICAgICAgICAgICBsaW5ldHlwZT0iZGFzaGVkIiwgc2l6ZT0xKSArIHRoZW1lX2J3KCkgKyAKICB0aGVtZShheGlzLnRleHQ9ZWxlbWVudF90ZXh0KHNpemU9MTgpLCBsZWdlbmQucG9zaXRpb249Im5vbmUiLAogICAgICAgIGF4aXMudGl0bGU9ZWxlbWVudF90ZXh0KHNpemU9MjAsIGZhY2U9ImJvbGQiKSwgCiAgICAgICAgYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoYW5nbGUgPSAwLHZqdXN0ID0gMC41KSwKICAgICAgICBwYW5lbC5ib3JkZXIgPSBlbGVtZW50X3JlY3QobGluZXR5cGUgPSAic29saWQiLCBmaWxsID0gTkEsIHNpemU9MS41KSkKCiMgcGxvdCBjYWwvdmFsIHNpZGUtYnktc2lkZQpzY2F0dGVycGxvdHMgPC0gZ3JpZC5hcnJhbmdlKGNhbF9zY2F0dGVyX3Bsb3QsIHZhbF9zY2F0dGVyX3Bsb3QsIGNhbF9yZXNpZF9oaXN0b2dyYW0sIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIHZhbF9yZXNpZF9oaXN0b2dyYW0sIG5yb3c9MixuY29sPTIpCmdnc2F2ZShmaWxlbmFtZSA9IGZpbGUucGF0aChvdXRkaXIscGFzdGUwKGluVmFyLCJfQ2FsX1ZhbF9TY2F0dGVycGxvdHMucG5nIikpLCAKICAgICAgIHBsb3QgPSBzY2F0dGVycGxvdHMsIGRldmljZT0icG5nIiwgCiAgICAgICB3aWR0aCA9IDMyLCAKICAgICAgIGhlaWdodCA9IDMwLCB1bml0cyA9ICJjbSIsCiAgICAgICBkcGkgPSAzMDApCmBgYAoKIyMjIEdlbmVyYXRlIENvZWZmaWNpZW50IGFuZCBWSVAgcGxvdHMKYGBge3IsIGZpZy5oZWlnaHQgPSA5LCBmaWcud2lkdGggPSAxMCwgZWNobz1UUlVFfQp2aXBzIDwtIHNwZWN0cmF0cmFpdDo6VklQKHBsc3Iub3V0KVtuQ29tcHMsXQpwYXIobWZyb3c9YygyLDEpKQpwbG90KHBsc3Iub3V0LCBwbG90dHlwZSA9ICJjb2VmIix4bGFiPSJXYXZlbGVuZ3RoIChubSkiLAogICAgIHlsYWI9IlJlZ3Jlc3Npb24gY29lZmZpY2llbnRzIixsZWdlbmRwb3MgPSAiYm90dG9tcmlnaHQiLAogICAgIG5jb21wPW5Db21wcyxsd2Q9MikKYm94KGx3ZD0yLjIpCnBsb3Qoc2VxKFN0YXJ0LndhdmUsRW5kLndhdmUsMSksdmlwcyx4bGFiPSJXYXZlbGVuZ3RoIChubSkiLHlsYWI9IlZJUCIsY2V4PTAuMDEpCmxpbmVzKHNlcShTdGFydC53YXZlLEVuZC53YXZlLDEpLHZpcHMsbHdkPTMpCmFibGluZShoPTAuOCxsdHk9Mixjb2w9ImRhcmsgZ3JleSIpCmJveChsd2Q9Mi4yKQpkZXYuY29weShwbmcsZmlsZS5wYXRoKG91dGRpcixwYXN0ZTAoaW5WYXIsJ19Db2VmZmljaWVudF9WSVBfcGxvdC5wbmcnKSksIAogICAgICAgICBoZWlnaHQ9MzEwMCwgd2lkdGg9NDEwMCwgcmVzPTM0MCkKZGV2Lm9mZigpOwpgYGAKCiMjIyBKYWNra25pZmUgdmFsaWRhdGlvbgpgYGB7ciwgZWNobz1UUlVFfQppZihncmVwbCgiV2luZG93cyIsIHNlc3Npb25JbmZvKCkkcnVubmluZykpewogIHBscy5vcHRpb25zKHBhcmFsbGVsID1OVUxMKQp9IGVsc2UgewogIHBscy5vcHRpb25zKHBhcmFsbGVsID0gcGFyYWxsZWw6OmRldGVjdENvcmVzKCktMSkKfQoKamsucGxzci5vdXQgPC0gcGxzOjpwbHNyKGFzLmZvcm11bGEocGFzdGUoaW5WYXIsIn4iLCJTcGVjdHJhIikpLCBzY2FsZT1GQUxTRSwgCiAgICAgICAgICAgICAgICAgICAgICAgICBjZW50ZXI9VFJVRSwgbmNvbXA9bkNvbXBzLCB2YWxpZGF0aW9uPSJMT08iLCB0cmFjZT1GQUxTRSwgCiAgICAgICAgICAgICAgICAgICAgICAgICBqYWNra25pZmU9VFJVRSwgCiAgICAgICAgICAgICAgICAgICAgICAgICBkYXRhPWNhbC5wbHNyLmRhdGEpCnBscy5vcHRpb25zKHBhcmFsbGVsID0gTlVMTCkKCkphY2trbmlmZV9jb2VmIDwtIHNwZWN0cmF0cmFpdDo6Zi5jb2VmLnZhbGlkKHBsc3Iub3V0ID0gamsucGxzci5vdXQsIGRhdGFfcGxzciA9IGNhbC5wbHNyLmRhdGEsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbmNvbXAgPSBuQ29tcHMsIGluVmFyPWluVmFyKQpKYWNra25pZmVfaW50ZXJjZXB0IDwtIEphY2trbmlmZV9jb2VmWzEsLCxdCkphY2trbmlmZV9jb2VmIDwtIEphY2trbmlmZV9jb2VmWzI6ZGltKEphY2trbmlmZV9jb2VmKVsxXSwsLF0KCmludGVydmFsIDwtIGMoMC4wMjUsMC45NzUpCkphY2trbmlmZV9QcmVkIDwtIHZhbC5wbHNyLmRhdGEkU3BlY3RyYSAlKiUgSmFja2tuaWZlX2NvZWYgKyAKICBtYXRyaXgocmVwKEphY2trbmlmZV9pbnRlcmNlcHQsIGxlbmd0aCh2YWwucGxzci5kYXRhWyxpblZhcl0pKSwgYnlyb3c9VFJVRSwgCiAgICAgICAgIG5jb2w9bGVuZ3RoKEphY2trbmlmZV9pbnRlcmNlcHQpKQpJbnRlcnZhbF9Db25mIDwtIGFwcGx5KFggPSBKYWNra25pZmVfUHJlZCwgTUFSR0lOID0gMSwgRlVOID0gcXVhbnRpbGUsIAogICAgICAgICAgICAgICAgICAgICAgIHByb2JzPWMoaW50ZXJ2YWxbMV0sIGludGVydmFsWzJdKSkKc2RfbWVhbiA8LSBhcHBseShYID0gSmFja2tuaWZlX1ByZWQsIE1BUkdJTiA9IDEsIEZVTiA9c2QpCnNkX3JlcyA8LSBzZCh2YWwucGxzci5vdXRwdXQkUExTUl9SZXNpZHVhbHMpCnNkX3RvdCA8LSBzcXJ0KHNkX21lYW5eMitzZF9yZXNeMikKdmFsLnBsc3Iub3V0cHV0JExDSSA8LSBJbnRlcnZhbF9Db25mWzEsXQp2YWwucGxzci5vdXRwdXQkVUNJIDwtIEludGVydmFsX0NvbmZbMixdCnZhbC5wbHNyLm91dHB1dCRMUEkgPC0gdmFsLnBsc3Iub3V0cHV0JFBMU1JfUHJlZGljdGVkLTEuOTYqc2RfdG90CnZhbC5wbHNyLm91dHB1dCRVUEkgPC0gdmFsLnBsc3Iub3V0cHV0JFBMU1JfUHJlZGljdGVkKzEuOTYqc2RfdG90CmhlYWQodmFsLnBsc3Iub3V0cHV0KQp2YWwucGxzci5vdXRwdXQkTFBJIDwtIHZhbC5wbHNyLm91dHB1dCRQTFNSX1ByZWRpY3RlZC0xLjk2KnNkX3RvdAp2YWwucGxzci5vdXRwdXQkVVBJIDwtIHZhbC5wbHNyLm91dHB1dCRQTFNSX1ByZWRpY3RlZCsxLjk2KnNkX3RvdApoZWFkKHZhbC5wbHNyLm91dHB1dCkKYGBgCgojIyMgSmFja2tuaWZlIGNvZWZmaWNpZW50IHBsb3QKYGBge3IsIGZpZy5oZWlnaHQgPSA2LCBmaWcud2lkdGggPSAxMCwgZWNobz1UUlVFfQpzcGVjdHJhdHJhaXQ6OmYucGxvdC5jb2VmKFogPSB0KEphY2trbmlmZV9jb2VmKSwgd3YgPSB3diwgCiAgICAgICAgICAgIHBsb3RfbGFiZWw9IkphY2trbmlmZSByZWdyZXNzaW9uIGNvZWZmaWNpZW50cyIscG9zaXRpb24gPSAnYm90dG9tbGVmdCcpCmFibGluZShoPTAsbHR5PTIsY29sPSJncmV5NTAiKQpib3gobHdkPTIuMikKZGV2LmNvcHkocG5nLGZpbGUucGF0aChvdXRkaXIscGFzdGUwKGluVmFyLCdfSmFja2tuaWZlX1JlZ3Jlc3Npb25fQ29lZmZpY2llbnRzLnBuZycpKSwgCiAgICAgICAgIGhlaWdodD0yMTAwLCB3aWR0aD0zODAwLCByZXM9MzQwKQpkZXYub2ZmKCk7CmBgYAoKIyMjIEphY2trbmlmZSB2YWxpZGF0aW9uIHBsb3QKYGBge3IsIGZpZy5oZWlnaHQgPSA3LCBmaWcud2lkdGggPSA4LCBlY2hvPVRSVUV9CnJtc2VwX3BlcmNybXNlcCA8LSBzcGVjdHJhdHJhaXQ6OnBlcmNlbnRfcm1zZShwbHNyX2RhdGFzZXQgPSB2YWwucGxzci5vdXRwdXQsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaW5WYXIgPSBpblZhciwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXNpZHVhbHMgPSB2YWwucGxzci5vdXRwdXQkUExTUl9SZXNpZHVhbHMsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmFuZ2U9ImZ1bGwiKQpSTVNFUCA8LSBybXNlcF9wZXJjcm1zZXAkcm1zZQpwZXJjX1JNU0VQIDwtIHJtc2VwX3BlcmNybXNlcCRwZXJjX3Jtc2UKcjIgPC0gcm91bmQocGxzOjpSMihwbHNyLm91dCwgbmV3ZGF0YSA9IHZhbC5wbHNyLmRhdGEsaW50ZXJjZXB0PUYpJHZhbFtuQ29tcHNdLDIpCmV4cHIgPC0gdmVjdG9yKCJleHByZXNzaW9uIiwgMykKZXhwcltbMV1dIDwtIGJxdW90ZShSXjI9PS4ocjIpKQpleHByW1syXV0gPC0gYnF1b3RlKFJNU0VQPT0uKHJvdW5kKFJNU0VQLDIpKSkKZXhwcltbM11dIDwtIGJxdW90ZSgiJVJNU0VQIj09Lihyb3VuZChwZXJjX1JNU0VQLDIpKSkKcm5nX3ZhbHMgPC0gYyhtaW4odmFsLnBsc3Iub3V0cHV0JExQSSksIG1heCh2YWwucGxzci5vdXRwdXQkVVBJKSkKcGFyKG1mcm93PWMoMSwxKSwgbWFyPWMoNC4yLDUuMywxLDAuNCksIG9tYT1jKDAsIDAuMSwgMCwgMC4yKSkKcGxvdHJpeDo6cGxvdENJKHZhbC5wbHNyLm91dHB1dCRQTFNSX1ByZWRpY3RlZCx2YWwucGxzci5vdXRwdXRbLGluVmFyXSwgCiAgICAgICBsaT12YWwucGxzci5vdXRwdXQkTFBJLCB1aT12YWwucGxzci5vdXRwdXQkVVBJLCBnYXA9MC4wMDksc2ZyYWM9MC4wMDQsIAogICAgICAgbHdkPTEuNiwgeGxpbT1jKHJuZ192YWxzWzFdLCBybmdfdmFsc1syXSksIHlsaW09YyhybmdfdmFsc1sxXSwgcm5nX3ZhbHNbMl0pLCAKICAgICAgIGVycj0ieCIsIHBjaD0yMSwgY29sPSJibGFjayIsIHB0LmJnPXNjYWxlczo6YWxwaGEoImdyZXk3MCIsMC43KSwgc2NvbD0iZ3JleTUwIiwKICAgICAgIGNleD0yLCB4bGFiPXBhc3RlMCgiUHJlZGljdGVkICIsIHBhc3RlKGluVmFyKSwgIiAodW5pdHMpIiksCiAgICAgICB5bGFiPXBhc3RlMCgiT2JzZXJ2ZWQgIiwgcGFzdGUoaW5WYXIpLCAiICh1bml0cykiKSwKICAgICAgIGNleC5heGlzPTEuNSxjZXgubGFiPTEuOCkKYWJsaW5lKDAsMSxsdHk9Mixsdz0yKQpsZWdlbmQoInRvcGxlZnQiLCBsZWdlbmQ9ZXhwciwgYnR5PSJuIiwgY2V4PTEuNSkKYm94KGx3ZD0yLjIpCmRldi5jb3B5KHBuZyxmaWxlLnBhdGgob3V0ZGlyLHBhc3RlMChpblZhciwiX1BMU1JfVmFsaWRhdGlvbl9TY2F0dGVycGxvdC5wbmciKSksIAogICAgICAgICBoZWlnaHQ9MjgwMCwgd2lkdGg9MzIwMCwgIHJlcz0zNDApCmRldi5vZmYoKTsKYGBgCgojIyMgT3V0cHV0IGphY2trbmlmZSByZXN1bHRzCmBgYHtyLCBlY2hvPVRSVUV9Cm91dC5qay5jb2VmcyA8LSBkYXRhLmZyYW1lKEl0ZXJhdGlvbj1zZXEoMSxsZW5ndGgoSmFja2tuaWZlX2ludGVyY2VwdCksMSksCiAgICAgICAgICAgICAgICAgICAgICAgICAgIEludGVyY2VwdD1KYWNra25pZmVfaW50ZXJjZXB0LHQoSmFja2tuaWZlX2NvZWYpKQpoZWFkKG91dC5qay5jb2VmcylbMTo2XQp3cml0ZS5jc3Yob3V0LmprLmNvZWZzLGZpbGU9ZmlsZS5wYXRoKG91dGRpciwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwYXN0ZTAoaW5WYXIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICdfSmFja2tpZmVfUExTUl9Db2VmZmljaWVudHMuY3N2JykpLAogICAgICAgICAgcm93Lm5hbWVzPUZBTFNFKQpgYGAKCiMjIyBDcmVhdGUgY29yZSBQTFNSIG91dHB1dHMKYGBge3IsIGVjaG89VFJVRX0KcHJpbnQocGFzdGUoIk91dHB1dCBkaXJlY3Rvcnk6ICIsIG91dGRpcikpCgojIE9ic2VydmVkIHZlcnN1cyBwcmVkaWN0ZWQKd3JpdGUuY3N2KGNhbC5wbHNyLm91dHB1dCxmaWxlPWZpbGUucGF0aChvdXRkaXIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcGFzdGUwKGluVmFyLCdfT2JzZXJ2ZWRfUExTUl9DVl9QcmVkXycsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG5Db21wcywnY29tcC5jc3YnKSksCiAgICAgICAgICByb3cubmFtZXM9RkFMU0UpCgojIFZhbGlkYXRpb24gZGF0YQp3cml0ZS5jc3YodmFsLnBsc3Iub3V0cHV0LGZpbGU9ZmlsZS5wYXRoKG91dGRpciwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwYXN0ZTAoaW5WYXIsJ19WYWxpZGF0aW9uX1BMU1JfUHJlZF8nLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuQ29tcHMsJ2NvbXAuY3N2JykpLAogICAgICAgICAgcm93Lm5hbWVzPUZBTFNFKQoKIyBNb2RlbCBjb2VmZmljaWVudHMKY29lZnMgPC0gY29lZihwbHNyLm91dCxuY29tcD1uQ29tcHMsaW50ZXJjZXB0PVRSVUUpCndyaXRlLmNzdihjb2VmcyxmaWxlPWZpbGUucGF0aChvdXRkaXIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwYXN0ZTAoaW5WYXIsJ19QTFNSX0NvZWZmaWNpZW50c18nLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG5Db21wcywnY29tcC5jc3YnKSksCiAgICAgICAgICByb3cubmFtZXM9VFJVRSkKCiMgUExTUiBWSVAKd3JpdGUuY3N2KHZpcHMsZmlsZT1maWxlLnBhdGgob3V0ZGlyLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwYXN0ZTAoaW5WYXIsJ19QTFNSX1ZJUHNfJywKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG5Db21wcywnY29tcC5jc3YnKSkpCmBgYAoKIyMjIENvbmZpcm0gZmlsZXMgd2VyZSB3cml0dGVuIHRvIHRlbXAgc3BhY2UKYGBge3IsIGVjaG89VFJVRX0KcHJpbnQoIioqKiogUExTUiBvdXRwdXQgZmlsZXM6ICIpCnByaW50KGxpc3QuZmlsZXMob3V0ZGlyKVtncmVwKHBhdHRlcm4gPSBpblZhciwgbGlzdC5maWxlcyhvdXRkaXIpKV0pCmBgYAo=