Overview

This is an R Markdown Notebook to illustrate how to develop pixel-scale spectra-trait PLSR models. This example uses image data from NEON AOP and associated field measurements of leaf nitrogen content collected across a range of CONUS NEON sites. For more information refer to the dataset EcoSIS page: https://ecosis.org/package/canopy-spectra-to-map-foliar-functional-traits-over-neon-domains-in-eastern-united-states

Getting Started

Load libraries

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

Setup other functions and options

### Setup other functions and options
# not in
`%notin%` <- Negate(`%in%`)

# 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? What is the variable name in the input dataset?
inVar <- "Nitrogen"

# What is the source dataset from EcoSIS?
ecosis_id <- "b9dbf3db-5b9c-4ab2-88c2-26c8b39d0903"

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

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...

── Column specification ────────────────────────────────────────────────────────────────────────────────────────────────────────
cols(
  .default = col_double(),
  Affiliation = col_character(),
  PI = col_character(),
  Plot_ID = col_character(),
  Project = col_character()
)
ℹ Use `spec()` for the full column specifications.

Download complete!
head(dat_raw)
names(dat_raw)[1:40]
 [1] "Affiliation"       "Boron"             "Calcium"           "Carbon"            "Carotenoids_area"  "Carotenoids_mass" 
 [7] "Cellulose"         "Chlorophylls_area" "Chlorophylls_mass" "Copper"            "EWT"               "Fiber"            
[13] "Flavonoids"        "LMA"               "Lignin"            "Magnesium"         "Manganese"         "NSC"              
[19] "Nitrogen"          "PI"                "Phenolics"         "Phosphorus"        "Plot_ID"           "Potassium"        
[25] "Project"           "SLA"               "Sample_Year"       "Starch"            "Sugar"             "Sulfur"           
[31] "Water"             "d13C"              "d15N"              "384"               "389"               "394"              
[37] "399"               "404"               "409"               "414"              

Create full plsr dataset

# identify the trait data and other metadata
sample_info <- dat_raw[,names(dat_raw) %notin% seq(300,2600,1)]
head(sample_info)

# spectra matrix
Spectra <- as.matrix(dat_raw[,names(dat_raw) %notin% names(sample_info)])

# set the desired spectra wavelength range to include
Start.wave <- 500
End.wave <- 2400
wv <- seq(Start.wave,End.wave,1)
final_spec <- Spectra[,round(as.numeric(colnames(Spectra))) %in% wv]
colnames(final_spec) <- c(paste0("Wave_",colnames(final_spec)))

## Drop bad spectra data - for canopy-scale reflectance, often the "water band" wavelengths
## are too noisy to use for trait estimation.  Its possible to remove these wavelengths
## prior to model fitting. Its best to first identify which wavelengths to drop
## before attempting PLSR, as these ranges may need to be considered on a case-by-case 
## basis or generalized for multiple datasets
dropwaves <- c(1350:1440, 1826:1946)
final_spec <- final_spec[,colnames(final_spec) %notin% paste0("Wave_",dropwaves)]
wv <- as.numeric(gsub(pattern = "Wave_",replacement = "", x = colnames(final_spec)))

## Drop bad spectra data - for canopy-scale reflectance, often the "water band" wavelengths
## are too noisy to use for trait estimation.  Its possible to remove these wavelengths
## prior to model fitting. Its best to first identify which wavelengths to drop
## before attempting PLSR, as these ranges may need to be considered on a case-by-case 
## basis or generalized for multiple datasets
dropwaves <- c(1350:1440, 1826:1946)
final_spec <- final_spec[,colnames(final_spec) %notin% paste0("Wave_",dropwaves)]
wv <- as.numeric(gsub(pattern = "Wave_",replacement = "", x = colnames(final_spec)))

# assemble example dataset
sample_info2 <- sample_info %>%
  select(Plot_ID,Sample_Year,SLA,Nitrogen)
site_plot <- data.frame(matrix(unlist(strsplit(sample_info2$Plot_ID, "_")), 
                               ncol=2, byrow=TRUE))
colnames(site_plot) <- c("Plot_Num","SampleID")
sample_info3 <- data.frame(site_plot,sample_info2)

plsr_data <- data.frame(sample_info3,final_spec*0.01)
rm(sample_info,sample_info2,sample_info3,Spectra, site_plot)

Example data cleaning.

# 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 %>%  # remove erroneously high values, or "bad spectra"
  filter(Nitrogen<50) %>%
  filter(Wave_859<80) %>%
  filter(Wave_859>15)
plsr_data <- plsr_data[complete.cases(plsr_data[,names(plsr_data) %in% 
                                                  c(inVar,paste0("Wave_",wv))]),]

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=2356326,
                                              prop=0.8, group_variables="Plot_Num")
D02   Cal: 80.46%
D03   Cal: 80.328%
D05   Cal: 80%
D06   Cal: 79.73%
D07   Cal: 79.245%
D08   Cal: 79.817%
D09   Cal: 79.63%
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: 517"
print(paste("Val observations: ",dim(val.plsr.data)[1],sep=""))
[1] "Val observations: 130"
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

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 permutation to determine 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: 12"
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  
      6.538        5.984        5.792        5.662        5.284        5.235        5.149        5.252        5.121  
    9 comps     10 comps     11 comps     12 comps  
      4.896        4.855        4.755        4.646  
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)

R2(plsr.out, newdata = val.plsr.data)
(Intercept)      1 comps      2 comps      3 comps      4 comps      5 comps      6 comps      7 comps      8 comps  
 -0.0001616    0.1621284    0.2150431    0.2498762    0.3467097    0.3586424    0.3796062    0.3544358    0.3863604  
    9 comps     10 comps     11 comps     12 comps  
  0.4391471    0.4484252    0.4708911    0.4948347  
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$coefficients[,,nComps], x=wv,xlab="Wavelength (nm)",
     ylab="Regression coefficients",lwd=2,type='l')
box(lwd=2.2)
plot(wv, vips, xlab="Wavelength (nm)",ylab="VIP",cex=0.01)
lines(wv, 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)

Bootstrap validation

[1] "*** Running permutation test.  Please hang tight, this can take awhile ***"
[1] "Options:"
[1] "Max Components: 12 Iterations: 500 Data Proportion (percent): 70"
[1] "*** Providing PRESS and coefficient array output ***"

Jackknife coefficient plot

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

Bootstrap 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.000, 
                lwd=1.6, xlim=c(rng_vals[1], rng_vals[2]), ylim=c(rng_vals[1], rng_vals[2]), 
                err="x", pch=21, col="black", pt.bg=scales::alpha("grey70",0.7), scol="grey80",
                cex=2, xlab=paste0("Predicted ", paste(inVar), " (units)"),
                ylab=paste0("Observed ", paste(inVar), " (units)"),
                cex.axis=1.5,cex.lab=1.8)
abline(0,1,lty=2,lw=2)
plotrix::plotCI(val.plsr.output$PLSR_Predicted,val.plsr.output[,inVar], 
                li=val.plsr.output$LCI, ui=val.plsr.output$UCI, gap=0.009,sfrac=0.004, 
                lwd=1.6, xlim=c(rng_vals[1], rng_vals[2]), ylim=c(rng_vals[1], rng_vals[2]), 
                err="x", pch=21, col="black", pt.bg=scales::alpha("grey70",0.7), scol="black",
                cex=2, xlab=paste0("Predicted ", paste(inVar), " (units)"),
                ylab=paste0("Observed ", paste(inVar), " (units)"),
                cex.axis=1.5,cex.lab=1.8, add=T)
legend("topleft", legend=expr, bty="n", cex=1.5)
legend("bottomright", legend=c("Prediction Interval","Confidence Interval"), 
       lty=c(1,1), col = c("grey80","black"), lwd=3, bty="n", cex=1.5)
box(lwd=2.2)
dev.copy(png,file.path(outdir,paste0(inVar,"_PLSR_Validation_Scatterplot.png")), 
         height=2800, width=3200,  res=340)
quartz_off_screen 
                3 
dev.off();
quartz_off_screen 
                2 

Output bootstrap results

out.jk.coefs <- data.frame(Iteration=seq(1,length(bootstrap_intercept),1),
                           Intercept=bootstrap_intercept,t(bootstrap_coef))
names(out.jk.coefs) <- c("Iteration","Intercept",paste0("Wave_",wv))
head(out.jk.coefs)[1:6]
write.csv(out.jk.coefs,file=file.path(outdir,paste0(inVar,'_Bootstrap_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] "Nitrogen_Bootstrap_PLSR_Coefficients.csv"       "Nitrogen_Bootstrap_Regression_Coefficients.png"
 [3] "Nitrogen_Cal_PLSR_Dataset.csv"                  "Nitrogen_Cal_Val_Histograms.png"               
 [5] "Nitrogen_Cal_Val_Scatterplots.png"              "Nitrogen_Cal_Val_Spectra.png"                  
 [7] "Nitrogen_Coefficient_VIP_plot.png"              "Nitrogen_Observed_PLSR_CV_Pred_12comp.csv"     
 [9] "Nitrogen_PLSR_Coefficients_12comp.csv"          "Nitrogen_PLSR_Component_Selection.png"         
[11] "Nitrogen_PLSR_Validation_Scatterplot.png"       "Nitrogen_PLSR_VIPs_12comp.csv"                 
[13] "Nitrogen_Val_PLSR_Dataset.csv"                  "Nitrogen_Validation_PLSR_Pred_12comp.csv"      
[15] "Nitrogen_Validation_RMSEP_R2_by_Component.png" 
LS0tCnRpdGxlOiBTcGVjdHJhLXRyYWl0IFBMU1IgZXhhbXBsZSB1c2luZyBORU9OIEFPUCBwaXhlbCBzcGVjdHJhIGFuZCBmaWVsZC1zYW1wbGVkIGxlYWYgbml0cm9nZW4gY29udGVudCBmcm9tIENPTlVTIE5FT04gc2l0ZXMKYXV0aG9yOiAiU2hhd24gUC4gU2VyYmluLCBKdWxpZW4gTGFtb3VyLCAmIEplcmVtaWFoIEFuZGVyc29uIgpvdXRwdXQ6CiAgaHRtbF9ub3RlYm9vazogZGVmYXVsdAogIHBkZl9kb2N1bWVudDogZGVmYXVsdAogIGh0bWxfZG9jdW1lbnQ6CiAgICBkZl9wcmludDogcGFnZWQKICBnaXRodWJfZG9jdW1lbnQ6IGRlZmF1bHQKcGFyYW1zOgogIGRhdGU6ICFyIFN5cy5EYXRlKCkKLS0tCgpgYGB7ciBzZXR1cCwgaW5jbHVkZT1GQUxTRX0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFKQpgYGAKCiMjIyBPdmVydmlldwpUaGlzIGlzIGFuIFtSIE1hcmtkb3duXShodHRwOi8vcm1hcmtkb3duLnJzdHVkaW8uY29tKSBOb3RlYm9vayB0byBpbGx1c3RyYXRlIGhvdyB0byBkZXZlbG9wCnBpeGVsLXNjYWxlIHNwZWN0cmEtdHJhaXQgUExTUiBtb2RlbHMuIFRoaXMgZXhhbXBsZSB1c2VzIGltYWdlIGRhdGEgZnJvbSBORU9OIEFPUCBhbmQgYXNzb2NpYXRlZCBmaWVsZCBtZWFzdXJlbWVudHMgb2YgbGVhZiBuaXRyb2dlbiBjb250ZW50IGNvbGxlY3RlZCBhY3Jvc3MgYSByYW5nZSBvZiBDT05VUyBORU9OIHNpdGVzLiAgRm9yIG1vcmUgaW5mb3JtYXRpb24gcmVmZXIgdG8gdGhlIGRhdGFzZXQgRWNvU0lTIHBhZ2U6Cmh0dHBzOi8vZWNvc2lzLm9yZy9wYWNrYWdlL2Nhbm9weS1zcGVjdHJhLXRvLW1hcC1mb2xpYXItZnVuY3Rpb25hbC10cmFpdHMtb3Zlci1uZW9uLWRvbWFpbnMtaW4tZWFzdGVybi11bml0ZWQtc3RhdGVzCgojIyMgR2V0dGluZyBTdGFydGVkCiMjIyBMb2FkIGxpYnJhcmllcwpgYGB7ciwgZXZhbD1UUlVFLCBlY2hvPVRSVUV9Cmxpc3Qub2YucGFja2FnZXMgPC0gYygicGxzIiwiZHBseXIiLCJoZXJlIiwicGxvdHJpeCIsImdncGxvdDIiLCJncmlkRXh0cmEiLCJzcGVjdHJhdHJhaXQiKQppbnZpc2libGUobGFwcGx5KGxpc3Qub2YucGFja2FnZXMsIGxpYnJhcnksIGNoYXJhY3Rlci5vbmx5ID0gVFJVRSkpCmBgYAoKIyMjIFNldHVwIG90aGVyIGZ1bmN0aW9ucyBhbmQgb3B0aW9ucwpgYGB7ciwgZWNobz1UUlVFfQojIyMgU2V0dXAgb3RoZXIgZnVuY3Rpb25zIGFuZCBvcHRpb25zCiMgbm90IGluCmAlbm90aW4lYCA8LSBOZWdhdGUoYCVpbiVgKQoKIyBTY3JpcHQgb3B0aW9ucwpwbHM6OnBscy5vcHRpb25zKHBsc3JhbGcgPSAib3Njb3Jlc3BscyIpCnBsczo6cGxzLm9wdGlvbnMoInBsc3JhbGciKQoKIyBEZWZhdWx0IHBhciBvcHRpb25zCm9wYXIgPC0gcGFyKG5vLnJlYWRvbmx5ID0gVCkKCiMgV2hhdCBpcyB0aGUgdGFyZ2V0IHZhcmlhYmxlPyBXaGF0IGlzIHRoZSB2YXJpYWJsZSBuYW1lIGluIHRoZSBpbnB1dCBkYXRhc2V0PwppblZhciA8LSAiTml0cm9nZW4iCgojIFdoYXQgaXMgdGhlIHNvdXJjZSBkYXRhc2V0IGZyb20gRWNvU0lTPwplY29zaXNfaWQgPC0gImI5ZGJmM2RiLTViOWMtNGFiMi04OGMyLTI2YzhiMzlkMDkwMyIKCiMgU3BlY2lmeSBvdXRwdXQgZGlyZWN0b3J5LCBvdXRwdXRfZGlyIAojIE9wdGlvbnM6IAojIHRlbXBkaXIgLSB1c2UgYSBPUy1zcGVjaWZpZWQgdGVtcG9yYXJ5IGRpcmVjdG9yeSAKIyB1c2VyIGRlZmluZWQgUEFUSCAtIGUuZy4gIn4vc2NyYXRjaC9QTFNSIgpvdXRwdXRfZGlyIDwtICJ0ZW1wZGlyIgpgYGAKCiMjIyBTZXQgd29ya2luZyBkaXJlY3RvcnkgKHNjcmF0Y2ggc3BhY2UpCmBgYHtyLCBlY2hvPUZBTFNFfQppZiAob3V0cHV0X2Rpcj09InRlbXBkaXIiKSB7CiAgb3V0ZGlyIDwtIHRlbXBkaXIoKQp9IGVsc2UgewogIGlmICghIGZpbGUuZXhpc3RzKG91dHB1dF9kaXIpKSBkaXIuY3JlYXRlKG91dHB1dF9kaXIscmVjdXJzaXZlPVRSVUUpCiAgb3V0ZGlyIDwtIGZpbGUucGF0aChwYXRoLmV4cGFuZChvdXRwdXRfZGlyKSkKfQpzZXR3ZChvdXRkaXIpICMgc2V0IHdvcmtpbmcgZGlyZWN0b3J5CnByaW50KGdldHdkKCkpICAjIGNoZWNrIHdkCmBgYAoKIyMjIEdyYWIgZGF0YSBmcm9tIEVjb1NJUwpgYGB7ciwgZWNobz1UUlVFfQpwcmludChwYXN0ZTAoIk91dHB1dCBkaXJlY3Rvcnk6ICIsZ2V0d2QoKSkpICAjIGNoZWNrIHdkCmRhdF9yYXcgPC0gc3BlY3RyYXRyYWl0OjpnZXRfZWNvc2lzX2RhdGEoZWNvc2lzX2lkID0gZWNvc2lzX2lkKQpoZWFkKGRhdF9yYXcpCm5hbWVzKGRhdF9yYXcpWzE6NDBdCmBgYAoKIyMjIENyZWF0ZSBmdWxsIHBsc3IgZGF0YXNldApgYGB7ciwgZWNobz1UUlVFfQojIGlkZW50aWZ5IHRoZSB0cmFpdCBkYXRhIGFuZCBvdGhlciBtZXRhZGF0YQpzYW1wbGVfaW5mbyA8LSBkYXRfcmF3WyxuYW1lcyhkYXRfcmF3KSAlbm90aW4lIHNlcSgzMDAsMjYwMCwxKV0KaGVhZChzYW1wbGVfaW5mbykKCiMgc3BlY3RyYSBtYXRyaXgKU3BlY3RyYSA8LSBhcy5tYXRyaXgoZGF0X3Jhd1ssbmFtZXMoZGF0X3JhdykgJW5vdGluJSBuYW1lcyhzYW1wbGVfaW5mbyldKQoKIyBzZXQgdGhlIGRlc2lyZWQgc3BlY3RyYSB3YXZlbGVuZ3RoIHJhbmdlIHRvIGluY2x1ZGUKU3RhcnQud2F2ZSA8LSA1MDAKRW5kLndhdmUgPC0gMjQwMAp3diA8LSBzZXEoU3RhcnQud2F2ZSxFbmQud2F2ZSwxKQpmaW5hbF9zcGVjIDwtIFNwZWN0cmFbLHJvdW5kKGFzLm51bWVyaWMoY29sbmFtZXMoU3BlY3RyYSkpKSAlaW4lIHd2XQpjb2xuYW1lcyhmaW5hbF9zcGVjKSA8LSBjKHBhc3RlMCgiV2F2ZV8iLGNvbG5hbWVzKGZpbmFsX3NwZWMpKSkKCiMjIERyb3AgYmFkIHNwZWN0cmEgZGF0YSAtIGZvciBjYW5vcHktc2NhbGUgcmVmbGVjdGFuY2UsIG9mdGVuIHRoZSAid2F0ZXIgYmFuZCIgd2F2ZWxlbmd0aHMKIyMgYXJlIHRvbyBub2lzeSB0byB1c2UgZm9yIHRyYWl0IGVzdGltYXRpb24uICBJdHMgcG9zc2libGUgdG8gcmVtb3ZlIHRoZXNlIHdhdmVsZW5ndGhzCiMjIHByaW9yIHRvIG1vZGVsIGZpdHRpbmcuIEl0cyBiZXN0IHRvIGZpcnN0IGlkZW50aWZ5IHdoaWNoIHdhdmVsZW5ndGhzIHRvIGRyb3AKIyMgYmVmb3JlIGF0dGVtcHRpbmcgUExTUiwgYXMgdGhlc2UgcmFuZ2VzIG1heSBuZWVkIHRvIGJlIGNvbnNpZGVyZWQgb24gYSBjYXNlLWJ5LWNhc2UgCiMjIGJhc2lzIG9yIGdlbmVyYWxpemVkIGZvciBtdWx0aXBsZSBkYXRhc2V0cwpkcm9wd2F2ZXMgPC0gYygxMzUwOjE0NDAsIDE4MjY6MTk0NikKZmluYWxfc3BlYyA8LSBmaW5hbF9zcGVjWyxjb2xuYW1lcyhmaW5hbF9zcGVjKSAlbm90aW4lIHBhc3RlMCgiV2F2ZV8iLGRyb3B3YXZlcyldCnd2IDwtIGFzLm51bWVyaWMoZ3N1YihwYXR0ZXJuID0gIldhdmVfIixyZXBsYWNlbWVudCA9ICIiLCB4ID0gY29sbmFtZXMoZmluYWxfc3BlYykpKQoKIyMgRHJvcCBiYWQgc3BlY3RyYSBkYXRhIC0gZm9yIGNhbm9weS1zY2FsZSByZWZsZWN0YW5jZSwgb2Z0ZW4gdGhlICJ3YXRlciBiYW5kIiB3YXZlbGVuZ3RocwojIyBhcmUgdG9vIG5vaXN5IHRvIHVzZSBmb3IgdHJhaXQgZXN0aW1hdGlvbi4gIEl0cyBwb3NzaWJsZSB0byByZW1vdmUgdGhlc2Ugd2F2ZWxlbmd0aHMKIyMgcHJpb3IgdG8gbW9kZWwgZml0dGluZy4gSXRzIGJlc3QgdG8gZmlyc3QgaWRlbnRpZnkgd2hpY2ggd2F2ZWxlbmd0aHMgdG8gZHJvcAojIyBiZWZvcmUgYXR0ZW1wdGluZyBQTFNSLCBhcyB0aGVzZSByYW5nZXMgbWF5IG5lZWQgdG8gYmUgY29uc2lkZXJlZCBvbiBhIGNhc2UtYnktY2FzZSAKIyMgYmFzaXMgb3IgZ2VuZXJhbGl6ZWQgZm9yIG11bHRpcGxlIGRhdGFzZXRzCmRyb3B3YXZlcyA8LSBjKDEzNTA6MTQ0MCwgMTgyNjoxOTQ2KQpmaW5hbF9zcGVjIDwtIGZpbmFsX3NwZWNbLGNvbG5hbWVzKGZpbmFsX3NwZWMpICVub3RpbiUgcGFzdGUwKCJXYXZlXyIsZHJvcHdhdmVzKV0Kd3YgPC0gYXMubnVtZXJpYyhnc3ViKHBhdHRlcm4gPSAiV2F2ZV8iLHJlcGxhY2VtZW50ID0gIiIsIHggPSBjb2xuYW1lcyhmaW5hbF9zcGVjKSkpCgojIGFzc2VtYmxlIGV4YW1wbGUgZGF0YXNldApzYW1wbGVfaW5mbzIgPC0gc2FtcGxlX2luZm8gJT4lCiAgc2VsZWN0KFBsb3RfSUQsU2FtcGxlX1llYXIsU0xBLE5pdHJvZ2VuKQpzaXRlX3Bsb3QgPC0gZGF0YS5mcmFtZShtYXRyaXgodW5saXN0KHN0cnNwbGl0KHNhbXBsZV9pbmZvMiRQbG90X0lELCAiXyIpKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuY29sPTIsIGJ5cm93PVRSVUUpKQpjb2xuYW1lcyhzaXRlX3Bsb3QpIDwtIGMoIlBsb3RfTnVtIiwiU2FtcGxlSUQiKQpzYW1wbGVfaW5mbzMgPC0gZGF0YS5mcmFtZShzaXRlX3Bsb3Qsc2FtcGxlX2luZm8yKQoKcGxzcl9kYXRhIDwtIGRhdGEuZnJhbWUoc2FtcGxlX2luZm8zLGZpbmFsX3NwZWMqMC4wMSkKcm0oc2FtcGxlX2luZm8sc2FtcGxlX2luZm8yLHNhbXBsZV9pbmZvMyxTcGVjdHJhLCBzaXRlX3Bsb3QpCmBgYAoKIyMjIyBFeGFtcGxlIGRhdGEgY2xlYW5pbmcuCmBgYHtyLCBlY2hvPVRSVUV9CiMgRXhhbXBsZSBkYXRhIGNsZWFuaW5nLiAgRW5kIHVzZXIgbmVlZHMgdG8gZG8gd2hhdCdzIGFwcHJvcHJpYXRlIGZvciB0aGVpciAKIyBkYXRhLiBUaGlzIG1heSBiZSBhbiBpdGVyYXRpdmUgcHJvY2Vzcy4KIyBLZWVwIG9ubHkgY29tcGxldGUgcm93cyBvZiBpblZhciBhbmQgc3BlYyBkYXRhIGJlZm9yZSBmaXR0aW5nCiMKcGxzcl9kYXRhIDwtIHBsc3JfZGF0YSAlPiUgICMgcmVtb3ZlIGVycm9uZW91c2x5IGhpZ2ggdmFsdWVzLCBvciAiYmFkIHNwZWN0cmEiCiAgZmlsdGVyKE5pdHJvZ2VuPDUwKSAlPiUKICBmaWx0ZXIoV2F2ZV84NTk8ODApICU+JQogIGZpbHRlcihXYXZlXzg1OT4xNSkKcGxzcl9kYXRhIDwtIHBsc3JfZGF0YVtjb21wbGV0ZS5jYXNlcyhwbHNyX2RhdGFbLG5hbWVzKHBsc3JfZGF0YSkgJWluJSAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjKGluVmFyLHBhc3RlMCgiV2F2ZV8iLHd2KSldKSxdCmBgYAoKIyMjIENyZWF0ZSBjYWwvdmFsIGRhdGFzZXRzCmBgYHtyLCBmaWcuaGVpZ2h0ID0gNSwgZmlnLndpZHRoID0gMTIsIGVjaG89VFJVRX0KIyMgTWFrZSBhIHN0cmF0aWZpZWQgcmFuZG9tIHNhbXBsaW5nIGluIHRoZSBzdHJhdGEgVVNEQV9TcGVjaWVzX0NvZGUgYW5kIERvbWFpbgoKbWV0aG9kIDwtICJiYXNlIiAjYmFzZS9kcGx5cgojIGJhc2UgUiAtIGEgYml0IHNsb3cKIyBkcGx5ciAtIG11Y2ggZmFzdGVyCnNwbGl0X2RhdGEgPC0gc3BlY3RyYXRyYWl0OjpjcmVhdGVfZGF0YV9zcGxpdChkYXRhc2V0PXBsc3JfZGF0YSwgYXBwcm9hY2g9bWV0aG9kLCBzcGxpdF9zZWVkPTIzNTYzMjYsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwcm9wPTAuOCwgZ3JvdXBfdmFyaWFibGVzPSJQbG90X051bSIpCm5hbWVzKHNwbGl0X2RhdGEpCmNhbC5wbHNyLmRhdGEgPC0gc3BsaXRfZGF0YSRjYWxfZGF0YQpoZWFkKGNhbC5wbHNyLmRhdGEpWzE6OF0KdmFsLnBsc3IuZGF0YSA8LSBzcGxpdF9kYXRhJHZhbF9kYXRhCmhlYWQodmFsLnBsc3IuZGF0YSlbMTo4XQpybShzcGxpdF9kYXRhKQoKIyBEYXRhc2V0czoKcHJpbnQocGFzdGUoIkNhbCBvYnNlcnZhdGlvbnM6ICIsZGltKGNhbC5wbHNyLmRhdGEpWzFdLHNlcD0iIikpCnByaW50KHBhc3RlKCJWYWwgb2JzZXJ2YXRpb25zOiAiLGRpbSh2YWwucGxzci5kYXRhKVsxXSxzZXA9IiIpKQoKY2FsX2hpc3RfcGxvdCA8LSBxcGxvdChjYWwucGxzci5kYXRhWyxwYXN0ZTAoaW5WYXIpXSxnZW9tPSJoaXN0b2dyYW0iLAogICAgICAgICAgICAgICAgICAgICAgIG1haW4gPSBwYXN0ZTAoIkNhbC4gSGlzdG9ncmFtIGZvciAiLGluVmFyKSwKICAgICAgICAgICAgICAgICAgICAgICB4bGFiID0gcGFzdGUwKGluVmFyKSx5bGFiID0gIkNvdW50IixmaWxsPUkoImdyZXk1MCIpLGNvbD1JKCJibGFjayIpLAogICAgICAgICAgICAgICAgICAgICAgIGFscGhhPUkoLjcpKQp2YWxfaGlzdF9wbG90IDwtIHFwbG90KHZhbC5wbHNyLmRhdGFbLHBhc3RlMChpblZhcildLGdlb209Imhpc3RvZ3JhbSIsCiAgICAgICAgICAgICAgICAgICAgICAgbWFpbiA9IHBhc3RlMCgiVmFsLiBIaXN0b2dyYW0gZm9yICIsaW5WYXIpLAogICAgICAgICAgICAgICAgICAgICAgIHhsYWIgPSBwYXN0ZTAoaW5WYXIpLHlsYWIgPSAiQ291bnQiLGZpbGw9SSgiZ3JleTUwIiksY29sPUkoImJsYWNrIiksCiAgICAgICAgICAgICAgICAgICAgICAgYWxwaGE9SSguNykpCmhpc3RvZ3JhbXMgPC0gZ3JpZC5hcnJhbmdlKGNhbF9oaXN0X3Bsb3QsIHZhbF9oaXN0X3Bsb3QsIG5jb2w9MikKZ2dzYXZlKGZpbGVuYW1lID0gZmlsZS5wYXRoKG91dGRpcixwYXN0ZTAoaW5WYXIsIl9DYWxfVmFsX0hpc3RvZ3JhbXMucG5nIikpLCBwbG90ID0gaGlzdG9ncmFtcywgCiAgICAgICBkZXZpY2U9InBuZyIsIHdpZHRoID0gMzAsIAogICAgICAgaGVpZ2h0ID0gMTIsIHVuaXRzID0gImNtIiwKICAgICAgIGRwaSA9IDMwMCkKIyBvdXRwdXQgY2FsL3ZhbCBkYXRhCndyaXRlLmNzdihjYWwucGxzci5kYXRhLGZpbGU9ZmlsZS5wYXRoKG91dGRpcixwYXN0ZTAoaW5WYXIsJ19DYWxfUExTUl9EYXRhc2V0LmNzdicpKSwKICAgICAgICAgIHJvdy5uYW1lcz1GQUxTRSkKd3JpdGUuY3N2KHZhbC5wbHNyLmRhdGEsZmlsZT1maWxlLnBhdGgob3V0ZGlyLHBhc3RlMChpblZhciwnX1ZhbF9QTFNSX0RhdGFzZXQuY3N2JykpLAogICAgICAgICAgcm93Lm5hbWVzPUZBTFNFKQpgYGAKCiMjIyBDcmVhdGUgY2FsaWJyYXRpb24gYW5kIHZhbGlkYXRpb24gUExTUiBkYXRhc2V0cwpgYGB7ciwgZWNobz1UUlVFfQpjYWxfc3BlYyA8LSBhcy5tYXRyaXgoY2FsLnBsc3IuZGF0YVssIHdoaWNoKG5hbWVzKGNhbC5wbHNyLmRhdGEpICVpbiUgcGFzdGUwKCJXYXZlXyIsd3YpKV0pCmNhbC5wbHNyLmRhdGEgPC0gZGF0YS5mcmFtZShjYWwucGxzci5kYXRhWywgd2hpY2gobmFtZXMoY2FsLnBsc3IuZGF0YSkgJW5vdGluJSBwYXN0ZTAoIldhdmVfIix3dikpXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIFNwZWN0cmE9SShjYWxfc3BlYykpCmhlYWQoY2FsLnBsc3IuZGF0YSlbMTo1XQoKdmFsX3NwZWMgPC0gYXMubWF0cml4KHZhbC5wbHNyLmRhdGFbLCB3aGljaChuYW1lcyh2YWwucGxzci5kYXRhKSAlaW4lIHBhc3RlMCgiV2F2ZV8iLHd2KSldKQp2YWwucGxzci5kYXRhIDwtIGRhdGEuZnJhbWUodmFsLnBsc3IuZGF0YVssIHdoaWNoKG5hbWVzKHZhbC5wbHNyLmRhdGEpICVub3RpbiUgcGFzdGUwKCJXYXZlXyIsd3YpKV0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBTcGVjdHJhPUkodmFsX3NwZWMpKQpoZWFkKHZhbC5wbHNyLmRhdGEpWzE6NV0KYGBgCgojIyMgcGxvdCBjYWwgYW5kIHZhbCBzcGVjdHJhCmBgYHtyLCBmaWcuaGVpZ2h0ID0gNSwgZmlnLndpZHRoID0gMTIsIGVjaG89VFJVRX0KcGFyKG1mcm93PWMoMSwyKSkgIyBCLCBMLCBULCBSCnNwZWN0cmF0cmFpdDo6Zi5wbG90LnNwZWMoWj1jYWwucGxzci5kYXRhJFNwZWN0cmEsd3Y9d3YscGxvdF9sYWJlbD0iQ2FsaWJyYXRpb24iKQpzcGVjdHJhdHJhaXQ6OmYucGxvdC5zcGVjKFo9dmFsLnBsc3IuZGF0YSRTcGVjdHJhLHd2PXd2LHBsb3RfbGFiZWw9IlZhbGlkYXRpb24iKQoKZGV2LmNvcHkocG5nLGZpbGUucGF0aChvdXRkaXIscGFzdGUwKGluVmFyLCdfQ2FsX1ZhbF9TcGVjdHJhLnBuZycpKSwgCiAgICAgICAgIGhlaWdodD0yNTAwLHdpZHRoPTQ5MDAsIHJlcz0zNDApCmRldi5vZmYoKTsKcGFyKG1mcm93PWMoMSwxKSkKYGBgCgojIyMgVXNlIHBlcm11dGF0aW9uIHRvIGRldGVybWluZSBvcHRpbWFsIG51bWJlciBvZiBjb21wb25lbnRzCmBgYHtyLCBmaWcuaGVpZ2h0ID0gNiwgZmlnLndpZHRoID0gMTAsIGVjaG89VFJVRX0KaWYoZ3JlcGwoIldpbmRvd3MiLCBzZXNzaW9uSW5mbygpJHJ1bm5pbmcpKXsKICBwbHMub3B0aW9ucyhwYXJhbGxlbCA9IE5VTEwpCn0gZWxzZSB7CiAgcGxzLm9wdGlvbnMocGFyYWxsZWwgPSBwYXJhbGxlbDo6ZGV0ZWN0Q29yZXMoKS0xKQp9CgptZXRob2QgPC0gInBscyIgI3BscywgZmlyc3RQbGF0ZWF1LCBmaXJzdE1pbgpyYW5kb21fc2VlZCA8LSAxMjQ1NTY1CnNlZyA8LSA1MAptYXhDb21wcyA8LSAxNgppdGVyYXRpb25zIDwtIDgwCnByb3AgPC0gMC43MAppZiAobWV0aG9kPT0icGxzIikgewogICMgcGxzIHBhY2thZ2UgYXBwcm9hY2ggLSBmYXN0ZXIgYnV0IGVzdGltYXRlcyBtb3JlIGNvbXBvbmVudHMuLi4uCiAgbkNvbXBzIDwtIHNwZWN0cmF0cmFpdDo6ZmluZF9vcHRpbWFsX2NvbXBvbmVudHMoZGF0YXNldD1jYWwucGxzci5kYXRhLCB0YXJnZXRWYXJpYWJsZT1pblZhciwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtZXRob2Q9bWV0aG9kLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtYXhDb21wcz1tYXhDb21wcywgc2VnPXNlZywgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmFuZG9tX3NlZWQ9cmFuZG9tX3NlZWQpCiAgcHJpbnQocGFzdGUwKCIqKiogT3B0aW1hbCBudW1iZXIgb2YgY29tcG9uZW50czogIiwgbkNvbXBzKSkKfSBlbHNlIHsKICBuQ29tcHMgPC0gc3BlY3RyYXRyYWl0OjpmaW5kX29wdGltYWxfY29tcG9uZW50cyhkYXRhc2V0PWNhbC5wbHNyLmRhdGEsIHRhcmdldFZhcmlhYmxlPWluVmFyLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1ldGhvZD1tZXRob2QsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1heENvbXBzPW1heENvbXBzLCBpdGVyYXRpb25zPWl0ZXJhdGlvbnMsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNlZz1zZWcsIHByb3A9cHJvcCwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmFuZG9tX3NlZWQ9cmFuZG9tX3NlZWQpCn0KZGV2LmNvcHkocG5nLGZpbGUucGF0aChvdXRkaXIscGFzdGUwKHBhc3RlMChpblZhciwiX1BMU1JfQ29tcG9uZW50X1NlbGVjdGlvbi5wbmciKSkpLCAKICAgICAgICAgaGVpZ2h0PTI4MDAsIHdpZHRoPTM0MDAsICByZXM9MzQwKQpkZXYub2ZmKCk7CmBgYAoKIyMjIEZpdCBmaW5hbCBtb2RlbApgYGB7ciwgZmlnLmhlaWdodCA9IDUsIGZpZy53aWR0aCA9IDEyLCBlY2hvPVRSVUV9CnBsc3Iub3V0IDwtIHBsc3IoYXMuZm9ybXVsYShwYXN0ZShpblZhciwifiIsIlNwZWN0cmEiKSksc2NhbGU9RkFMU0UsbmNvbXA9bkNvbXBzLHZhbGlkYXRpb249IkxPTyIsCiAgICAgICAgICAgICAgICAgdHJhY2U9RkFMU0UsZGF0YT1jYWwucGxzci5kYXRhKQpmaXQgPC0gcGxzci5vdXQkZml0dGVkLnZhbHVlc1ssMSxuQ29tcHNdCnBscy5vcHRpb25zKHBhcmFsbGVsID0gTlVMTCkKCiMgRXh0ZXJuYWwgdmFsaWRhdGlvbiBmaXQgc3RhdHMKcGFyKG1mcm93PWMoMSwyKSkgIyBCLCBMLCBULCBSCnBsczo6Uk1TRVAocGxzci5vdXQsIG5ld2RhdGEgPSB2YWwucGxzci5kYXRhKQpwbG90KHBsczo6Uk1TRVAocGxzci5vdXQsZXN0aW1hdGU9YygidGVzdCIpLG5ld2RhdGEgPSB2YWwucGxzci5kYXRhKSwgbWFpbj0iTU9ERUwgUk1TRVAiLAogICAgIHhsYWI9Ik51bWJlciBvZiBDb21wb25lbnRzIix5bGFiPSJNb2RlbCBWYWxpZGF0aW9uIFJNU0VQIixsdHk9MSxjb2w9ImJsYWNrIixjZXg9MS41LGx3ZD0yKQpib3gobHdkPTIuMikKClIyKHBsc3Iub3V0LCBuZXdkYXRhID0gdmFsLnBsc3IuZGF0YSkKcGxvdChwbHM6OlIyKHBsc3Iub3V0LGVzdGltYXRlPWMoInRlc3QiKSxuZXdkYXRhID0gdmFsLnBsc3IuZGF0YSksIG1haW49Ik1PREVMIFIyIiwKICAgICB4bGFiPSJOdW1iZXIgb2YgQ29tcG9uZW50cyIseWxhYj0iTW9kZWwgVmFsaWRhdGlvbiBSMiIsbHR5PTEsY29sPSJibGFjayIsY2V4PTEuNSxsd2Q9MikKYm94KGx3ZD0yLjIpCmRldi5jb3B5KHBuZyxmaWxlLnBhdGgob3V0ZGlyLHBhc3RlMChwYXN0ZTAoaW5WYXIsIl9WYWxpZGF0aW9uX1JNU0VQX1IyX2J5X0NvbXBvbmVudC5wbmciKSkpLCAKICAgICAgICAgaGVpZ2h0PTI4MDAsIHdpZHRoPTQ4MDAsICByZXM9MzQwKQpkZXYub2ZmKCk7CnBhcihvcGFyKQpgYGAKCiMjIyBQTFNSIGZpdCBvYnNlcnZlZCB2cy4gcHJlZGljdGVkIHBsb3QgZGF0YQpgYGB7ciwgZmlnLmhlaWdodCA9IDE1LCBmaWcud2lkdGggPSAxNSwgZWNobz1UUlVFfSAKI2NhbGlicmF0aW9uCmNhbC5wbHNyLm91dHB1dCA8LSBkYXRhLmZyYW1lKGNhbC5wbHNyLmRhdGFbLCB3aGljaChuYW1lcyhjYWwucGxzci5kYXRhKSAlbm90aW4lICJTcGVjdHJhIildLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgUExTUl9QcmVkaWN0ZWQ9Zml0LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBQTFNSX0NWX1ByZWRpY3RlZD1hcy52ZWN0b3IocGxzci5vdXQkdmFsaWRhdGlvbiRwcmVkWywsbkNvbXBzXSkpCmNhbC5wbHNyLm91dHB1dCA8LSBjYWwucGxzci5vdXRwdXQgJT4lCiAgbXV0YXRlKFBMU1JfQ1ZfUmVzaWR1YWxzID0gUExTUl9DVl9QcmVkaWN0ZWQtZ2V0KGluVmFyKSkKaGVhZChjYWwucGxzci5vdXRwdXQpCmNhbC5SMiA8LSByb3VuZChwbHM6OlIyKHBsc3Iub3V0LGludGVyY2VwdD1GKVtbMV1dW25Db21wc10sMikKY2FsLlJNU0VQIDwtIHJvdW5kKHNxcnQobWVhbihjYWwucGxzci5vdXRwdXQkUExTUl9DVl9SZXNpZHVhbHNeMikpLDIpCgp2YWwucGxzci5vdXRwdXQgPC0gZGF0YS5mcmFtZSh2YWwucGxzci5kYXRhWywgd2hpY2gobmFtZXModmFsLnBsc3IuZGF0YSkgJW5vdGluJSAiU3BlY3RyYSIpXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgUExTUl9QcmVkaWN0ZWQ9YXMudmVjdG9yKHByZWRpY3QocGxzci5vdXQsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuZXdkYXRhID0gdmFsLnBsc3IuZGF0YSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG5jb21wPW5Db21wcywgdHlwZT0icmVzcG9uc2UiKVssLDFdKSkKdmFsLnBsc3Iub3V0cHV0IDwtIHZhbC5wbHNyLm91dHB1dCAlPiUKICBtdXRhdGUoUExTUl9SZXNpZHVhbHMgPSBQTFNSX1ByZWRpY3RlZC1nZXQoaW5WYXIpKQpoZWFkKHZhbC5wbHNyLm91dHB1dCkKdmFsLlIyIDwtIHJvdW5kKHBsczo6UjIocGxzci5vdXQsbmV3ZGF0YT12YWwucGxzci5kYXRhLGludGVyY2VwdD1GKVtbMV1dW25Db21wc10sMikKdmFsLlJNU0VQIDwtIHJvdW5kKHNxcnQobWVhbih2YWwucGxzci5vdXRwdXQkUExTUl9SZXNpZHVhbHNeMikpLDIpCgpybmdfcXVhbnQgPC0gcXVhbnRpbGUoY2FsLnBsc3Iub3V0cHV0WyxpblZhcl0sIHByb2JzID0gYygwLjAwMSwgMC45OTkpKQpjYWxfc2NhdHRlcl9wbG90IDwtIGdncGxvdChjYWwucGxzci5vdXRwdXQsIGFlcyh4PVBMU1JfQ1ZfUHJlZGljdGVkLCB5PWdldChpblZhcikpKSArIAogIHRoZW1lX2J3KCkgKyBnZW9tX3BvaW50KCkgKyBnZW9tX2FibGluZShpbnRlcmNlcHQgPSAwLCBzbG9wZSA9IDEsIGNvbG9yPSJkYXJrIGdyZXkiLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbGluZXR5cGU9ImRhc2hlZCIsIHNpemU9MS41KSArIHhsaW0ocm5nX3F1YW50WzFdLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcm5nX3F1YW50WzJdKSArIAogIHlsaW0ocm5nX3F1YW50WzFdLCBybmdfcXVhbnRbMl0pICsKICBsYWJzKHg9cGFzdGUwKCJQcmVkaWN0ZWQgIiwgcGFzdGUoaW5WYXIpLCAiICh1bml0cykiKSwKICAgICAgIHk9cGFzdGUwKCJPYnNlcnZlZCAiLCBwYXN0ZShpblZhciksICIgKHVuaXRzKSIpLAogICAgICAgdGl0bGU9cGFzdGUwKCJDYWxpYnJhdGlvbjogIiwgcGFzdGUwKCJSc3EgPSAiLCBjYWwuUjIpLCAiOyAiLCBwYXN0ZTAoIlJNU0VQID0gIiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjYWwuUk1TRVApKSkgKwogIHRoZW1lKGF4aXMudGV4dD1lbGVtZW50X3RleHQoc2l6ZT0xOCksIGxlZ2VuZC5wb3NpdGlvbj0ibm9uZSIsCiAgICAgICAgYXhpcy50aXRsZT1lbGVtZW50X3RleHQoc2l6ZT0yMCwgZmFjZT0iYm9sZCIpLCAKICAgICAgICBheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChhbmdsZSA9IDAsdmp1c3QgPSAwLjUpLAogICAgICAgIHBhbmVsLmJvcmRlciA9IGVsZW1lbnRfcmVjdChsaW5ldHlwZSA9ICJzb2xpZCIsIGZpbGwgPSBOQSwgc2l6ZT0xLjUpKQoKY2FsX3Jlc2lkX2hpc3RvZ3JhbSA8LSBnZ3Bsb3QoY2FsLnBsc3Iub3V0cHV0LCBhZXMoeD1QTFNSX0NWX1Jlc2lkdWFscykpICsKICBnZW9tX2hpc3RvZ3JhbShhbHBoYT0uNSwgcG9zaXRpb249ImlkZW50aXR5IikgKyAKICBnZW9tX3ZsaW5lKHhpbnRlcmNlcHQgPSAwLCBjb2xvcj0iYmxhY2siLCAKICAgICAgICAgICAgIGxpbmV0eXBlPSJkYXNoZWQiLCBzaXplPTEpICsgdGhlbWVfYncoKSArIAogIHRoZW1lKGF4aXMudGV4dD1lbGVtZW50X3RleHQoc2l6ZT0xOCksIGxlZ2VuZC5wb3NpdGlvbj0ibm9uZSIsCiAgICAgICAgYXhpcy50aXRsZT1lbGVtZW50X3RleHQoc2l6ZT0yMCwgZmFjZT0iYm9sZCIpLCAKICAgICAgICBheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChhbmdsZSA9IDAsdmp1c3QgPSAwLjUpLAogICAgICAgIHBhbmVsLmJvcmRlciA9IGVsZW1lbnRfcmVjdChsaW5ldHlwZSA9ICJzb2xpZCIsIGZpbGwgPSBOQSwgc2l6ZT0xLjUpKQoKcm5nX3F1YW50IDwtIHF1YW50aWxlKHZhbC5wbHNyLm91dHB1dFssaW5WYXJdLCBwcm9icyA9IGMoMC4wMDEsIDAuOTk5KSkKdmFsX3NjYXR0ZXJfcGxvdCA8LSBnZ3Bsb3QodmFsLnBsc3Iub3V0cHV0LCBhZXMoeD1QTFNSX1ByZWRpY3RlZCwgeT1nZXQoaW5WYXIpKSkgKyAKICB0aGVtZV9idygpICsgZ2VvbV9wb2ludCgpICsgZ2VvbV9hYmxpbmUoaW50ZXJjZXB0ID0gMCwgc2xvcGUgPSAxLCBjb2xvcj0iZGFyayBncmV5IiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGxpbmV0eXBlPSJkYXNoZWQiLCBzaXplPTEuNSkgKyB4bGltKHJuZ19xdWFudFsxXSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJuZ19xdWFudFsyXSkgKyAKICB5bGltKHJuZ19xdWFudFsxXSwgcm5nX3F1YW50WzJdKSArCiAgbGFicyh4PXBhc3RlMCgiUHJlZGljdGVkICIsIHBhc3RlKGluVmFyKSwgIiAodW5pdHMpIiksCiAgICAgICB5PXBhc3RlMCgiT2JzZXJ2ZWQgIiwgcGFzdGUoaW5WYXIpLCAiICh1bml0cykiKSwKICAgICAgIHRpdGxlPXBhc3RlMCgiVmFsaWRhdGlvbjogIiwgcGFzdGUwKCJSc3EgPSAiLCB2YWwuUjIpLCAiOyAiLCBwYXN0ZTAoIlJNU0VQID0gIiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHZhbC5STVNFUCkpKSArCiAgdGhlbWUoYXhpcy50ZXh0PWVsZW1lbnRfdGV4dChzaXplPTE4KSwgbGVnZW5kLnBvc2l0aW9uPSJub25lIiwKICAgICAgICBheGlzLnRpdGxlPWVsZW1lbnRfdGV4dChzaXplPTIwLCBmYWNlPSJib2xkIiksIAogICAgICAgIGF4aXMudGV4dC54ID0gZWxlbWVudF90ZXh0KGFuZ2xlID0gMCx2anVzdCA9IDAuNSksCiAgICAgICAgcGFuZWwuYm9yZGVyID0gZWxlbWVudF9yZWN0KGxpbmV0eXBlID0gInNvbGlkIiwgZmlsbCA9IE5BLCBzaXplPTEuNSkpCgp2YWxfcmVzaWRfaGlzdG9ncmFtIDwtIGdncGxvdCh2YWwucGxzci5vdXRwdXQsIGFlcyh4PVBMU1JfUmVzaWR1YWxzKSkgKwogIGdlb21faGlzdG9ncmFtKGFscGhhPS41LCBwb3NpdGlvbj0iaWRlbnRpdHkiKSArIAogIGdlb21fdmxpbmUoeGludGVyY2VwdCA9IDAsIGNvbG9yPSJibGFjayIsIAogICAgICAgICAgICAgbGluZXR5cGU9ImRhc2hlZCIsIHNpemU9MSkgKyB0aGVtZV9idygpICsgCiAgdGhlbWUoYXhpcy50ZXh0PWVsZW1lbnRfdGV4dChzaXplPTE4KSwgbGVnZW5kLnBvc2l0aW9uPSJub25lIiwKICAgICAgICBheGlzLnRpdGxlPWVsZW1lbnRfdGV4dChzaXplPTIwLCBmYWNlPSJib2xkIiksIAogICAgICAgIGF4aXMudGV4dC54ID0gZWxlbWVudF90ZXh0KGFuZ2xlID0gMCx2anVzdCA9IDAuNSksCiAgICAgICAgcGFuZWwuYm9yZGVyID0gZWxlbWVudF9yZWN0KGxpbmV0eXBlID0gInNvbGlkIiwgZmlsbCA9IE5BLCBzaXplPTEuNSkpCgojIHBsb3QgY2FsL3ZhbCBzaWRlLWJ5LXNpZGUKc2NhdHRlcnBsb3RzIDwtIGdyaWQuYXJyYW5nZShjYWxfc2NhdHRlcl9wbG90LCB2YWxfc2NhdHRlcl9wbG90LCBjYWxfcmVzaWRfaGlzdG9ncmFtLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICB2YWxfcmVzaWRfaGlzdG9ncmFtLCBucm93PTIsbmNvbD0yKQpnZ3NhdmUoZmlsZW5hbWUgPSBmaWxlLnBhdGgob3V0ZGlyLHBhc3RlMChpblZhciwiX0NhbF9WYWxfU2NhdHRlcnBsb3RzLnBuZyIpKSwgCiAgICAgICBwbG90ID0gc2NhdHRlcnBsb3RzLCBkZXZpY2U9InBuZyIsIHdpZHRoID0gMzIsIGhlaWdodCA9IDMwLCB1bml0cyA9ICJjbSIsCiAgICAgICBkcGkgPSAzMDApCmBgYAoKIyMjIEdlbmVyYXRlIENvZWZmaWNpZW50IGFuZCBWSVAgcGxvdHMKYGBge3IsIGZpZy5oZWlnaHQgPSA5LCBmaWcud2lkdGggPSAxMCwgZWNobz1UUlVFfQp2aXBzIDwtIHNwZWN0cmF0cmFpdDo6VklQKHBsc3Iub3V0KVtuQ29tcHMsXQoKcGFyKG1mcm93PWMoMiwxKSkKcGxvdChwbHNyLm91dCRjb2VmZmljaWVudHNbLCxuQ29tcHNdLCB4PXd2LHhsYWI9IldhdmVsZW5ndGggKG5tKSIsCiAgICAgeWxhYj0iUmVncmVzc2lvbiBjb2VmZmljaWVudHMiLGx3ZD0yLHR5cGU9J2wnKQpib3gobHdkPTIuMikKcGxvdCh3diwgdmlwcywgeGxhYj0iV2F2ZWxlbmd0aCAobm0pIix5bGFiPSJWSVAiLGNleD0wLjAxKQpsaW5lcyh3diwgdmlwcywgbHdkPTMpCmFibGluZShoPTAuOCwgbHR5PTIsIGNvbD0iZGFyayBncmV5IikKYm94KGx3ZD0yLjIpCmRldi5jb3B5KHBuZywgZmlsZS5wYXRoKG91dGRpciwgcGFzdGUwKGluVmFyLCdfQ29lZmZpY2llbnRfVklQX3Bsb3QucG5nJykpLCAKICAgICAgICAgaGVpZ2h0PTMxMDAsIHdpZHRoPTQxMDAsIHJlcz0zNDApCmRldi5vZmYoKTsKcGFyKG9wYXIpCmBgYAoKIyMjIEJvb3RzdHJhcCB2YWxpZGF0aW9uCmBgYHtyLCBlY2hvPUZBTFNFfQppZihncmVwbCgiV2luZG93cyIsIHNlc3Npb25JbmZvKCkkcnVubmluZykpewogIHBscy5vcHRpb25zKHBhcmFsbGVsID1OVUxMKQp9IGVsc2UgewogIHBscy5vcHRpb25zKHBhcmFsbGVsID0gcGFyYWxsZWw6OmRldGVjdENvcmVzKCktMSkKfQoKIyMjIFBMU1IgYm9vdHN0cmFwIHBlcm11dGF0aW9uIHVuY2VydGFpbnR5IGFuYWx5c2lzCml0ZXJhdGlvbnMgPC0gNTAwICAgICMgaG93IG1hbnkgcGVybXV0YXRpb24gaXRlcmF0aW9ucyB0byBydW4KcHJvcCA8LSAwLjcwICAgICAgICAgICMgZnJhY3Rpb24gb2YgdHJhaW5pbmcgZGF0YSB0byBrZWVwIGZvciBlYWNoIGl0ZXJhdGlvbgpwbHNyX3Blcm11dGF0aW9uIDwtIHNwZWN0cmF0cmFpdDo6cGxzX3Blcm11dGF0aW9uKGRhdGFzZXQ9Y2FsLnBsc3IuZGF0YSwgdGFyZ2V0VmFyaWFibGU9aW5WYXIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbWF4Q29tcHM9bkNvbXBzLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBpdGVyYXRpb25zPWl0ZXJhdGlvbnMsIHByb3A9cHJvcCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB2ZXJib3NlID0gRkFMU0UpCmJvb3RzdHJhcF9pbnRlcmNlcHQgPC0gcGxzcl9wZXJtdXRhdGlvbiRjb2VmX2FycmF5WzEsLG5Db21wc10KYm9vdHN0cmFwX2NvZWYgPC0gcGxzcl9wZXJtdXRhdGlvbiRjb2VmX2FycmF5WzI6bGVuZ3RoKHBsc3JfcGVybXV0YXRpb24kY29lZl9hcnJheVssMSxuQ29tcHNdKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICxuQ29tcHNdCnJtKHBsc3JfcGVybXV0YXRpb24pCgojIGFwcGx5IGNvZWZmaWNpZW50cyB0byBsZWZ0LW91dCB2YWxpZGF0aW9uIGRhdGEKaW50ZXJ2YWwgPC0gYygwLjAyNSwwLjk3NSkKQm9vdHN0cmFwX1ByZWQgPC0gdmFsLnBsc3IuZGF0YSRTcGVjdHJhICUqJSBib290c3RyYXBfY29lZiArIAogIG1hdHJpeChyZXAoYm9vdHN0cmFwX2ludGVyY2VwdCwgbGVuZ3RoKHZhbC5wbHNyLmRhdGFbLGluVmFyXSkpLCBieXJvdz1UUlVFLCAKICAgICAgICAgbmNvbD1sZW5ndGgoYm9vdHN0cmFwX2ludGVyY2VwdCkpCkludGVydmFsX0NvbmYgPC0gYXBwbHkoWCA9IEJvb3RzdHJhcF9QcmVkLCBNQVJHSU4gPSAxLCBGVU4gPSBxdWFudGlsZSwgCiAgICAgICAgICAgICAgICAgICAgICAgcHJvYnM9YyhpbnRlcnZhbFsxXSwgaW50ZXJ2YWxbMl0pKQpzZF9tZWFuIDwtIGFwcGx5KFggPSBCb290c3RyYXBfUHJlZCwgTUFSR0lOID0gMSwgRlVOID0gc2QpCnNkX3JlcyA8LSBzZCh2YWwucGxzci5vdXRwdXQkUExTUl9SZXNpZHVhbHMpCnNkX3RvdCA8LSBzcXJ0KHNkX21lYW5eMitzZF9yZXNeMikKdmFsLnBsc3Iub3V0cHV0JExDSSA8LSBJbnRlcnZhbF9Db25mWzEsXQp2YWwucGxzci5vdXRwdXQkVUNJIDwtIEludGVydmFsX0NvbmZbMixdCnZhbC5wbHNyLm91dHB1dCRMUEkgPC0gdmFsLnBsc3Iub3V0cHV0JFBMU1JfUHJlZGljdGVkLTEuOTYqc2RfdG90CnZhbC5wbHNyLm91dHB1dCRVUEkgPC0gdmFsLnBsc3Iub3V0cHV0JFBMU1JfUHJlZGljdGVkKzEuOTYqc2RfdG90CmhlYWQodmFsLnBsc3Iub3V0cHV0KQpgYGAKCiMjIyBKYWNra25pZmUgY29lZmZpY2llbnQgcGxvdApgYGB7ciwgZmlnLmhlaWdodCA9IDYsIGZpZy53aWR0aCA9IDEwLCBlY2hvPVRSVUV9CnNwZWN0cmF0cmFpdDo6Zi5wbG90LmNvZWYoWiA9IHQoYm9vdHN0cmFwX2NvZWYpLCB3diA9IHd2LCAKICAgICAgICAgICAgICAgICAgICAgICAgICBwbG90X2xhYmVsPSJCb290c3RyYXAgcmVncmVzc2lvbiBjb2VmZmljaWVudHMiLCAKICAgICAgICAgICAgICAgICAgICAgICAgICBwb3NpdGlvbiA9ICdib3R0b21sZWZ0JykKYWJsaW5lKGg9MCxsdHk9Mixjb2w9ImdyZXk1MCIpCmJveChsd2Q9Mi4yKQpkZXYuY29weShwbmcsZmlsZS5wYXRoKG91dGRpcixwYXN0ZTAoaW5WYXIsJ19Cb290c3RyYXBfUmVncmVzc2lvbl9Db2VmZmljaWVudHMucG5nJykpLCAKICAgICAgICAgaGVpZ2h0PTIxMDAsIHdpZHRoPTM4MDAsIHJlcz0zNDApCmRldi5vZmYoKTsKYGBgCgoKIyMjIEJvb3RzdHJhcCB2YWxpZGF0aW9uIHBsb3QKYGBge3IsIGZpZy5oZWlnaHQgPSA3LCBmaWcud2lkdGggPSA4LCBlY2hvPVRSVUV9CnJtc2VwX3BlcmNybXNlcCA8LSBzcGVjdHJhdHJhaXQ6OnBlcmNlbnRfcm1zZShwbHNyX2RhdGFzZXQgPSB2YWwucGxzci5vdXRwdXQsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaW5WYXIgPSBpblZhciwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXNpZHVhbHMgPSB2YWwucGxzci5vdXRwdXQkUExTUl9SZXNpZHVhbHMsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmFuZ2U9ImZ1bGwiKQpSTVNFUCA8LSBybXNlcF9wZXJjcm1zZXAkcm1zZQpwZXJjX1JNU0VQIDwtIHJtc2VwX3BlcmNybXNlcCRwZXJjX3Jtc2UKcjIgPC0gcm91bmQocGxzOjpSMihwbHNyLm91dCwgbmV3ZGF0YSA9IHZhbC5wbHNyLmRhdGEsIGludGVyY2VwdD1GKSR2YWxbbkNvbXBzXSwyKQpleHByIDwtIHZlY3RvcigiZXhwcmVzc2lvbiIsIDMpCmV4cHJbWzFdXSA8LSBicXVvdGUoUl4yPT0uKHIyKSkKZXhwcltbMl1dIDwtIGJxdW90ZShSTVNFUD09Lihyb3VuZChSTVNFUCwyKSkpCmV4cHJbWzNdXSA8LSBicXVvdGUoIiVSTVNFUCI9PS4ocm91bmQocGVyY19STVNFUCwyKSkpCnJuZ192YWxzIDwtIGMobWluKHZhbC5wbHNyLm91dHB1dCRMUEkpLCBtYXgodmFsLnBsc3Iub3V0cHV0JFVQSSkpCnBhcihtZnJvdz1jKDEsMSksIG1hcj1jKDQuMiw1LjMsMSwwLjQpLCBvbWE9YygwLCAwLjEsIDAsIDAuMikpCnBsb3RyaXg6OnBsb3RDSSh2YWwucGxzci5vdXRwdXQkUExTUl9QcmVkaWN0ZWQsdmFsLnBsc3Iub3V0cHV0WyxpblZhcl0sIAogICAgICAgICAgICAgICAgbGk9dmFsLnBsc3Iub3V0cHV0JExQSSwgdWk9dmFsLnBsc3Iub3V0cHV0JFVQSSwgZ2FwPTAuMDA5LHNmcmFjPTAuMDAwLCAKICAgICAgICAgICAgICAgIGx3ZD0xLjYsIHhsaW09YyhybmdfdmFsc1sxXSwgcm5nX3ZhbHNbMl0pLCB5bGltPWMocm5nX3ZhbHNbMV0sIHJuZ192YWxzWzJdKSwgCiAgICAgICAgICAgICAgICBlcnI9IngiLCBwY2g9MjEsIGNvbD0iYmxhY2siLCBwdC5iZz1zY2FsZXM6OmFscGhhKCJncmV5NzAiLDAuNyksIHNjb2w9ImdyZXk4MCIsCiAgICAgICAgICAgICAgICBjZXg9MiwgeGxhYj1wYXN0ZTAoIlByZWRpY3RlZCAiLCBwYXN0ZShpblZhciksICIgKHVuaXRzKSIpLAogICAgICAgICAgICAgICAgeWxhYj1wYXN0ZTAoIk9ic2VydmVkICIsIHBhc3RlKGluVmFyKSwgIiAodW5pdHMpIiksCiAgICAgICAgICAgICAgICBjZXguYXhpcz0xLjUsY2V4LmxhYj0xLjgpCmFibGluZSgwLDEsbHR5PTIsbHc9MikKcGxvdHJpeDo6cGxvdENJKHZhbC5wbHNyLm91dHB1dCRQTFNSX1ByZWRpY3RlZCx2YWwucGxzci5vdXRwdXRbLGluVmFyXSwgCiAgICAgICAgICAgICAgICBsaT12YWwucGxzci5vdXRwdXQkTENJLCB1aT12YWwucGxzci5vdXRwdXQkVUNJLCBnYXA9MC4wMDksc2ZyYWM9MC4wMDQsIAogICAgICAgICAgICAgICAgbHdkPTEuNiwgeGxpbT1jKHJuZ192YWxzWzFdLCBybmdfdmFsc1syXSksIHlsaW09YyhybmdfdmFsc1sxXSwgcm5nX3ZhbHNbMl0pLCAKICAgICAgICAgICAgICAgIGVycj0ieCIsIHBjaD0yMSwgY29sPSJibGFjayIsIHB0LmJnPXNjYWxlczo6YWxwaGEoImdyZXk3MCIsMC43KSwgc2NvbD0iYmxhY2siLAogICAgICAgICAgICAgICAgY2V4PTIsIHhsYWI9cGFzdGUwKCJQcmVkaWN0ZWQgIiwgcGFzdGUoaW5WYXIpLCAiICh1bml0cykiKSwKICAgICAgICAgICAgICAgIHlsYWI9cGFzdGUwKCJPYnNlcnZlZCAiLCBwYXN0ZShpblZhciksICIgKHVuaXRzKSIpLAogICAgICAgICAgICAgICAgY2V4LmF4aXM9MS41LGNleC5sYWI9MS44LCBhZGQ9VCkKbGVnZW5kKCJ0b3BsZWZ0IiwgbGVnZW5kPWV4cHIsIGJ0eT0ibiIsIGNleD0xLjUpCmxlZ2VuZCgiYm90dG9tcmlnaHQiLCBsZWdlbmQ9YygiUHJlZGljdGlvbiBJbnRlcnZhbCIsIkNvbmZpZGVuY2UgSW50ZXJ2YWwiKSwgCiAgICAgICBsdHk9YygxLDEpLCBjb2wgPSBjKCJncmV5ODAiLCJibGFjayIpLCBsd2Q9MywgYnR5PSJuIiwgY2V4PTEuNSkKYm94KGx3ZD0yLjIpCmRldi5jb3B5KHBuZyxmaWxlLnBhdGgob3V0ZGlyLHBhc3RlMChpblZhciwiX1BMU1JfVmFsaWRhdGlvbl9TY2F0dGVycGxvdC5wbmciKSksIAogICAgICAgICBoZWlnaHQ9MjgwMCwgd2lkdGg9MzIwMCwgIHJlcz0zNDApCmRldi5vZmYoKTsKYGBgCgojIyMgT3V0cHV0IGJvb3RzdHJhcCByZXN1bHRzCmBgYHtyLCBlY2hvPVRSVUV9Cm91dC5qay5jb2VmcyA8LSBkYXRhLmZyYW1lKEl0ZXJhdGlvbj1zZXEoMSxsZW5ndGgoYm9vdHN0cmFwX2ludGVyY2VwdCksMSksCiAgICAgICAgICAgICAgICAgICAgICAgICAgIEludGVyY2VwdD1ib290c3RyYXBfaW50ZXJjZXB0LHQoYm9vdHN0cmFwX2NvZWYpKQpuYW1lcyhvdXQuamsuY29lZnMpIDwtIGMoIkl0ZXJhdGlvbiIsIkludGVyY2VwdCIscGFzdGUwKCJXYXZlXyIsd3YpKQpoZWFkKG91dC5qay5jb2VmcylbMTo2XQp3cml0ZS5jc3Yob3V0LmprLmNvZWZzLGZpbGU9ZmlsZS5wYXRoKG91dGRpcixwYXN0ZTAoaW5WYXIsJ19Cb290c3RyYXBfUExTUl9Db2VmZmljaWVudHMuY3N2JykpLAogICAgICAgICAgcm93Lm5hbWVzPUZBTFNFKQpgYGAKCiMjIyBDcmVhdGUgY29yZSBQTFNSIG91dHB1dHMKYGBge3IsIGVjaG89VFJVRX0KcHJpbnQocGFzdGUoIk91dHB1dCBkaXJlY3Rvcnk6ICIsIGdldHdkKCkpKQoKIyBPYnNlcnZlZCB2ZXJzdXMgcHJlZGljdGVkCndyaXRlLmNzdihjYWwucGxzci5vdXRwdXQsZmlsZT1maWxlLnBhdGgob3V0ZGlyLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBhc3RlMChpblZhciwnX09ic2VydmVkX1BMU1JfQ1ZfUHJlZF8nLG5Db21wcywKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJ2NvbXAuY3N2JykpLHJvdy5uYW1lcz1GQUxTRSkKCiMgVmFsaWRhdGlvbiBkYXRhCndyaXRlLmNzdih2YWwucGxzci5vdXRwdXQsZmlsZT1maWxlLnBhdGgob3V0ZGlyLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBhc3RlMChpblZhciwnX1ZhbGlkYXRpb25fUExTUl9QcmVkXycsbkNvbXBzLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAnY29tcC5jc3YnKSkscm93Lm5hbWVzPUZBTFNFKQoKIyBNb2RlbCBjb2VmZmljaWVudHMKY29lZnMgPC0gY29lZihwbHNyLm91dCxuY29tcD1uQ29tcHMsaW50ZXJjZXB0PVRSVUUpCndyaXRlLmNzdihjb2VmcyxmaWxlPWZpbGUucGF0aChvdXRkaXIscGFzdGUwKGluVmFyLCdfUExTUl9Db2VmZmljaWVudHNfJywKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbkNvbXBzLCdjb21wLmNzdicpKSwKICAgICAgICAgIHJvdy5uYW1lcz1UUlVFKQoKIyBQTFNSIFZJUAp3cml0ZS5jc3YodmlwcyxmaWxlPWZpbGUucGF0aChvdXRkaXIscGFzdGUwKGluVmFyLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICdfUExTUl9WSVBzXycsbkNvbXBzLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICdjb21wLmNzdicpKSkKYGBgCgojIyMgQ29uZmlybSBmaWxlcyB3ZXJlIHdyaXR0ZW4gdG8gdGVtcCBzcGFjZQpgYGB7ciwgZWNobz1UUlVFfQpwcmludCgiKioqKiBQTFNSIG91dHB1dCBmaWxlczogIikKcHJpbnQobGlzdC5maWxlcyhvdXRkaXIpW2dyZXAocGF0dGVybiA9IGluVmFyLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGxpc3QuZmlsZXMob3V0ZGlyKSldKQoKYGBg