1. Setup

rm( list = ls() )
require( ggplot2 )
require( Matrix )
require( corrplot )
# load an tidy data
d <- read.csv( "Data_prediction_FEP.csv")
N <- nrow( d )
# remove the ID column
d <- d[ , -1 ]
str( d )
'data.frame':   77 obs. of  19 variables:
 $ age         : int  22 24 20 23 28 25 19 37 21 20 ...
 $ gender      : int  2 2 1 2 2 1 1 1 2 1 ...
 $ iq          : int  116 93 98 90 102 90 89 79 102 133 ...
 $ PANSS_pos   : int  17 20 14 11 13 18 23 15 13 9 ...
 $ PANSS_neg   : int  20 26 9 16 18 22 23 20 8 8 ...
 $ PANSS_gen   : int  36 35 24 29 30 40 41 37 29 22 ...
 $ PANSS_total : int  73 81 47 56 61 80 87 72 50 39 ...
 $ Glu.Cre     : num  1.31 1.13 1.07 1.44 1.18 ...
 $ MI.Cre      : num  0.743 0.635 0.698 0.708 0.713 0.62 0.731 0.752 0.627 0.711 ...
 $ NAA.Cre     : num  1.22 1.06 1.14 1.49 1.33 ...
 $ GPC.Pch_Cre : num  0.228 0.182 0.231 0.252 0.248 0.266 0.213 0.303 0.26 0.252 ...
 $ NAA.NAAG_Cre: num  1.28 1.1 1.25 1.49 1.33 ...
 $ BA8         : num  -6.07 -5.85 -66.47 19.48 33.93 ...
 $ Caudaate    : num  -42.15 39.86 40.09 5.29 -23.81 ...
 $ PAC         : num  -87.46 5.25 -77.49 -59.63 -62.94 ...
 $ lgi_parietal: num  3.21 3.33 3.62 3.74 3.29 ...
 $ lgi_occ     : num  4.35 3.92 4.5 4.76 4.08 ...
 $ lgi_dlpfc   : num  2.54 2.45 2.68 2.83 2.66 ...
 $ group       : int  1 1 0 0 0 0 1 0 0 0 ...

Gender is coded numerically, as 1 and 2. This means it needs dummy coding, but as only two levels, we can hack it to be 0/1 and keep it as a numerical predictor for a preliminary linear model / GLM

d$gender <- d$gender - 1

2. Inferential / Association Analysis

\(Y = \{ 0,1 \}\), so clearly a binomial, which suggests a logistic regression is the most basic model for this kind of data. Include all variables (so no regularisation / online feature selection) and let’s just see what it shows.

# independent (predictor) variables
x.vars <- names( d )[1:(length(names(d))-1)]
# dependent (outcome, response) variables
y.var  <- names( d )[length(names(d))]
# build formula object for the glm
(modelFormula <- as.formula( paste(y.var, paste(x.vars, collapse=" + "), sep=" ~ ") ) )
group ~ age + gender + iq + PANSS_pos + PANSS_neg + PANSS_gen + 
    PANSS_total + Glu.Cre + MI.Cre + NAA.Cre + GPC.Pch_Cre + 
    NAA.NAAG_Cre + BA8 + Caudaate + PAC + lgi_parietal + lgi_occ + 
    lgi_dlpfc

Now, fit the GLM with all the data available – just a classic inferential analysis

lr.fit <- glm( modelFormula, family = binomial, data = d )
glm.fit: algorithm did not convergeglm.fit: fitted probabilities numerically 0 or 1 occurred
summary(lr.fit)

Call:
glm(formula = modelFormula, family = binomial, data = d)

Deviance Residuals: 
       Min          1Q      Median          3Q         Max  
-3.227e-05  -2.100e-08  -2.100e-08   2.100e-08   3.071e-05  

Coefficients: (1 not defined because of singularities)
               Estimate Std. Error z value Pr(>|z|)
(Intercept)  -1.570e+03  1.672e+06  -0.001    0.999
age           9.241e+00  7.278e+03   0.001    0.999
gender        1.051e+02  9.432e+04   0.001    0.999
iq            3.106e+00  2.182e+03   0.001    0.999
PANSS_pos     2.986e+01  1.627e+04   0.002    0.999
PANSS_neg     6.640e+00  1.085e+04   0.001    1.000
PANSS_gen    -5.542e+00  1.142e+04   0.000    1.000
PANSS_total          NA         NA      NA       NA
Glu.Cre       1.193e+02  1.858e+05   0.001    0.999
MI.Cre       -2.623e+02  3.062e+05  -0.001    0.999
NAA.Cre       1.105e+02  6.476e+05   0.000    1.000
GPC.Pch_Cre   6.366e+01  1.118e+06   0.000    1.000
NAA.NAAG_Cre -2.870e+02  5.809e+05   0.000    1.000
BA8          -5.172e-01  4.407e+02  -0.001    0.999
Caudaate      2.436e-02  5.769e+02   0.000    1.000
PAC          -1.332e-01  6.447e+02   0.000    1.000
lgi_parietal  3.222e+02  6.336e+05   0.001    1.000
lgi_occ      -5.592e+00  8.162e+04   0.000    1.000
lgi_dlpfc    -9.581e+01  3.562e+05   0.000    1.000

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 8.8209e+01  on 76  degrees of freedom
Residual deviance: 7.9491e-09  on 59  degrees of freedom
AIC: 36

Number of Fisher Scoring iterations: 25

Something suggesing rank deficiency in the design matrix – likely that the predictors are not linearly-independent or one of them is perfectly associated with the response variable group. summary( lr.fit ) suggests PANSS_total is problematic, but there might be others, so look at correlations:

# explore ?highly correlated variables
corr.d <- cor( d )
corr.d[ lower.tri( corr.d, diag = TRUE ) ] <- NA
corrplot( corr.d, type = "upper", diag = FALSE )

Prepare a table of pairs of variables, descending-ordered by correlation:

# sort pairs of correlated variables
m <- melt( corr.d )
m <- m[order(- abs(m$value)), ]
# get rid of NAs
m <- na.omit(m)
# view the highest correlated: rho > 0.5
(m[ which( m$value > 0.5 ), ])

3. Debugging the GLM

Observe:

  1. NAA.Cre is highly correlated with NAA.NAAG_Cre – unsurprising given the properties of MRS data.
  2. PANSS_pos is impressively correlated with group – suggesting that if group represents something meaningful, it may be confounded with positive symptoms.

There are two possibility:

  1. If group is defined as a function of PANSS_pos
  2. group is not defined as a function of PANSS_pos, but represents a measurement only at some baseline/timepoint.

The first problem is easily seen as follows: If the response (dependent) variable group represents a function of PANSS_pos across time points, then we have found the reason why our GLM is failing - they are correlated because one is a function of the other. Let PANSS_pos measured at some “baseline” be \(P_+(t=0)\). Let the same variable at time \(t=1\) (some follow up) be \(P_+(t=1)\). Then if group represents “treatment resistance” status I suspect that in fact group is defined:

\[ Group\left( P_+( t = 0), P_+( t = 1 ) \right) = \left\{ \begin{array} \\ 1 \text{ if } P_+( t = 1 ) - P_+( t = 0 ) > \tau_{TRS} \\ 0 \text{ otherwise} \end{array} \right. \] Where \(\tau_{TRS}\) is some threshold value such that: if the patient’s positive PANSS score does not change \(> \tau_{TRS}\) between two time points (\(t=0\) and \(t=1\)), the patient is assigned TRS status (group=1). So, if group is defined by the function above, there is a reason to expect group will be linearly dependent on PANSS_pos.

The second problem is more prosaically related to the value of biomarkers. Assume that PANSS_pos is measured only at baseline, so we hope it is one of many possible predictors. Then, given it’s correlation with group it is a pretty good predictor all on it’s own (irrespective of any of the biomarkers).

To proceed : I assume that PANSS_pos is the measurement taken at baseline, and in fact, group is not defined as above so that there is no way \(P_+(t=0)\) and \(P_+(t=1)\) both enter into the derived variable group.

So we’ll try this: removing “redundant” independent variables (predictors) which are highly correlated with each other (since being correlated with the response/outcome group is not the problem, its linear-dependence between predictors which causes the GLM to fail)

4. Revising the GLM

Start be defining (somewhat arbitrarily) the predictor variables which are highly linearly dependent based on their correlation scores:

m[ which( m$value > 0.5 ),  ]

And now try a re-fit of the original GLM, with variables that are highly correlated with each other removed, and apply a bit of clinical intuition also; PANSS_neg is correlated with PANSS_gen and likewise, PANSS_pos is correlated with PANSS_gen suggesting that PANSS_gen is redundant or might be seen as a confounder and is adding to the rank-deficiency in estimating the GLM. As discussed above, NAA.NAAG_Cre also highly correlated with NAA.Cre and since one is derived from the other (by the LCModel) we cna ditch one.

dropCols <- which( names( d ) %in% c("NAA.NAAG_Cre", "PANSS_gen") )
d <- d[ , -dropCols]
x.vars <- names( d )[1:(length(names(d))-1)]
y.var  <- names( d )[length(names(d))]
(modelFormula <- as.formula( paste(y.var, paste(x.vars, collapse=" + "), sep=" ~ ") ) )
group ~ age + gender + iq + PANSS_pos + PANSS_neg + PANSS_total + 
    Glu.Cre + MI.Cre + NAA.Cre + GPC.Pch_Cre + BA8 + Caudaate + 
    PAC + lgi_parietal + lgi_occ + lgi_dlpfc
lr.fit <- glm( modelFormula, family = binomial, data = d )
glm.fit: algorithm did not convergeglm.fit: fitted probabilities numerically 0 or 1 occurred
summary(lr.fit)

Call:
glm(formula = modelFormula, family = binomial, data = d)

Deviance Residuals: 
       Min          1Q      Median          3Q         Max  
-4.379e-05  -2.100e-08  -2.100e-08   2.100e-08   4.229e-05  

Coefficients:
               Estimate Std. Error z value Pr(>|z|)
(Intercept)  -1.999e+03  1.991e+06  -0.001    0.999
age           1.035e+01  1.407e+04   0.001    0.999
gender        1.348e+02  2.267e+05   0.001    1.000
iq            2.937e+00  2.219e+03   0.001    0.999
PANSS_pos     4.753e+01  3.361e+04   0.001    0.999
PANSS_neg     2.177e+01  2.909e+04   0.001    0.999
PANSS_total  -1.010e+01  1.446e+04  -0.001    0.999
Glu.Cre       8.214e+01  5.903e+05   0.000    1.000
MI.Cre       -4.160e+02  4.935e+05  -0.001    0.999
NAA.Cre      -1.188e+02  1.585e+06   0.000    1.000
GPC.Pch_Cre   3.332e+02  1.580e+06   0.000    1.000
BA8          -4.787e-01  9.683e+02   0.000    1.000
Caudaate      3.585e-02  4.583e+02   0.000    1.000
PAC          -2.497e-02  1.045e+03   0.000    1.000
lgi_parietal  3.706e+02  6.331e+05   0.001    1.000
lgi_occ       4.432e+00  3.107e+05   0.000    1.000
lgi_dlpfc    -3.385e+01  4.452e+05   0.000    1.000

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 8.8209e+01  on 76  degrees of freedom
Residual deviance: 1.0513e-08  on 60  degrees of freedom
AIC: 34

Number of Fisher Scoring iterations: 25

Still, we have a problem – applying a bit of “human intelligence” and knowledge of the data set to the problem doesn’t fix it.

5. Automated Feature Selection with Redundancy Reduced

It’s going to be hard to proceed by “manually” trying to debug the design matrix for the GLM, so we can resort to some automated methods. With complex covariance / linear-dependence structures, sometimes using regularisation can help in feature selecting the best predictors – especially in cases like this, where there is a bunch of dependencies we haven’t worked out.

We’ll try to see if using LASSO-type regularisation can help reduce the number of predictors in the model.

require( glmnet )
x.vars <- names( d )[1:(length(names(d))-1)]
y.var  <- names( d )[length(names(d))]
Y <- d[ , y.var ]
X <- as.matrix( d[ , x.vars ] )
# note: using largely default parameters (alpha = 1 is the LASSO regularisation), and we let glmnet
# standardise variables (mean = 0, sd = 1) as units / scales not equivalent across all predictors (IQ and Age for example - you 
# can have an IQ of 100, but unlikely that Age will be on the same scale)
net.fit <- glmnet( X, Y, family = "binomial", alpha = 1 )

The difficulty with glmnets is that they have a free parameter (\(\lambda\)) – the “strength” of penalising the fit – which alters the number of predictors included, so we use cross-validation to find the optimal \(\lambda\) for this data set – remember, this is not cross validation to test the predictive value of the model overall, but “within” the model building phase, a way of feature selecting the best predictors given the problems highlighted above (dependency, sparsity, correlated predictors etc.).

cv.fit <- cv.glmnet( X, Y, family = "binomial", alpha = 1, type.measure = "class")
plot( cv.fit )

So, this graph shows the misclassification error (the lower the better) on the group variable as a function of \(\lambda\) – where varying \(\lambda\) includes (or excludes) more (or less) predictors. By looking for a global minimum in this plot, we can find the \(\lambda\) – or range of \(\lambda\) values – that gives us the best performing model and then investigate which predictors were included. The dotted lines show us the best \(\lambda\) values. And the axis label on the top of the graph tells us the number of predictors, which appears to be one predictor. We can go hunting for this predictor for values of \(\lambda\):

# lambda that gives best performance on misclassification error
bestlambda <- cv.fit$lambda.min
# the best lambda which is within one standard error of the minimum misclassification error
best1se    <- cv.fit$lambda.1se
# extract parameters at each of these lambda values
coefs.bestLambda <- as.matrix( coef(cv.fit, s = bestlambda ) )
coefs.best1se    <- as.matrix( coef(cv.fit, s = best1se ) )
# which coefficients are non-zero (e.g. contributing to the optimal performing model at lambda)
# 1. for bestLambda
( rbind( rownames( coefs.bestLambda )[ which( coefs.bestLambda > 0 ) ], coefs.bestLambda[ which( coefs.bestLambda > 0 ) ] ) )
     [,1]               
[1,] "PANSS_pos"        
[2,] "0.304969219156202"
# 2. for best1se
( rbind( rownames( coefs.best1se )[ which( coefs.best1se > 0 ) ], coefs.best1se[ which( coefs.best1se > 0 ) ] ) )
     [,1]               
[1,] "PANSS_pos"        
[2,] "0.215960414480748"

So it seems, PANSS_pos – among all the predictors – is the best predictor of group when feature selection is automated by a LASSO-penalty regularised logistic regression.

Just in case we think some ‘magic’ is happening, we can apply the same model to the original complete data set without the pre-processing (where we used context and understanding of the problem to remove redundancy in the predictors)

# re-load and tidy data
d <- read.csv( "Data_prediction_FEP.csv")
N <- nrow( d )
# remove the ID column
d <- d[ , -1 ]
# convert gender to a 0/1
d$gender <- d$gender - 1
str( d )
'data.frame':   77 obs. of  19 variables:
 $ age         : int  22 24 20 23 28 25 19 37 21 20 ...
 $ gender      : num  1 1 0 1 1 0 0 0 1 0 ...
 $ iq          : int  116 93 98 90 102 90 89 79 102 133 ...
 $ PANSS_pos   : int  17 20 14 11 13 18 23 15 13 9 ...
 $ PANSS_neg   : int  20 26 9 16 18 22 23 20 8 8 ...
 $ PANSS_gen   : int  36 35 24 29 30 40 41 37 29 22 ...
 $ PANSS_total : int  73 81 47 56 61 80 87 72 50 39 ...
 $ Glu.Cre     : num  1.31 1.13 1.07 1.44 1.18 ...
 $ MI.Cre      : num  0.743 0.635 0.698 0.708 0.713 0.62 0.731 0.752 0.627 0.711 ...
 $ NAA.Cre     : num  1.22 1.06 1.14 1.49 1.33 ...
 $ GPC.Pch_Cre : num  0.228 0.182 0.231 0.252 0.248 0.266 0.213 0.303 0.26 0.252 ...
 $ NAA.NAAG_Cre: num  1.28 1.1 1.25 1.49 1.33 ...
 $ BA8         : num  -6.07 -5.85 -66.47 19.48 33.93 ...
 $ Caudaate    : num  -42.15 39.86 40.09 5.29 -23.81 ...
 $ PAC         : num  -87.46 5.25 -77.49 -59.63 -62.94 ...
 $ lgi_parietal: num  3.21 3.33 3.62 3.74 3.29 ...
 $ lgi_occ     : num  4.35 3.92 4.5 4.76 4.08 ...
 $ lgi_dlpfc   : num  2.54 2.45 2.68 2.83 2.66 ...
 $ group       : int  1 1 0 0 0 0 1 0 0 0 ...

Now, re-run the exact same analyses using the glmnet:

x.vars <- names( d )[1:(length(names(d))-1)]
y.var  <- names( d )[length(names(d))]
# this time, keep **all** variables
Y <- d[ , y.var ]
X <- as.matrix( d[ , x.vars ] )
# CV the glmnet to find the best lambda's
cv.fit <- cv.glmnet( X, Y, family = "binomial", alpha = 1, type.measure = "class")
plot( cv.fit )

And now, retrieve the best predictors at the \(\lambda\) that minimised misclassification error:

bestlambda <- cv.fit$lambda.min
best1se    <- cv.fit$lambda.1se
coefs.bestLambda <- as.matrix( coef(cv.fit, s = bestlambda ) )
coefs.best1se    <- as.matrix( coef(cv.fit, s = best1se ) )
# which coefficients are non-zero (e.g. contributing to the optimal performing model at lambda)
# 1. for bestLambda
( rbind( rownames( coefs.bestLambda )[ which( coefs.bestLambda > 0 ) ], coefs.bestLambda[ which( coefs.bestLambda > 0 ) ] ) )
     [,1]               
[1,] "PANSS_pos"        
[2,] "0.377010551976357"
# 2. for best1se
( rbind( rownames( coefs.best1se )[ which( coefs.best1se > 0 ) ], coefs.best1se[ which( coefs.best1se > 0 ) ] ) )
     [,1]              
[1,] "PANSS_pos"       
[2,] "0.32290657768718"

And again : it’s PANSS_pos which is the strongest predictor of group

This tells us:

  1. using context knowledge to manually debug the GLM showed some redundancy, but removing it didn’t help with a classic GLM
  2. with the context knowledge (“human expertise”) applied, we then defer to some automatic feature selection (glmnet) which tells us it’s the PANSS_pos that does the work in predicting group
  3. Throwing away human expertise, and using the original unfetered data – deferring entirely to the automated feature selection – we arrive at the same answer.

6. Assume PANSS scores are not Needed / Relevant

Assume that we want to use the biomarker data, which we hope is a better or more informative predictor than clinical data. We can strip out the clinical data and try on just the biomarkers.

# identify the clinical data and remove (note, we leave demographic data in)
dropCols <- c("PANSS_pos", "PANSS_neg", "PANSS_gen", "PANSS_total")
d <- d[ , -which( names(d) %in% dropCols ) ]
(str( d ))
'data.frame':   77 obs. of  15 variables:
 $ age         : int  22 24 20 23 28 25 19 37 21 20 ...
 $ gender      : num  1 1 0 1 1 0 0 0 1 0 ...
 $ iq          : int  116 93 98 90 102 90 89 79 102 133 ...
 $ Glu.Cre     : num  1.31 1.13 1.07 1.44 1.18 ...
 $ MI.Cre      : num  0.743 0.635 0.698 0.708 0.713 0.62 0.731 0.752 0.627 0.711 ...
 $ NAA.Cre     : num  1.22 1.06 1.14 1.49 1.33 ...
 $ GPC.Pch_Cre : num  0.228 0.182 0.231 0.252 0.248 0.266 0.213 0.303 0.26 0.252 ...
 $ NAA.NAAG_Cre: num  1.28 1.1 1.25 1.49 1.33 ...
 $ BA8         : num  -6.07 -5.85 -66.47 19.48 33.93 ...
 $ Caudaate    : num  -42.15 39.86 40.09 5.29 -23.81 ...
 $ PAC         : num  -87.46 5.25 -77.49 -59.63 -62.94 ...
 $ lgi_parietal: num  3.21 3.33 3.62 3.74 3.29 ...
 $ lgi_occ     : num  4.35 3.92 4.5 4.76 4.08 ...
 $ lgi_dlpfc   : num  2.54 2.45 2.68 2.83 2.66 ...
 $ group       : int  1 1 0 0 0 0 1 0 0 0 ...
NULL
x.vars <- names( d )[1:(length(names(d))-1)]
y.var  <- names( d )[length(names(d))]
Y <- d[ , y.var ]
X <- as.matrix( d[ , x.vars ] )
# CV the glmnet to find the best lambda's
cv.fit <- cv.glmnet( X, Y, family = "binomial", alpha = 1, type.measure = "class")
plot( cv.fit )

Notice how the misclassification error is now much higher overall (0.25, as compared with 0.10 when clinical data was included) suggesting the biomarkers are not doing better than the clinical data. Problematically, the best values for \(\lambda\) and the one-standard error range all indicate a model with zero predictors, suggesting the model is degenerate.

This could be because we are being too “strict” with the elastic- versus LASSO-penalty. Assume that rather than their being an optimal set of linearly-independent predictors, there are in fact a small set of relatively weak predictors that “work together” to assist in prediction. This can be controlled by altering the value of the \(\alpha\) parameter in the GLMnet model. Here’s how: setting \(\alpha = 1\) means we want a “strict” LASSO regularisation and \(\alpha = 0\) is the strict ridge-regression. The LASSO tends to find the best predictors and throw out all others. The ridge model allows for groups of variables to work together. So if we set \(\alpha = 0.5\), (and then vary \(\lambda\) as before), groups of predictors which covary together are kept in the model by balancing LASSO against elastic-net behaviours in the model fitting (feature selection). This may or may not improve the model performance, but it will likely keep the number of predictors included > 0. The authors of GLMnet recommend that setting \(\alpha = 1 - \epsilon\) (where \(\epsilon\) is some small value \(> 0\)) means predictors are included using LASSO, but degenerate or extreme correlations in variables are thrown out.

# the model with alpha = 0.5
cv.fit <- cv.glmnet( X, Y, family = "binomial", alpha = 0.5, type.measure = "class")
plot( cv.fit )

Still the same problem. Let’s now try the \(\epsilon\) approach to remove degeneracy but keep the robustness of LASSO:

# the model with alpha = 0.05
cv.fit <- cv.glmnet( X, Y, family = "binomial", alpha = 0.05, type.measure = "class")
plot( cv.fit )

Same problems – of note, rerunning these above two analyses gives different results each time (they are stochastic algorithms) but the repeatability can be tested by re-running and keeping track of the predictors that are included. Note that a notion of “p values” on the coefficients in a GLMnet is somewhat moot – instead, we rely on resampling to find out if the predictors are consistently associated with misclassification error.

Let’s try that, assuming that some occasional stochastic factors are causing the solution to vary and we’ll record the predictors found to be useful on each run of the estimation to see if there’s a pattern, and to do this, we’ll use only the best \(\lambda\):

# Warning : this takes about 10 minutes to run on a single CPU
N.sims <- 10000
tabResults <- data.frame( Var = colnames(X), Included = rep(0,length(colnames(X))) )
tabError   <- rep(0, N.sims)

for ( i in 1:N.sims ) {
  cv.fit <- cv.glmnet( X, Y, family = "binomial", alpha = 0.05, type.measure = "class")
  bestlambda <- cv.fit$lambda.min
  coefs.bestLambda <- round( as.matrix( coef(cv.fit, s = bestlambda ) ), 6 )
  
  # find non-zero coefficients (at 6 dec. places precision) -- excluding the intercept
  nonZero <- which( coefs.bestLambda[2:nrow(coefs.bestLambda)] != 0)
  
  if( length( nonZero ) > 0 ) {  ## if there are non-zero coefficients
    # increment counts
    tabResults$Included[ nonZero ] <- tabResults$Included[ nonZero ] + 1
  }
  
  # store the best CV misclassification error
  tabError[i] <- cv.fit$cvm[ which( cv.fit$lambda == bestlambda ) ]
}

The mean classification error works out to be with standard deviation:

(mean( tabError ))
[1] 0.2523286
(sd(tabError))
[1] 0.008595453

Examining the table of predictors which are inluded / excluded on each of 10,000 runs:

(tabResults)

We can see that on aprroximately half the runs all predictors are included in the model, and on the other 5000+ runs, none are included (that’s why they all have 4764 as their counts - they were all in, or out, together). Glu.cre creeps in for a very small proportion: 0.46% of the models used this predictor).

This tells us that the best error we’ll get is 0.25 (so 25% of the sample are incorrectly classified) – there are 57 for group = 0 and 20 for group = 1. So assuming symmetric errors (e.g. we get 25% wrong equally in both groups) this means 14 people are classified incorrectly in group = 0 and 5 are incorrectly classified in group = 1 – and these can be with either none, or all of the predictors included.

This suggests – even with access to all the data for training and testing (validating) the model, we can’t find very robust predictors beyond the PANSS_pos variable which outperforms all the biomarkers (recall, the minimum misclassification error was 0.1 for PANSS_pos).

LS0tCnRpdGxlOiAiRkVQIFByZWRpY3RvcnM6IEV4cGxvcmF0b3J5IEFuYWx5c2lzIGFuZCBSZWd1bGFyaXNlZCBHTE1zIgpkYXRlOiAiYHIgZm9ybWF0KFN5cy50aW1lKCksICclZCAlQiwgJVknKWAiCmF1dGhvcjogRGFuIFcgSm95Y2UKb3V0cHV0OiBodG1sX25vdGVib29rCi0tLQoKIyMgMS4gU2V0dXAKYGBge3J9CnJtKCBsaXN0ID0gbHMoKSApCnJlcXVpcmUoIGdncGxvdDIgKQpyZXF1aXJlKCBNYXRyaXggKQpyZXF1aXJlKCBjb3JycGxvdCApCgojIGxvYWQgYW4gdGlkeSBkYXRhCmQgPC0gcmVhZC5jc3YoICJEYXRhX3ByZWRpY3Rpb25fRkVQLmNzdiIpCk4gPC0gbnJvdyggZCApCiMgcmVtb3ZlIHRoZSBJRCBjb2x1bW4KZCA8LSBkWyAsIC0xIF0KCnN0ciggZCApCmBgYAoKR2VuZGVyIGlzIGNvZGVkIG51bWVyaWNhbGx5LCBhcyAxIGFuZCAyLiAgVGhpcyBtZWFucyBpdCBuZWVkcyBkdW1teSBjb2RpbmcsIGJ1dCBhcyBvbmx5IHR3byBsZXZlbHMsIHdlIGNhbiBoYWNrIGl0IHRvIGJlIDAvMSBhbmQga2VlcCBpdCBhcyBhIG51bWVyaWNhbCBwcmVkaWN0b3IgZm9yIGEgcHJlbGltaW5hcnkgbGluZWFyIG1vZGVsIC8gR0xNCgpgYGB7cn0KZCRnZW5kZXIgPC0gZCRnZW5kZXIgLSAxCmBgYAoKCiMjIDIuIEluZmVyZW50aWFsIC8gQXNzb2NpYXRpb24gQW5hbHlzaXMKJFkgPSBceyAwLDEgXH0kLCBzbyBjbGVhcmx5IGEgYmlub21pYWwsIHdoaWNoIHN1Z2dlc3RzIGEgbG9naXN0aWMgcmVncmVzc2lvbiBpcyB0aGUgbW9zdCBiYXNpYyBtb2RlbCBmb3IgdGhpcyBraW5kIG9mIGRhdGEuICBJbmNsdWRlIGFsbCB2YXJpYWJsZXMgKHNvIG5vIHJlZ3VsYXJpc2F0aW9uIC8gb25saW5lIGZlYXR1cmUgc2VsZWN0aW9uKSBhbmQgbGV0J3MganVzdCBzZWUgd2hhdCBpdCBzaG93cy4KYGBge3J9CgojIGluZGVwZW5kZW50IChwcmVkaWN0b3IpIHZhcmlhYmxlcwp4LnZhcnMgPC0gbmFtZXMoIGQgKVsxOihsZW5ndGgobmFtZXMoZCkpLTEpXQoKIyBkZXBlbmRlbnQgKG91dGNvbWUsIHJlc3BvbnNlKSB2YXJpYWJsZXMKeS52YXIgIDwtIG5hbWVzKCBkIClbbGVuZ3RoKG5hbWVzKGQpKV0KCiMgYnVpbGQgZm9ybXVsYSBvYmplY3QgZm9yIHRoZSBnbG0KKG1vZGVsRm9ybXVsYSA8LSBhcy5mb3JtdWxhKCBwYXN0ZSh5LnZhciwgcGFzdGUoeC52YXJzLCBjb2xsYXBzZT0iICsgIiksIHNlcD0iIH4gIikgKSApCmBgYAoKTm93LCBmaXQgdGhlIEdMTSB3aXRoICphbGwqIHRoZSBkYXRhIGF2YWlsYWJsZSAtLSBqdXN0IGEgY2xhc3NpYyBpbmZlcmVudGlhbCBhbmFseXNpcwoKYGBge3J9CmxyLmZpdCA8LSBnbG0oIG1vZGVsRm9ybXVsYSwgZmFtaWx5ID0gYmlub21pYWwsIGRhdGEgPSBkICkKc3VtbWFyeShsci5maXQpCmBgYAoKU29tZXRoaW5nIHN1Z2dlc2luZyByYW5rIGRlZmljaWVuY3kgaW4gdGhlIGRlc2lnbiBtYXRyaXggLS0gbGlrZWx5IHRoYXQgdGhlIHByZWRpY3RvcnMgYXJlIG5vdCBsaW5lYXJseS1pbmRlcGVuZGVudCBvciBvbmUgb2YgdGhlbSBpcyBwZXJmZWN0bHkgYXNzb2NpYXRlZCB3aXRoIHRoZSByZXNwb25zZSB2YXJpYWJsZSBgZ3JvdXBgLiAgYHN1bW1hcnkoIGxyLmZpdCApYCBzdWdnZXN0cyBgUEFOU1NfdG90YWxgIGlzIHByb2JsZW1hdGljLCBidXQgdGhlcmUgbWlnaHQgYmUgb3RoZXJzLCBzbyBsb29rIGF0IGNvcnJlbGF0aW9uczoKCmBgYHtyfQojIGV4cGxvcmUgP2hpZ2hseSBjb3JyZWxhdGVkIHZhcmlhYmxlcwpjb3JyLmQgPC0gY29yKCBkICkKY29yci5kWyBsb3dlci50cmkoIGNvcnIuZCwgZGlhZyA9IFRSVUUgKSBdIDwtIE5BCmNvcnJwbG90KCBjb3JyLmQsIHR5cGUgPSAidXBwZXIiLCBkaWFnID0gRkFMU0UgKQpgYGAKClByZXBhcmUgYSB0YWJsZSBvZiBwYWlycyBvZiB2YXJpYWJsZXMsIGRlc2NlbmRpbmctb3JkZXJlZCBieSBjb3JyZWxhdGlvbjoKCmBgYHtyfQojIHNvcnQgcGFpcnMgb2YgY29ycmVsYXRlZCB2YXJpYWJsZXMKbSA8LSBtZWx0KCBjb3JyLmQgKQptIDwtIG1bb3JkZXIoLSBhYnMobSR2YWx1ZSkpLCBdCiMgZ2V0IHJpZCBvZiBOQXMKbSA8LSBuYS5vbWl0KG0pCgojIHZpZXcgdGhlIGhpZ2hlc3QgY29ycmVsYXRlZDogcmhvID4gMC41CihtWyB3aGljaCggbSR2YWx1ZSA+IDAuNSApLCBdKQoKYGBgCiMjIDMuIERlYnVnZ2luZyB0aGUgR0xNIAoKT2JzZXJ2ZToKCiAgMS4gYE5BQS5DcmVgIGlzIGhpZ2hseSBjb3JyZWxhdGVkIHdpdGggYE5BQS5OQUFHX0NyZWAgLS0gdW5zdXJwcmlzaW5nIGdpdmVuIHRoZSBwcm9wZXJ0aWVzIG9mIE1SUyBkYXRhLgogIDIuIGBQQU5TU19wb3NgIGlzIGltcHJlc3NpdmVseSBjb3JyZWxhdGVkIHdpdGggYGdyb3VwYCAtLSBzdWdnZXN0aW5nIHRoYXQgaWYgZ3JvdXAgcmVwcmVzZW50cyBzb21ldGhpbmcgbWVhbmluZ2Z1bCwgaXQgKiptYXkgYmUqKiBjb25mb3VuZGVkIHdpdGggcG9zaXRpdmUgc3ltcHRvbXMuCgpUaGVyZSBhcmUgdHdvIHBvc3NpYmlsaXR5OgoKICAxLiBJZiBgZ3JvdXBgIGlzIGRlZmluZWQgYXMgYSBmdW5jdGlvbiBvZiBgUEFOU1NfcG9zYAogIDIuIGBncm91cGAgaXMgbm90IGRlZmluZWQgYXMgYSBmdW5jdGlvbiBvZiBgUEFOU1NfcG9zYCwgYnV0IHJlcHJlc2VudHMgYSBtZWFzdXJlbWVudCBvbmx5IGF0IHNvbWUgYmFzZWxpbmUvdGltZXBvaW50LgoKVGhlIGZpcnN0IHByb2JsZW0gaXMgZWFzaWx5IHNlZW4gYXMgZm9sbG93czogIElmIHRoZSByZXNwb25zZSAoZGVwZW5kZW50KSB2YXJpYWJsZSBgZ3JvdXBgIHJlcHJlc2VudHMgYSBmdW5jdGlvbiBvZiBgUEFOU1NfcG9zYCAqKmFjcm9zcyoqIHRpbWUgcG9pbnRzLCB0aGVuIHdlIGhhdmUgZm91bmQgdGhlIHJlYXNvbiB3aHkgb3VyIEdMTSBpcyBmYWlsaW5nIC0gdGhleSBhcmUgY29ycmVsYXRlZCBiZWNhdXNlIG9uZSBpcyBhIGZ1bmN0aW9uIG9mIHRoZSBvdGhlci4gIExldCBQQU5TU19wb3MgbWVhc3VyZWQgYXQgc29tZSAiYmFzZWxpbmUiIGJlICRQXysodD0wKSQuICBMZXQgdGhlIHNhbWUgdmFyaWFibGUgYXQgdGltZSAkdD0xJCAoc29tZSBmb2xsb3cgdXApIGJlICRQXysodD0xKSQuICBUaGVuIGlmIGBncm91cGAgcmVwcmVzZW50cyAidHJlYXRtZW50IHJlc2lzdGFuY2UiIHN0YXR1cyBJIHN1c3BlY3QgdGhhdCBpbiBmYWN0IGdyb3VwIGlzIGRlZmluZWQ6IAoKJCQgCkdyb3VwXGxlZnQoIFBfKyggdCA9IDApLCBQXysoIHQgPSAxICkgXHJpZ2h0KSA9IFxsZWZ0XHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIFxiZWdpbnthcnJheX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIFxcCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMSBcdGV4dHsgaWYgfSBQXysoIHQgPSAxICkgLSBQXysoIHQgPSAwICkgPiBcdGF1X3tUUlN9IFxcCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMCBcdGV4dHsgb3RoZXJ3aXNlfQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgXGVuZHthcnJheX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIFxyaWdodC4KJCQKV2hlcmUgJFx0YXVfe1RSU30kIGlzIHNvbWUgdGhyZXNob2xkIHZhbHVlIHN1Y2ggdGhhdDogaWYgdGhlIHBhdGllbnQncyBwb3NpdGl2ZSBQQU5TUyBzY29yZSAqZG9lcyBub3QqIGNoYW5nZSAkPiBcdGF1X3tUUlN9JCBiZXR3ZWVuIHR3byB0aW1lIHBvaW50cyAoJHQ9MCQgYW5kICR0PTEkKSwgdGhlIHBhdGllbnQgaXMgYXNzaWduZWQgVFJTIHN0YXR1cyAoYGdyb3VwPTFgKS4gIFNvLCBpZiBgZ3JvdXBgIGlzIGRlZmluZWQgYnkgdGhlIGZ1bmN0aW9uIGFib3ZlLCB0aGVyZSBpcyBhIHJlYXNvbiB0byBleHBlY3QgYGdyb3VwYCB3aWxsIGJlIGxpbmVhcmx5IGRlcGVuZGVudCBvbiBgUEFOU1NfcG9zYC4KClRoZSBzZWNvbmQgcHJvYmxlbSBpcyBtb3JlIHByb3NhaWNhbGx5IHJlbGF0ZWQgdG8gdGhlICoqdmFsdWUqKiBvZiBiaW9tYXJrZXJzLiAgQXNzdW1lIHRoYXQgYFBBTlNTX3Bvc2AgaXMgbWVhc3VyZWQgKipvbmx5KiogYXQgYmFzZWxpbmUsIHNvIHdlIGhvcGUgaXQgaXMgKm9uZSBvZiBtYW55KiBwb3NzaWJsZSBwcmVkaWN0b3JzLiAgVGhlbiwgZ2l2ZW4gaXQncyBjb3JyZWxhdGlvbiB3aXRoIGBncm91cGAgaXQgaXMgYSBwcmV0dHkgZ29vZCBwcmVkaWN0b3IgYWxsIG9uIGl0J3Mgb3duIChpcnJlc3BlY3RpdmUgb2YgYW55IG9mIHRoZSBiaW9tYXJrZXJzKS4KCgpUbyBwcm9jZWVkIDogSSBhc3N1bWUgdGhhdCBgUEFOU1NfcG9zYCBpcyB0aGUgbWVhc3VyZW1lbnQgdGFrZW4gYXQgKipiYXNlbGluZSoqLCBhbmQgaW4gZmFjdCwgYGdyb3VwYCBpcyAqKm5vdCoqIGRlZmluZWQgYXMgYWJvdmUgc28gdGhhdCB0aGVyZSBpcyBubyB3YXkgJFBfKyh0PTApJCBhbmQgJFBfKyh0PTEpJCBib3RoIGVudGVyIGludG8gdGhlIGRlcml2ZWQgdmFyaWFibGUgYGdyb3VwYC4KClNvIHdlJ2xsIHRyeSB0aGlzOiByZW1vdmluZyAicmVkdW5kYW50IiBpbmRlcGVuZGVudCB2YXJpYWJsZXMgKHByZWRpY3RvcnMpIHdoaWNoIGFyZSBoaWdobHkgY29ycmVsYXRlZCAqKndpdGggZWFjaCBvdGhlcioqIChzaW5jZSBiZWluZyBjb3JyZWxhdGVkIHdpdGggdGhlIHJlc3BvbnNlL291dGNvbWUgYGdyb3VwYCBpcyBub3QgdGhlIHByb2JsZW0sIGl0cyBsaW5lYXItKmRlcGVuZGVuY2UqIGJldHdlZW4gcHJlZGljdG9ycyB3aGljaCBjYXVzZXMgdGhlIEdMTSB0byBmYWlsKQoKIyMgNC4gUmV2aXNpbmcgdGhlIEdMTQoKU3RhcnQgYmUgZGVmaW5pbmcgKHNvbWV3aGF0IGFyYml0cmFyaWx5KSB0aGUgcHJlZGljdG9yIHZhcmlhYmxlcyB3aGljaCBhcmUgaGlnaGx5IGxpbmVhcmx5IGRlcGVuZGVudCBiYXNlZCBvbiB0aGVpciBjb3JyZWxhdGlvbiBzY29yZXM6CgpgYGB7cn0KbVsgd2hpY2goIG0kdmFsdWUgPiAwLjUgKSwgIF0KYGBgCgpBbmQgbm93IHRyeSBhIHJlLWZpdCBvZiB0aGUgb3JpZ2luYWwgR0xNLCB3aXRoIHZhcmlhYmxlcyB0aGF0IGFyZSBoaWdobHkgY29ycmVsYXRlZCB3aXRoIGVhY2ggb3RoZXIgcmVtb3ZlZCwgYW5kIGFwcGx5IGEgYml0IG9mIGNsaW5pY2FsIGludHVpdGlvbiBhbHNvOyBgUEFOU1NfbmVnYCBpcyBjb3JyZWxhdGVkIHdpdGggYFBBTlNTX2dlbmAgYW5kIGxpa2V3aXNlLCBgUEFOU1NfcG9zYCBpcyBjb3JyZWxhdGVkIHdpdGggYFBBTlNTX2dlbmAgc3VnZ2VzdGluZyB0aGF0IGBQQU5TU19nZW5gIGlzIHJlZHVuZGFudCBvciBtaWdodCBiZSBzZWVuIGFzIGEgY29uZm91bmRlciBhbmQgaXMgYWRkaW5nIHRvIHRoZSByYW5rLWRlZmljaWVuY3kgaW4gZXN0aW1hdGluZyB0aGUgR0xNLiAgQXMgZGlzY3Vzc2VkIGFib3ZlLCBgTkFBLk5BQUdfQ3JlYCBhbHNvIGhpZ2hseSBjb3JyZWxhdGVkIHdpdGggYE5BQS5DcmVgIGFuZCBzaW5jZSBvbmUgaXMgZGVyaXZlZCBmcm9tIHRoZSBvdGhlciAoYnkgdGhlIExDTW9kZWwpIHdlIGNuYSBkaXRjaCBvbmUuCgpgYGB7cn0KCmRyb3BDb2xzIDwtIHdoaWNoKCBuYW1lcyggZCApICVpbiUgYygiTkFBLk5BQUdfQ3JlIiwgIlBBTlNTX2dlbiIpICkKZCA8LSBkWyAsIC1kcm9wQ29sc10KCngudmFycyA8LSBuYW1lcyggZCApWzE6KGxlbmd0aChuYW1lcyhkKSktMSldCnkudmFyICA8LSBuYW1lcyggZCApW2xlbmd0aChuYW1lcyhkKSldCihtb2RlbEZvcm11bGEgPC0gYXMuZm9ybXVsYSggcGFzdGUoeS52YXIsIHBhc3RlKHgudmFycywgY29sbGFwc2U9IiArICIpLCBzZXA9IiB+ICIpICkgKQpsci5maXQgPC0gZ2xtKCBtb2RlbEZvcm11bGEsIGZhbWlseSA9IGJpbm9taWFsLCBkYXRhID0gZCApCnN1bW1hcnkobHIuZml0KQoKYGBgClN0aWxsLCB3ZSBoYXZlIGEgcHJvYmxlbSAtLSBhcHBseWluZyBhIGJpdCBvZiAiaHVtYW4gaW50ZWxsaWdlbmNlIiBhbmQga25vd2xlZGdlIG9mIHRoZSBkYXRhIHNldCB0byB0aGUgcHJvYmxlbSBkb2Vzbid0IGZpeCBpdC4KCgoKIyMgNS4gQXV0b21hdGVkIEZlYXR1cmUgU2VsZWN0aW9uICoqd2l0aCoqIFJlZHVuZGFuY3kgUmVkdWNlZApJdCdzIGdvaW5nIHRvIGJlIGhhcmQgdG8gcHJvY2VlZCBieSAibWFudWFsbHkiIHRyeWluZyB0byBkZWJ1ZyB0aGUgZGVzaWduIG1hdHJpeCBmb3IgdGhlIEdMTSwgc28gd2UgY2FuIHJlc29ydCB0byBzb21lIGF1dG9tYXRlZCBtZXRob2RzLiAgV2l0aCBjb21wbGV4IGNvdmFyaWFuY2UgLyBsaW5lYXItZGVwZW5kZW5jZSBzdHJ1Y3R1cmVzLCBzb21ldGltZXMgdXNpbmcgcmVndWxhcmlzYXRpb24gY2FuIGhlbHAgaW4gZmVhdHVyZSBzZWxlY3RpbmcgdGhlIGJlc3QgcHJlZGljdG9ycyAtLSBlc3BlY2lhbGx5IGluIGNhc2VzIGxpa2UgdGhpcywgd2hlcmUgdGhlcmUgaXMgYSBidW5jaCBvZiBkZXBlbmRlbmNpZXMgd2UgaGF2ZW4ndCB3b3JrZWQgb3V0LiAKCldlJ2xsIHRyeSB0byBzZWUgaWYgdXNpbmcgTEFTU08tdHlwZSByZWd1bGFyaXNhdGlvbiBjYW4gaGVscCByZWR1Y2UgdGhlIG51bWJlciBvZiBwcmVkaWN0b3JzIGluIHRoZSBtb2RlbC4KCmBgYHtyfQpyZXF1aXJlKCBnbG1uZXQgKQoKeC52YXJzIDwtIG5hbWVzKCBkIClbMToobGVuZ3RoKG5hbWVzKGQpKS0xKV0KeS52YXIgIDwtIG5hbWVzKCBkIClbbGVuZ3RoKG5hbWVzKGQpKV0KClkgPC0gZFsgLCB5LnZhciBdClggPC0gYXMubWF0cml4KCBkWyAsIHgudmFycyBdICkKCiMgbm90ZTogdXNpbmcgbGFyZ2VseSBkZWZhdWx0IHBhcmFtZXRlcnMgKGFscGhhID0gMSBpcyB0aGUgTEFTU08gcmVndWxhcmlzYXRpb24pLCBhbmQgd2UgbGV0IGdsbW5ldAojIHN0YW5kYXJkaXNlIHZhcmlhYmxlcyAobWVhbiA9IDAsIHNkID0gMSkgYXMgdW5pdHMgLyBzY2FsZXMgbm90IGVxdWl2YWxlbnQgYWNyb3NzIGFsbCBwcmVkaWN0b3JzIChJUSBhbmQgQWdlIGZvciBleGFtcGxlIC0geW91IAojIGNhbiBoYXZlIGFuIElRIG9mIDEwMCwgYnV0IHVubGlrZWx5IHRoYXQgQWdlIHdpbGwgYmUgb24gdGhlIHNhbWUgc2NhbGUpCm5ldC5maXQgPC0gZ2xtbmV0KCBYLCBZLCBmYW1pbHkgPSAiYmlub21pYWwiLCBhbHBoYSA9IDEgKQoKCmBgYAoKVGhlIGRpZmZpY3VsdHkgd2l0aCBgZ2xtbmV0YHMgaXMgdGhhdCB0aGV5IGhhdmUgYSBmcmVlIHBhcmFtZXRlciAoJFxsYW1iZGEkKSAtLSB0aGUgInN0cmVuZ3RoIiBvZiBwZW5hbGlzaW5nIHRoZSBmaXQgLS0gIHdoaWNoIGFsdGVycyB0aGUgbnVtYmVyIG9mIHByZWRpY3RvcnMgaW5jbHVkZWQsIHNvIHdlIHVzZSBjcm9zcy12YWxpZGF0aW9uIHRvIGZpbmQgdGhlIG9wdGltYWwgJFxsYW1iZGEkIGZvciB0aGlzIGRhdGEgc2V0IC0tIHJlbWVtYmVyLCB0aGlzIGlzICoqbm90KiogY3Jvc3MgdmFsaWRhdGlvbiB0byAqKnRlc3QqKiB0aGUgcHJlZGljdGl2ZSB2YWx1ZSBvZiB0aGUgbW9kZWwgb3ZlcmFsbCwgYnV0ICJ3aXRoaW4iIHRoZSBtb2RlbCBidWlsZGluZyBwaGFzZSwgYSB3YXkgb2YgZmVhdHVyZSBzZWxlY3RpbmcgdGhlICoqYmVzdCoqIHByZWRpY3RvcnMgZ2l2ZW4gdGhlIHByb2JsZW1zIGhpZ2hsaWdodGVkIGFib3ZlIChkZXBlbmRlbmN5LCBzcGFyc2l0eSwgY29ycmVsYXRlZCBwcmVkaWN0b3JzIGV0Yy4pLiAgCgoKYGBge3J9CmN2LmZpdCA8LSBjdi5nbG1uZXQoIFgsIFksIGZhbWlseSA9ICJiaW5vbWlhbCIsIGFscGhhID0gMSwgdHlwZS5tZWFzdXJlID0gImNsYXNzIikKcGxvdCggY3YuZml0ICkKYGBgClNvLCB0aGlzIGdyYXBoIHNob3dzIHRoZSAqKm1pc2NsYXNzaWZpY2F0aW9uIGVycm9yKiogKHRoZSBsb3dlciB0aGUgYmV0dGVyKSBvbiB0aGUgYGdyb3VwYCB2YXJpYWJsZSBhcyBhIGZ1bmN0aW9uIG9mICRcbGFtYmRhJCAtLSB3aGVyZSB2YXJ5aW5nICRcbGFtYmRhJCBpbmNsdWRlcyAob3IgZXhjbHVkZXMpIG1vcmUgKG9yIGxlc3MpIHByZWRpY3RvcnMuICBCeSBsb29raW5nIGZvciBhIGdsb2JhbCBtaW5pbXVtIGluIHRoaXMgcGxvdCwgd2UgY2FuIGZpbmQgdGhlICRcbGFtYmRhJCAtLSBvciByYW5nZSBvZiAkXGxhbWJkYSQgdmFsdWVzIC0tIHRoYXQgZ2l2ZXMgdXMgdGhlIGJlc3QgcGVyZm9ybWluZyBtb2RlbCBhbmQgdGhlbiBpbnZlc3RpZ2F0ZSB3aGljaCBwcmVkaWN0b3JzIHdlcmUgaW5jbHVkZWQuICBUaGUgZG90dGVkIGxpbmVzIHNob3cgdXMgdGhlIGJlc3QgJFxsYW1iZGEkIHZhbHVlcy4gIEFuZCB0aGUgYXhpcyBsYWJlbCBvbiB0aGUgdG9wIG9mIHRoZSBncmFwaCB0ZWxscyB1cyB0aGUgbnVtYmVyIG9mIHByZWRpY3RvcnMsIHdoaWNoIGFwcGVhcnMgdG8gYmUgKipvbmUqKiBwcmVkaWN0b3IuICBXZSBjYW4gZ28gaHVudGluZyBmb3IgdGhpcyBwcmVkaWN0b3IgZm9yIHZhbHVlcyBvZiAkXGxhbWJkYSQ6CgpgYGB7cn0KIyBsYW1iZGEgdGhhdCBnaXZlcyBiZXN0IHBlcmZvcm1hbmNlIG9uIG1pc2NsYXNzaWZpY2F0aW9uIGVycm9yCmJlc3RsYW1iZGEgPC0gY3YuZml0JGxhbWJkYS5taW4KCiMgdGhlIGJlc3QgbGFtYmRhIHdoaWNoIGlzIHdpdGhpbiBvbmUgc3RhbmRhcmQgZXJyb3Igb2YgdGhlIG1pbmltdW0gbWlzY2xhc3NpZmljYXRpb24gZXJyb3IKYmVzdDFzZSAgICA8LSBjdi5maXQkbGFtYmRhLjFzZQoKIyBleHRyYWN0IHBhcmFtZXRlcnMgYXQgZWFjaCBvZiB0aGVzZSBsYW1iZGEgdmFsdWVzCmNvZWZzLmJlc3RMYW1iZGEgPC0gYXMubWF0cml4KCBjb2VmKGN2LmZpdCwgcyA9IGJlc3RsYW1iZGEgKSApCmNvZWZzLmJlc3Qxc2UgICAgPC0gYXMubWF0cml4KCBjb2VmKGN2LmZpdCwgcyA9IGJlc3Qxc2UgKSApCgojIHdoaWNoIGNvZWZmaWNpZW50cyBhcmUgbm9uLXplcm8gKGUuZy4gY29udHJpYnV0aW5nIHRvIHRoZSBvcHRpbWFsIHBlcmZvcm1pbmcgbW9kZWwgYXQgbGFtYmRhKQojIDEuIGZvciBiZXN0TGFtYmRhCiggcmJpbmQoIHJvd25hbWVzKCBjb2Vmcy5iZXN0TGFtYmRhIClbIHdoaWNoKCBjb2Vmcy5iZXN0TGFtYmRhID4gMCApIF0sIGNvZWZzLmJlc3RMYW1iZGFbIHdoaWNoKCBjb2Vmcy5iZXN0TGFtYmRhID4gMCApIF0gKSApCiMgMi4gZm9yIGJlc3Qxc2UKKCByYmluZCggcm93bmFtZXMoIGNvZWZzLmJlc3Qxc2UgKVsgd2hpY2goIGNvZWZzLmJlc3Qxc2UgPiAwICkgXSwgY29lZnMuYmVzdDFzZVsgd2hpY2goIGNvZWZzLmJlc3Qxc2UgPiAwICkgXSApICkKCgpgYGAKClNvIGl0IHNlZW1zLCBgUEFOU1NfcG9zYCAtLSBhbW9uZyBhbGwgdGhlIHByZWRpY3RvcnMgLS0gaXMgdGhlIGJlc3QgcHJlZGljdG9yIG9mIGBncm91cGAgd2hlbiBmZWF0dXJlIHNlbGVjdGlvbiBpcyBhdXRvbWF0ZWQgYnkgYSBMQVNTTy1wZW5hbHR5IHJlZ3VsYXJpc2VkIGxvZ2lzdGljIHJlZ3Jlc3Npb24uCgpKdXN0IGluIGNhc2Ugd2UgdGhpbmsgc29tZSAnbWFnaWMnIGlzIGhhcHBlbmluZywgd2UgY2FuIGFwcGx5IHRoZSBzYW1lIG1vZGVsIHRvIHRoZSBvcmlnaW5hbCBjb21wbGV0ZSBkYXRhIHNldCB3aXRob3V0IHRoZSBwcmUtcHJvY2Vzc2luZyAod2hlcmUgd2UgdXNlZCBjb250ZXh0IGFuZCB1bmRlcnN0YW5kaW5nIG9mIHRoZSBwcm9ibGVtIHRvIHJlbW92ZSByZWR1bmRhbmN5IGluIHRoZSBwcmVkaWN0b3JzKQoKYGBge3J9CiMgcmUtbG9hZCBhbmQgdGlkeSBkYXRhCmQgPC0gcmVhZC5jc3YoICJEYXRhX3ByZWRpY3Rpb25fRkVQLmNzdiIpCk4gPC0gbnJvdyggZCApCiMgcmVtb3ZlIHRoZSBJRCBjb2x1bW4KZCA8LSBkWyAsIC0xIF0KIyBjb252ZXJ0IGdlbmRlciB0byBhIDAvMQpkJGdlbmRlciA8LSBkJGdlbmRlciAtIDEKc3RyKCBkICkKYGBgCgpOb3csIHJlLXJ1biB0aGUgZXhhY3Qgc2FtZSBhbmFseXNlcyB1c2luZyB0aGUgYGdsbW5ldGA6CgpgYGB7cn0KeC52YXJzIDwtIG5hbWVzKCBkIClbMToobGVuZ3RoKG5hbWVzKGQpKS0xKV0KeS52YXIgIDwtIG5hbWVzKCBkIClbbGVuZ3RoKG5hbWVzKGQpKV0KCiMgdGhpcyB0aW1lLCBrZWVwICoqYWxsKiogdmFyaWFibGVzClkgPC0gZFsgLCB5LnZhciBdClggPC0gYXMubWF0cml4KCBkWyAsIHgudmFycyBdICkKCiMgQ1YgdGhlIGdsbW5ldCB0byBmaW5kIHRoZSBiZXN0IGxhbWJkYSdzCmN2LmZpdCA8LSBjdi5nbG1uZXQoIFgsIFksIGZhbWlseSA9ICJiaW5vbWlhbCIsIGFscGhhID0gMSwgdHlwZS5tZWFzdXJlID0gImNsYXNzIikKcGxvdCggY3YuZml0ICkKCgoKYGBgCkFuZCBub3csIHJldHJpZXZlIHRoZSBiZXN0IHByZWRpY3RvcnMgYXQgdGhlICRcbGFtYmRhJCB0aGF0IG1pbmltaXNlZCBtaXNjbGFzc2lmaWNhdGlvbiBlcnJvcjoKCmBgYHtyfQpiZXN0bGFtYmRhIDwtIGN2LmZpdCRsYW1iZGEubWluCmJlc3Qxc2UgICAgPC0gY3YuZml0JGxhbWJkYS4xc2UKCmNvZWZzLmJlc3RMYW1iZGEgPC0gYXMubWF0cml4KCBjb2VmKGN2LmZpdCwgcyA9IGJlc3RsYW1iZGEgKSApCmNvZWZzLmJlc3Qxc2UgICAgPC0gYXMubWF0cml4KCBjb2VmKGN2LmZpdCwgcyA9IGJlc3Qxc2UgKSApCgojIHdoaWNoIGNvZWZmaWNpZW50cyBhcmUgbm9uLXplcm8gKGUuZy4gY29udHJpYnV0aW5nIHRvIHRoZSBvcHRpbWFsIHBlcmZvcm1pbmcgbW9kZWwgYXQgbGFtYmRhKQojIDEuIGZvciBiZXN0TGFtYmRhCiggcmJpbmQoIHJvd25hbWVzKCBjb2Vmcy5iZXN0TGFtYmRhIClbIHdoaWNoKCBjb2Vmcy5iZXN0TGFtYmRhID4gMCApIF0sIGNvZWZzLmJlc3RMYW1iZGFbIHdoaWNoKCBjb2Vmcy5iZXN0TGFtYmRhID4gMCApIF0gKSApCiMgMi4gZm9yIGJlc3Qxc2UKKCByYmluZCggcm93bmFtZXMoIGNvZWZzLmJlc3Qxc2UgKVsgd2hpY2goIGNvZWZzLmJlc3Qxc2UgPiAwICkgXSwgY29lZnMuYmVzdDFzZVsgd2hpY2goIGNvZWZzLmJlc3Qxc2UgPiAwICkgXSApICkKYGBgCgpBbmQgYWdhaW4gOiBpdCdzIGBQQU5TU19wb3NgIHdoaWNoIGlzIHRoZSBzdHJvbmdlc3QgcHJlZGljdG9yIG9mIGBncm91cGAKClRoaXMgdGVsbHMgdXM6CgogIDEuIHVzaW5nIGNvbnRleHQga25vd2xlZGdlIHRvIG1hbnVhbGx5IGRlYnVnIHRoZSBHTE0gc2hvd2VkIHNvbWUgcmVkdW5kYW5jeSwgYnV0IHJlbW92aW5nIGl0IGRpZG4ndCBoZWxwIHdpdGggYSBjbGFzc2ljIEdMTQogIDIuIHdpdGggdGhlIGNvbnRleHQga25vd2xlZGdlICgiaHVtYW4gZXhwZXJ0aXNlIikgYXBwbGllZCwgd2UgdGhlbiBkZWZlciB0byBzb21lIGF1dG9tYXRpYyBmZWF0dXJlIHNlbGVjdGlvbiAoYGdsbW5ldGApIHdoaWNoIHRlbGxzIHVzIGl0J3MgdGhlIGBQQU5TU19wb3NgIHRoYXQgZG9lcyB0aGUgd29yayBpbiBwcmVkaWN0aW5nIGBncm91cGAKICAzLiBUaHJvd2luZyBhd2F5IGh1bWFuIGV4cGVydGlzZSwgYW5kIHVzaW5nIHRoZSBvcmlnaW5hbCB1bmZldGVyZWQgZGF0YSAtLSBkZWZlcnJpbmcgZW50aXJlbHkgdG8gdGhlIGF1dG9tYXRlZCBmZWF0dXJlIHNlbGVjdGlvbiAtLSB3ZSBhcnJpdmUgYXQgdGhlIHNhbWUgYW5zd2VyLgogIAogIAojIyA2LiBBc3N1bWUgUEFOU1Mgc2NvcmVzIGFyZSAqKm5vdCoqIE5lZWRlZCAvIFJlbGV2YW50IApBc3N1bWUgdGhhdCB3ZSB3YW50IHRvIHVzZSB0aGUgYmlvbWFya2VyIGRhdGEsIHdoaWNoIHdlIGhvcGUgaXMgYSBiZXR0ZXIgb3IgbW9yZSBpbmZvcm1hdGl2ZSBwcmVkaWN0b3IgdGhhbiBjbGluaWNhbCBkYXRhLiBXZSBjYW4gc3RyaXAgb3V0IHRoZSBjbGluaWNhbCBkYXRhIGFuZCB0cnkgb24gKipqdXN0KiogdGhlIGJpb21hcmtlcnMuCgpgYGB7cn0KCiMgaWRlbnRpZnkgdGhlIGNsaW5pY2FsIGRhdGEgYW5kIHJlbW92ZSAobm90ZSwgd2UgbGVhdmUgZGVtb2dyYXBoaWMgZGF0YSBpbikKZHJvcENvbHMgPC0gYygiUEFOU1NfcG9zIiwgIlBBTlNTX25lZyIsICJQQU5TU19nZW4iLCAiUEFOU1NfdG90YWwiKQoKZCA8LSBkWyAsIC13aGljaCggbmFtZXMoZCkgJWluJSBkcm9wQ29scyApIF0KCihzdHIoIGQgKSkKCngudmFycyA8LSBuYW1lcyggZCApWzE6KGxlbmd0aChuYW1lcyhkKSktMSldCnkudmFyICA8LSBuYW1lcyggZCApW2xlbmd0aChuYW1lcyhkKSldCgpZIDwtIGRbICwgeS52YXIgXQpYIDwtIGFzLm1hdHJpeCggZFsgLCB4LnZhcnMgXSApCgojIENWIHRoZSBnbG1uZXQgdG8gZmluZCB0aGUgYmVzdCBsYW1iZGEncwpjdi5maXQgPC0gY3YuZ2xtbmV0KCBYLCBZLCBmYW1pbHkgPSAiYmlub21pYWwiLCBhbHBoYSA9IDEsIHR5cGUubWVhc3VyZSA9ICJjbGFzcyIpCnBsb3QoIGN2LmZpdCApCgpgYGAKTm90aWNlIGhvdyB0aGUgbWlzY2xhc3NpZmljYXRpb24gZXJyb3IgaXMgbm93IG11Y2ggaGlnaGVyIG92ZXJhbGwgKDAuMjUsIGFzIGNvbXBhcmVkIHdpdGggMC4xMCB3aGVuIGNsaW5pY2FsIGRhdGEgKip3YXMqKiBpbmNsdWRlZCkgc3VnZ2VzdGluZyB0aGUgYmlvbWFya2VycyBhcmUgbm90IGRvaW5nIGJldHRlciB0aGFuIHRoZSBjbGluaWNhbCBkYXRhLiAgUHJvYmxlbWF0aWNhbGx5LCB0aGUgKipiZXN0KiogdmFsdWVzIGZvciAkXGxhbWJkYSQgYW5kIHRoZSBvbmUtc3RhbmRhcmQgZXJyb3IgcmFuZ2UgYWxsIGluZGljYXRlIGEgbW9kZWwgd2l0aCAqKnplcm8qKiBwcmVkaWN0b3JzLCBzdWdnZXN0aW5nIHRoZSBtb2RlbCBpcyBkZWdlbmVyYXRlLgoKVGhpcyBjb3VsZCBiZSBiZWNhdXNlIHdlIGFyZSBiZWluZyB0b28gInN0cmljdCIgd2l0aCB0aGUgZWxhc3RpYy0gdmVyc3VzIExBU1NPLXBlbmFsdHkuICBBc3N1bWUgdGhhdCByYXRoZXIgdGhhbiB0aGVpciBiZWluZyBhbiBvcHRpbWFsIHNldCBvZiBsaW5lYXJseS1pbmRlcGVuZGVudCBwcmVkaWN0b3JzLCB0aGVyZSBhcmUgaW4gZmFjdCBhIHNtYWxsIHNldCBvZiByZWxhdGl2ZWx5IHdlYWsgcHJlZGljdG9ycyB0aGF0ICJ3b3JrIHRvZ2V0aGVyIiB0byBhc3Npc3QgaW4gcHJlZGljdGlvbi4gIFRoaXMgY2FuIGJlIGNvbnRyb2xsZWQgYnkgYWx0ZXJpbmcgdGhlIHZhbHVlIG9mIHRoZSAkXGFscGhhJCBwYXJhbWV0ZXIgaW4gdGhlIEdMTW5ldCBtb2RlbC4gIEhlcmUncyBob3c6IHNldHRpbmcgJFxhbHBoYSA9IDEkIG1lYW5zIHdlIHdhbnQgYSAic3RyaWN0IiBMQVNTTyByZWd1bGFyaXNhdGlvbiBhbmQgJFxhbHBoYSA9IDAkIGlzIHRoZSBzdHJpY3QgcmlkZ2UtcmVncmVzc2lvbi4gIFRoZSBMQVNTTyB0ZW5kcyB0byBmaW5kIHRoZSBiZXN0IHByZWRpY3RvcnMgYW5kIHRocm93IG91dCBhbGwgb3RoZXJzLiAgVGhlIHJpZGdlIG1vZGVsIGFsbG93cyBmb3IgZ3JvdXBzIG9mIHZhcmlhYmxlcyB0byB3b3JrIHRvZ2V0aGVyLiAgU28gaWYgd2Ugc2V0ICRcYWxwaGEgPSAwLjUkLCAoYW5kIHRoZW4gdmFyeSAkXGxhbWJkYSQgYXMgYmVmb3JlKSwgZ3JvdXBzIG9mIHByZWRpY3RvcnMgd2hpY2ggY292YXJ5IHRvZ2V0aGVyIGFyZSBrZXB0IGluIHRoZSBtb2RlbCBieSBiYWxhbmNpbmcgTEFTU08gYWdhaW5zdCBlbGFzdGljLW5ldCBiZWhhdmlvdXJzIGluIHRoZSBtb2RlbCBmaXR0aW5nIChmZWF0dXJlIHNlbGVjdGlvbikuICBUaGlzIG1heSBvciBtYXkgbm90IGltcHJvdmUgdGhlIG1vZGVsIHBlcmZvcm1hbmNlLCBidXQgaXQgd2lsbCBsaWtlbHkga2VlcCB0aGUgbnVtYmVyIG9mIHByZWRpY3RvcnMgaW5jbHVkZWQgPiAwLiAgVGhlIGF1dGhvcnMgb2YgR0xNbmV0IHJlY29tbWVuZCB0aGF0IHNldHRpbmcgJFxhbHBoYSA9IDEgLSBcZXBzaWxvbiQgKHdoZXJlICRcZXBzaWxvbiQgaXMgc29tZSBzbWFsbCB2YWx1ZSAgJD4gMCQpIG1lYW5zIHByZWRpY3RvcnMgYXJlIGluY2x1ZGVkIHVzaW5nIExBU1NPLCBidXQgZGVnZW5lcmF0ZSBvciBleHRyZW1lIGNvcnJlbGF0aW9ucyBpbiB2YXJpYWJsZXMgYXJlIHRocm93biBvdXQuIAoKYGBge3J9CiMgdGhlIG1vZGVsIHdpdGggYWxwaGEgPSAwLjUKY3YuZml0IDwtIGN2LmdsbW5ldCggWCwgWSwgZmFtaWx5ID0gImJpbm9taWFsIiwgYWxwaGEgPSAwLjUsIHR5cGUubWVhc3VyZSA9ICJjbGFzcyIpCnBsb3QoIGN2LmZpdCApCmBgYAoKU3RpbGwgdGhlIHNhbWUgcHJvYmxlbS4gIExldCdzIG5vdyB0cnkgdGhlICRcZXBzaWxvbiQgYXBwcm9hY2ggdG8gcmVtb3ZlIGRlZ2VuZXJhY3kgYnV0IGtlZXAgdGhlIHJvYnVzdG5lc3Mgb2YgTEFTU086CgpgYGB7cn0KIyB0aGUgbW9kZWwgd2l0aCBhbHBoYSA9IDAuMDUKY3YuZml0IDwtIGN2LmdsbW5ldCggWCwgWSwgZmFtaWx5ID0gImJpbm9taWFsIiwgYWxwaGEgPSAwLjA1LCB0eXBlLm1lYXN1cmUgPSAiY2xhc3MiKQpwbG90KCBjdi5maXQgKQpgYGAKU2FtZSBwcm9ibGVtcyAtLSBvZiBub3RlLCByZXJ1bm5pbmcgdGhlc2UgYWJvdmUgdHdvIGFuYWx5c2VzIGdpdmVzIGRpZmZlcmVudCByZXN1bHRzIGVhY2ggdGltZSAodGhleSBhcmUgc3RvY2hhc3RpYyBhbGdvcml0aG1zKSBidXQgdGhlICByZXBlYXRhYmlsaXR5IGNhbiBiZSB0ZXN0ZWQgYnkgcmUtcnVubmluZyBhbmQga2VlcGluZyB0cmFjayBvZiB0aGUgcHJlZGljdG9ycyB0aGF0IGFyZSBpbmNsdWRlZC4gIE5vdGUgdGhhdCBhIG5vdGlvbiBvZiAicCB2YWx1ZXMiIG9uIHRoZSBjb2VmZmljaWVudHMgaW4gYSBHTE1uZXQgaXMgc29tZXdoYXQgbW9vdCAtLSBpbnN0ZWFkLCB3ZSByZWx5IG9uIHJlc2FtcGxpbmcgdG8gZmluZCBvdXQgaWYgdGhlIHByZWRpY3RvcnMgYXJlIGNvbnNpc3RlbnRseSBhc3NvY2lhdGVkIHdpdGggbWlzY2xhc3NpZmljYXRpb24gZXJyb3IuICAKCkxldCdzIHRyeSB0aGF0LCBhc3N1bWluZyB0aGF0IHNvbWUgb2NjYXNpb25hbCBzdG9jaGFzdGljIGZhY3RvcnMgYXJlIGNhdXNpbmcgdGhlIHNvbHV0aW9uIHRvIHZhcnkgYW5kIHdlJ2xsIHJlY29yZCB0aGUgcHJlZGljdG9ycyBmb3VuZCB0byBiZSB1c2VmdWwgb24gZWFjaCBydW4gb2YgdGhlIGVzdGltYXRpb24gdG8gc2VlIGlmIHRoZXJlJ3MgYSBwYXR0ZXJuLCBhbmQgdG8gZG8gdGhpcywgd2UnbGwgdXNlIG9ubHkgdGhlIGJlc3QgJFxsYW1iZGEkOgoKYGBge3J9CiMgV2FybmluZyA6IHRoaXMgdGFrZXMgYWJvdXQgMTAgbWludXRlcyB0byBydW4gb24gYSBzaW5nbGUgQ1BVCk4uc2ltcyA8LSAxMDAwMAp0YWJSZXN1bHRzIDwtIGRhdGEuZnJhbWUoIFZhciA9IGNvbG5hbWVzKFgpLCBJbmNsdWRlZCA9IHJlcCgwLGxlbmd0aChjb2xuYW1lcyhYKSkpICkKdGFiRXJyb3IgICA8LSByZXAoMCwgTi5zaW1zKQoKZm9yICggaSBpbiAxOk4uc2ltcyApIHsKICBjdi5maXQgPC0gY3YuZ2xtbmV0KCBYLCBZLCBmYW1pbHkgPSAiYmlub21pYWwiLCBhbHBoYSA9IDAuMDUsIHR5cGUubWVhc3VyZSA9ICJjbGFzcyIpCiAgYmVzdGxhbWJkYSA8LSBjdi5maXQkbGFtYmRhLm1pbgogIGNvZWZzLmJlc3RMYW1iZGEgPC0gcm91bmQoIGFzLm1hdHJpeCggY29lZihjdi5maXQsIHMgPSBiZXN0bGFtYmRhICkgKSwgNiApCiAgCiAgIyBmaW5kIG5vbi16ZXJvIGNvZWZmaWNpZW50cyAoYXQgNiBkZWMuIHBsYWNlcyBwcmVjaXNpb24pIC0tIGV4Y2x1ZGluZyB0aGUgaW50ZXJjZXB0CiAgbm9uWmVybyA8LSB3aGljaCggY29lZnMuYmVzdExhbWJkYVsyOm5yb3coY29lZnMuYmVzdExhbWJkYSldICE9IDApCiAgCiAgaWYoIGxlbmd0aCggbm9uWmVybyApID4gMCApIHsgICMjIGlmIHRoZXJlIGFyZSBub24temVybyBjb2VmZmljaWVudHMKICAgICMgaW5jcmVtZW50IGNvdW50cwogICAgdGFiUmVzdWx0cyRJbmNsdWRlZFsgbm9uWmVybyBdIDwtIHRhYlJlc3VsdHMkSW5jbHVkZWRbIG5vblplcm8gXSArIDEKICB9CiAgCiAgIyBzdG9yZSB0aGUgYmVzdCBDViBtaXNjbGFzc2lmaWNhdGlvbiBlcnJvcgogIHRhYkVycm9yW2ldIDwtIGN2LmZpdCRjdm1bIHdoaWNoKCBjdi5maXQkbGFtYmRhID09IGJlc3RsYW1iZGEgKSBdCn0KYGBgCgpUaGUgbWVhbiBjbGFzc2lmaWNhdGlvbiBlcnJvciB3b3JrcyBvdXQgdG8gYmUgd2l0aCBzdGFuZGFyZCBkZXZpYXRpb246CmBgYHtyfQoobWVhbiggdGFiRXJyb3IgKSkKKHNkKHRhYkVycm9yKSkKYGBgCgpFeGFtaW5pbmcgdGhlIHRhYmxlIG9mIHByZWRpY3RvcnMgd2hpY2ggYXJlIGlubHVkZWQgLyBleGNsdWRlZCBvbiBlYWNoIG9mIDEwLDAwMCBydW5zOgpgYGB7cn0KKHRhYlJlc3VsdHMpCmBgYApXZSBjYW4gc2VlIHRoYXQgb24gYXBycm94aW1hdGVseSBoYWxmIHRoZSBydW5zICoqYWxsKiogcHJlZGljdG9ycyBhcmUgaW5jbHVkZWQgaW4gdGhlIG1vZGVsLCBhbmQgb24gdGhlIG90aGVyIDUwMDArIHJ1bnMsIG5vbmUgYXJlIGluY2x1ZGVkICh0aGF0J3Mgd2h5IHRoZXkgYWxsIGhhdmUgNDc2NCBhcyB0aGVpciBjb3VudHMgLSB0aGV5IHdlcmUgYWxsIGluLCBvciBvdXQsIHRvZ2V0aGVyKS4gIGBHbHUuY3JlYCBjcmVlcHMgaW4gZm9yIGEgdmVyeSBzbWFsbCBwcm9wb3J0aW9uOiAwLjQ2JSBvZiB0aGUgbW9kZWxzIHVzZWQgdGhpcyBwcmVkaWN0b3IpLgoKVGhpcyB0ZWxscyB1cyB0aGF0IHRoZSBiZXN0IGVycm9yIHdlJ2xsIGdldCBpcyAwLjI1IChzbyAyNSUgb2YgdGhlIHNhbXBsZSBhcmUgaW5jb3JyZWN0bHkgY2xhc3NpZmllZCkgIC0tIHRoZXJlIGFyZSA1NyBmb3IgYGdyb3VwID0gMGAgYW5kIDIwIGZvciBgZ3JvdXAgPSAxYC4gIFNvIGFzc3VtaW5nIHN5bW1ldHJpYyBlcnJvcnMgKGUuZy4gd2UgZ2V0IDI1JSB3cm9uZyBlcXVhbGx5IGluIGJvdGggZ3JvdXBzKSB0aGlzIG1lYW5zIDE0IHBlb3BsZSBhcmUgY2xhc3NpZmllZCBpbmNvcnJlY3RseSBpbiBgZ3JvdXAgPSAwYCBhbmQgNSBhcmUgaW5jb3JyZWN0bHkgY2xhc3NpZmllZCBpbiBgZ3JvdXAgPSAxYCAtLSBhbmQgdGhlc2UgY2FuIGJlIHdpdGggZWl0aGVyIG5vbmUsIG9yIGFsbCBvZiB0aGUgcHJlZGljdG9ycyBpbmNsdWRlZC4gIAoKVGhpcyBzdWdnZXN0cyAtLSBldmVuIHdpdGggYWNjZXNzIHRvICphbGwqIHRoZSBkYXRhIGZvciB0cmFpbmluZyBhbmQgdGVzdGluZyAodmFsaWRhdGluZykgdGhlIG1vZGVsLCB3ZSBjYW4ndCBmaW5kIHZlcnkgcm9idXN0IHByZWRpY3RvcnMgYmV5b25kIHRoZSBgUEFOU1NfcG9zYCB2YXJpYWJsZSB3aGljaCBvdXRwZXJmb3JtcyBhbGwgdGhlIGJpb21hcmtlcnMgKHJlY2FsbCwgdGhlIG1pbmltdW0gbWlzY2xhc3NpZmljYXRpb24gZXJyb3Igd2FzIDAuMSBmb3IgYFBBTlNTX3Bvc2ApLgoK