This Vaar R Markdown Notebook using Python aims to return:
- Whether data is more likely to be MCAR or MAR/MNAR
- A recommended approach for managing missing data that considers stability of model results
- Evaluation of imputation efforts based on data model experiment results

This particular Vaar notebook uses UCI’s Breast Cancer Wisconsin Original Data as an exemplar.

Step 1: Read in data and show data summary

library(reticulate)
py_available(TRUE)
py_install("pandas")
reticulate::repl_python()
py_install("numpy")

Print summary statistics for dataframe.

import numpy as np
print(df.describe())
                 0           1           2   ...          8           9           10
count  6.990000e+02  699.000000  699.000000  ...  699.000000  699.000000  699.000000
mean   1.071704e+06    4.417740    3.134478  ...    2.866953    1.589413    2.689557
std    6.170957e+05    2.815741    3.051459  ...    3.053634    1.715078    0.951273
min    6.163400e+04    1.000000    1.000000  ...    1.000000    1.000000    2.000000
25%    8.706885e+05    2.000000    1.000000  ...    1.000000    1.000000    2.000000
50%    1.171710e+06    4.000000    1.000000  ...    1.000000    1.000000    2.000000
75%    1.238298e+06    6.000000    5.000000  ...    4.000000    1.000000    4.000000
max    1.345435e+07   10.000000   10.000000  ...   10.000000   10.000000    4.000000

[8 rows x 11 columns]

Count number of observations in each class if classification problem.

diagnosis = df.groupby(10)
diagnosis.count()
      0    1    2    3    4    5    6    7    8    9
10                                                  
2   458  458  458  458  458  458  444  458  458  458
4   241  241  241  241  241  241  239  241  241  241

Step 2: Tidy Data

#rename variables to something more meaningful in copy of original df
dfcp = df
cols = df.columns
df.rename(columns = {cols[0]:'sampleID',cols[1]:'ClumpThickness',cols[2]:'CellSize', cols[3]:'CellShape',cols[4]:'Adhesion',cols[5]:'SingleESize',cols[6]:'BareNuclei', cols[7]:'BlandChromatin', cols[8]:'NormalNucleoli', cols[9]:'Mitoses', cols[10]:'Diagnosis'}, inplace=True)

Get the type for variables in the dataframe.

dfcp.dtypes
sampleID            int64
ClumpThickness      int64
CellSize            int64
CellShape           int64
Adhesion            int64
SingleESize         int64
BareNuclei        float64
BlandChromatin      int64
NormalNucleoli      int64
Mitoses             int64
Diagnosis           int64
dtype: object

Check for and remove any duplicate observations if there is a unique identifier. Comment out if no identifier.

duplicates = dfcp.duplicated(keep='first')
print('Duplicates:')
Duplicates:
print(duplicates)
0      False
1      False
2      False
3      False
4      False
       ...  
694    False
695    False
696    False
697    False
698    False
Length: 699, dtype: bool
print('\n')
print('DataFrame after keeping only the first instance of the duplicate rows:')
DataFrame after keeping only the first instance of the duplicate rows:
dfcp[~duplicates]
     sampleID  ClumpThickness  CellSize  ...  NormalNucleoli  Mitoses  Diagnosis
0     1000025               5         1  ...               1        1          2
1     1002945               5         4  ...               2        1          2
2     1015425               3         1  ...               1        1          2
3     1016277               6         8  ...               7        1          2
4     1017023               4         1  ...               1        1          2
..        ...             ...       ...  ...             ...      ...        ...
694    776715               3         1  ...               1        1          2
695    841769               2         1  ...               1        1          2
696    888820               5        10  ...              10        2          4
697    897471               4         8  ...               6        1          4
698    897471               4         8  ...               4        1          4

[691 rows x 11 columns]

Remove unique identifier from dataframe as this will not add any value to model.

dfcp = dfcp.drop(columns=['sampleID'])

Step 3: Visualise Missing Data and Run MCAR Test

Count missing values in each column.

dfcp.isna().sum()
ClumpThickness     0
CellSize           0
CellShape          0
Adhesion           0
SingleESize        0
BareNuclei        16
BlandChromatin     0
NormalNucleoli     0
Mitoses            0
Diagnosis          0
dtype: int64
len(dfcp)
699
missingness = 16/699

Use seaborn module to visualise missing data.

py_install("seaborn")
import seaborn as sns

Install matplotlib module also.

py_install("matplotlib==3.6")

Import matplotlib.

import matplotlib.pyplot as plt

Visualise missing data patterns in matrix.

plt.figure(figsize=(10,6))
<Figure size 1000x600 with 0 Axes>
sns.heatmap(dfcp.isna().transpose(), cmap="Blues", cbar_kws={'label': 'Missing Data'})
<AxesSubplot: >
plt.show()

Run MCAR Test.

py_install("scipy")
py_install("r-naniar")

The following code, including Little’s MCAR Test, has been performed in R, as no equivalent exists in Python yet (August 2023). There will be a warning with MCAR test if non-numeric columns are present. If p-value >0.05 likely to be MCAR as not statistically significant.
The MCAR Test may error if data is singular, there is no fix available for this yet (August 2023).

library(naniar)
library(reticulate)
mcar_test(py$dfcp)

Step 4: Look at Outputs from Visualising the Missing Data and Answer the Following Questions

What is the p.value returned by the MCAR Test?

If the statistic is high and the p.value <0.05 it is likely to be MNAR or MAR and cannot be ignored. A p-value >0.05 indicates MCAR but other information will be considered also. The value is set as a variable in the following code.

#set MCAR Test p.value as variable
mcar_presult = 0.2133409

if mcar_presult=="error" or  isinstance(mcar_presult, (int, float, complex)):
  print(f"{mcar_presult} is MCAR Test p.value variable value.")
else:
  print("Please enter either a number or 'error' as the mcar_presult variable value.")
    
0.2133409 is MCAR Test p.value variable value.

Is data more likely to be missing in some variables?

If so, this indicates that the data could be missing at random (MAR) or missing not at random (MNAR) and can therefore not be ignored. Yes or No is set as a variable in the following code.

#set likelihood variable
likelihood = "Yes"

if likelihood=="Yes" or  likelihood=="No":
  print(f"{likelihood} is likelihood variable value.")
else:
  print("Please enter either 'Yes' or 'No' as the likelihood variable value.")
Yes is likelihood variable value.

Is data missing from the dependent variable only?

If so, this suggests that complete case analysis may be the best option. However, other factors need to be considered also. Yes or No is set as variable in the following code.

#set dependent missingness variable
dependent_only = "No"

if dependent_only=="Yes" or  dependent_only=="No":
  print(f"{dependent_only} is dependent_only variable value.")
else:
  print("Please enter either 'Yes' or 'No' as the dependent_only variable value.")
  
No is dependent_only variable value.

What percentage of data is missing?

If it’s between 20-50% multiple imputation is likely to result in a more effective, unbiased model. The value is set as a variable in the following code.

#print missingness variable value
print(missingness)
0.022889842632331903

It’s good practice to test different approaches and the Vaar Notebooks will consider these, as well as the variable values just set in recommendations.

Visualise Correlation

Visualise correlation for numerical variables.

n=10 #number of columns
names = ['ClumpThickness','CellSize','CellShape','Adhesion','SingleESize','BareNuclei','BlandChromatin','NormalNucleoli','Mitoses','Diagnosis'] #col names
fig = plt.matshow(dfcp.corr(),)
ax = plt.gca()

ax.set_xticks(np.arange(n))
[<matplotlib.axis.XTick object at 0x00000192F1FF6220>, <matplotlib.axis.XTick object at 0x00000192F1FF6E80>, <matplotlib.axis.XTick object at 0x0000019284A26070>, <matplotlib.axis.XTick object at 0x0000019284A5E2E0>, <matplotlib.axis.XTick object at 0x0000019287792940>, <matplotlib.axis.XTick object at 0x00000192F1F96C40>, <matplotlib.axis.XTick object at 0x0000019284A39C10>, <matplotlib.axis.XTick object at 0x0000019287792EB0>, <matplotlib.axis.XTick object at 0x000001928779A9A0>, <matplotlib.axis.XTick ob
ax.set_xticklabels(names)
ject at 0x000001928779F490>]
[Text(0, 1, 'ClumpThickness'), Text(1, 1, 'CellSize'), Text(2, 1, 'CellShape'), Text(3, 1, 'Adhesion'), Text(4, 1, 'SingleESize'), Text(5, 1, 'BareNuclei'), Text(6, 1, 'BlandChromatin'), Text(7, 1, 'NormalNucleoli'), Text(8, 1, 'Mitoses'), Text(9, 1, 'Diagnosis')]
ax.set_yticks(np.arange(n))
[<matplotlib.axis.YTick object at 0x00000192F1E34970>, <matplotlib.axis.YTick object at 0x00000192F1FF6E20>, <matplotlib.axis.YTick object at 0x00000192848123D0>, <matplotlib.axis.YTick object at 0x00000192877A6280>, <matplotlib.axis.YTick object at 0x00000192877AEA60>, <matplotlib.axis.YTick object at 0x00000192877A6FA0>, <matplotlib.axis.YTick object at 0x000001928779F850>, <matplotlib.axis.YTick object at 0x000001928779AD00>, <matplotlib.axis.YTick object at 0x00000192877B6AF0>, <matplotlib.axis.YTick object at 0x00000192877BA5E0>]
[Text(0, 0, 'ClumpThickness'), Text(0, 1, 'CellSize'), Text(0, 2, 'CellShape'), Text(0, 3, 'Adhesion'), Text(0, 4, 'SingleESize'), Text(0, 5, 'BareNuclei'), Text(0, 6, 'BlandChromatin'), Text(0, 7, 'NormalNucleoli'), Text(0, 8, 'Mitoses'), Text(0, 9, 'Diagnosis')]
plt.setp([tick.label2 for tick in ax.xaxis.get_major_ticks()], rotation=40,
         ha="left", va="center",rotation_mode="anchor")
[None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None]
plt.show()

Use Seaborn’s PairGrid to visualise interactions between variables.

#use seaborn's PairGrid to visualise interactions between variables.
pairs = sns.PairGrid(dfcp, hue="Diagnosis")
pairs.map_diag(sns.histplot)
pairs.map_offdiag(sns.scatterplot)
<seaborn.axisgrid.PairGrid object at 0x00000192F1FE0880>
pairs.add_legend()
<seaborn.axisgrid.PairGrid object at 0x00000192F1FE0880>
plt.show()

Calculate Skew

Install and import scipy.

py_install("scipy")
import scipy as sci
from scipy.stats import skew

Calculate and print skewness. For variables with missing data ‘nan’ will be returned.

print(skew(dfcp, axis = 0, bias=True))
[0.59158554 1.23048876 1.15936443 1.52119475 1.70849542        nan
 1.09760722 1.41920737 3.55301239 0.65315834]

Step 5: which variables and values are important for predicting proportion of missingness?

Naniar (R) used for prop_miss functions, to add to dataframe temporarily for use in simple decision tree.

library(rpart)
library(rpart.plot)
py$dfcp %>%
  add_prop_miss() %>%
  rpart(prop_miss_all ~ ., data=.) %>%
  prp(type=4, extra = 101, roundint=FALSE, prefix="Prop.Miss = ")

Which numerical variable with missing data is nearest the top of the tree?

Set variable value for missingness influencer in code below.

#set value for missingness influencer
miss_influencer="BareNuclei"

Step 6: Create Complete Case and Iterative Imputation Datasets

Create complete dataset by deleting records with missing values and run iterative chained equations which return single imputation instead of multiple.

#Create CCA data
dfcp_cca = dfcp.copy()
dfcp_cca = dfcp_cca.dropna()
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
dfcp_mi = IterativeImputer(random_state=0, max_iter=5).fit_transform(dfcp)

Check that there is no missing data in new datasets.

#convert array to dataframe
dfcp_mi = pd.DataFrame(dfcp_mi)
print(dfcp_cca.isna().sum())
ClumpThickness    0
CellSize          0
CellShape         0
Adhesion          0
SingleESize       0
BareNuclei        0
BlandChromatin    0
NormalNucleoli    0
Mitoses           0
Diagnosis         0
dtype: int64
print(dfcp_mi.isna().sum())
0    0
1    0
2    0
3    0
4    0
5    0
6    0
7    0
8    0
9    0
dtype: int64

Add column names to imputed dataframe.

dfcp_imp = dfcp_mi
cols = dfcp_imp.columns
dfcp_imp.rename(columns = {cols[0]:'ClumpThickness',cols[1]:'CellSize', cols[2]:'CellShape',cols[3]:'Adhesion',cols[4]:'SingleESize',cols[5]:'BareNuclei', cols[6]:'BlandChromatin', cols[7]:'NormalNucleoli', cols[8]:'Mitoses', cols[9]:'Diagnosis'}, inplace=True)

Step 7: Conduct MNAR Sensitivity Test

Use the variable with missing data which has the most influence on proportion of missingness, according to step 5 and look at the range of values in this variable. Generate imputations under delta adjustment to imitate deviations from MAR (Gink and Van Buuren, no date). The code uses 0 and a reasonable range based on the variable range as the delta values.

#Pull out index of NaN from original data
df[df['BareNuclei'].isna()]
     sampleID  ClumpThickness  CellSize  ...  NormalNucleoli  Mitoses  Diagnosis
23    1057013               8         4  ...               3        1          4
40    1096800               6         6  ...               8        1          2
139   1183246               1         1  ...               1        1          2
145   1184840               1         1  ...               1        1          2
158   1193683               1         1  ...               1        1          2
164   1197510               5         1  ...               1        1          2
235   1241232               3         1  ...               1        1          2
249    169356               3         1  ...               1        1          2
275    432809               3         1  ...               1        1          2
292    563649               8         8  ...              10        1          4
294    606140               1         1  ...               1        1          2
297     61634               5         4  ...               3        1          2
315    704168               4         6  ...               9        1          2
321    733639               3         1  ...               1        1          2
411   1238464               1         1  ...               1        1          2
617   1057067               1         1  ...               1        1          2

[16 rows x 11 columns]
#subset these rows and variable identified from imputed data set
delta0 = dfcp_mi.iloc[[23,40,139,145,158,164,235,249,275,292,294,297,315,321,411,617],[5]] 
#set delta values then set upper clip
#create new values by delta increase
delta=[0,1,2,3,4]
delta1 = delta0.copy()
delta1.loc[delta1['BareNuclei']<10, 'BareNuclei']+=1

Repeat for other delta values

delta2 = delta1.copy()
delta2.loc[delta2['BareNuclei']<10, 'BareNuclei']+=1
delta2.clip(upper=10)
     BareNuclei
23     9.116345
40     5.627595
139    3.186619
145    3.588273
158    3.262360
164    3.479783
235    3.992673
249    3.423504
275    3.644552
292    8.325052
294    3.208827
297    3.068113
315    4.168468
321    3.423504
411    3.186619
617    3.028221
delta3 = delta2.copy()
delta3.loc[delta3['BareNuclei']<10, 'BareNuclei']+=1
delta3 = delta3.clip(upper=10)

delta4 = delta3.copy()
delta4.loc[delta4['BareNuclei']<10, 'BareNuclei']+=1
delta4 = delta4.clip(upper=10)

Replace NaN with increased values to test sensitivity. Example below to change based on outputs created above.

dfcp_mi_d1 = dfcp.copy()
dfcp_mi_d1.iloc[[23],[5]] = 8.116345
dfcp_mi_d1.iloc[[40],[5]] = 4.627595
dfcp_mi_d1.iloc[[139],[5]] = 2.186619
dfcp_mi_d1.iloc[[145],[5]] = 2.588273
dfcp_mi_d1.iloc[[158],[5]] = 2.262360
dfcp_mi_d1.iloc[[164],[5]] = 2.479783
dfcp_mi_d1.iloc[[235],[5]] = 2.992673
dfcp_mi_d1.iloc[[249],[5]] = 2.423504
dfcp_mi_d1.iloc[[275],[5]] = 2.644552
dfcp_mi_d1.iloc[[292],[5]] = 7.325052
dfcp_mi_d1.iloc[[294],[5]] = 2.208827
dfcp_mi_d1.iloc[[297],[5]] = 2.068113
dfcp_mi_d1.iloc[[315],[5]] = 3.168468
dfcp_mi_d1.iloc[[321],[5]] = 2.423504
dfcp_mi_d1.iloc[[411],[5]] = 2.186619
dfcp_mi_d1.iloc[[617],[5]] = 2.028221

Repeat for other delta values.

dfcp_mi_d2 = dfcp_mi_d1.copy()
dfcp_mi_d2.iloc[[23],[5]] = 9.116345
dfcp_mi_d2.iloc[[40],[5]] = 5.627595
dfcp_mi_d2.iloc[[139],[5]] = 3.186619
dfcp_mi_d2.iloc[[145],[5]] = 3.588273
dfcp_mi_d2.iloc[[158],[5]] = 3.262360
dfcp_mi_d2.iloc[[164],[5]] = 3.479783
dfcp_mi_d2.iloc[[235],[5]] = 3.992673
dfcp_mi_d2.iloc[[249],[5]] = 3.423504
dfcp_mi_d2.iloc[[275],[5]] = 3.644552
dfcp_mi_d2.iloc[[292],[5]] = 8.325052
dfcp_mi_d2.iloc[[294],[5]] = 3.208827
dfcp_mi_d2.iloc[[297],[5]] = 3.068113
dfcp_mi_d2.iloc[[315],[5]] = 4.168468
dfcp_mi_d2.iloc[[321],[5]] = 3.423504
dfcp_mi_d2.iloc[[411],[5]] = 3.186619
dfcp_mi_d2.iloc[[617],[5]] = 3.028221
dfcp_mi_d3 = dfcp_mi_d2.copy()
dfcp_mi_d3.iloc[[23],[5]] = 10
dfcp_mi_d3.iloc[[40],[5]] = 6.627595
dfcp_mi_d3.iloc[[139],[5]] = 4.186619
dfcp_mi_d3.iloc[[145],[5]] = 4.588273
dfcp_mi_d3.iloc[[158],[5]] = 4.262360
dfcp_mi_d3.iloc[[164],[5]] = 4.479783
dfcp_mi_d3.iloc[[235],[5]] = 4.992673
dfcp_mi_d3.iloc[[249],[5]] = 4.423504
dfcp_mi_d3.iloc[[275],[5]] = 4.644552
dfcp_mi_d3.iloc[[292],[5]] = 9.325052
dfcp_mi_d3.iloc[[294],[5]] = 4.208827
dfcp_mi_d3.iloc[[297],[5]] = 4.068113
dfcp_mi_d3.iloc[[315],[5]] = 5.168468
dfcp_mi_d3.iloc[[321],[5]] = 4.423504
dfcp_mi_d3.iloc[[411],[5]] = 4.186619
dfcp_mi_d3.iloc[[617],[5]] = 4.028221
dfcp_mi_d4 = dfcp_mi_d3.copy()
dfcp_mi_d4.iloc[[23],[5]] = 10
dfcp_mi_d4.iloc[[40],[5]] = 7.627595
dfcp_mi_d4.iloc[[139],[5]] = 5.186619
dfcp_mi_d4.iloc[[145],[5]] = 5.588273
dfcp_mi_d4.iloc[[158],[5]] = 5.262360
dfcp_mi_d4.iloc[[164],[5]] = 5.479783
dfcp_mi_d4.iloc[[235],[5]] = 5.992673
dfcp_mi_d4.iloc[[249],[5]] = 5.423504
dfcp_mi_d4.iloc[[275],[5]] = 5.644552
dfcp_mi_d4.iloc[[292],[5]] = 10
dfcp_mi_d4.iloc[[294],[5]] = 5.208827
dfcp_mi_d4.iloc[[297],[5]] = 5.068113
dfcp_mi_d4.iloc[[315],[5]] = 6.168468
dfcp_mi_d4.iloc[[321],[5]] = 5.423504
dfcp_mi_d4.iloc[[411],[5]] = 5.186619
dfcp_mi_d4.iloc[[617],[5]] = 5.028221

Optional Step: Remove Outliers

There can be valuable information in outliers and it is good practice to remove obvious errors only - such as impossible values for particular variables. Outliers could be an accurate representation of the natural variability in data and reflective of diagnostic challenge.

Step 8: Select Features

This code is an example of how to determine which variables have above 0.9 correlation, as highly-correlated features do not generally improve models. Then, remove any variables identified from all datasets.

#select highly correlated features
#remove target variables from consideration
dfcp_mi_cor = dfcp_mi.copy()
dfcp_mi_cor = dfcp_mi_cor.drop(columns=['Diagnosis'])
cor = dfcp_mi_cor.corr()
print(cor)
                ClumpThickness  CellSize  ...  NormalNucleoli   Mitoses
ClumpThickness        1.000000  0.644913  ...        0.535835  0.350034
CellSize              0.644913  1.000000  ...        0.722865  0.458693
CellShape             0.654589  0.906882  ...        0.719446  0.438911
Adhesion              0.486356  0.705582  ...        0.603352  0.417633
SingleESize           0.521816  0.751799  ...        0.628881  0.479101
BareNuclei            0.595850  0.691747  ...        0.582114  0.340126
BlandChromatin        0.558428  0.755721  ...        0.665878  0.344169
NormalNucleoli        0.535835  0.722865  ...        1.000000  0.428336
Mitoses               0.350034  0.458693  ...        0.428336  1.000000

[9 rows x 9 columns]

Remove from datasets if over 0.9.

dfcp_mi = dfcp_mi.drop(columns=['CellSize'])
dfcp_mi_d1 = dfcp_mi_d1.drop(columns=['CellSize'])
dfcp_mi_d2 = dfcp_mi_d2.drop(columns=['CellSize'])
dfcp_mi_d3 = dfcp_mi_d3.drop(columns=['CellSize'])
dfcp_mi_d4 = dfcp_mi_d4.drop(columns=['CellSize'])
dfcp_cca = dfcp_cca.drop(columns=['CellSize'])

Optional step: Transform data

Scaling data allows models to compare relative relationships between data points more effectively. Some datasets will already be scaled like this one.

Log transformation can be helpful for linear models for example.

#from sklearn.preprocessing import MinMaxScaler
#scaler = MinMaxScaler()
#dfcp_mi = scaler.fit_transform(dfcp_mi)
#dfcp_cca = scaler.fit_transform(dfcp_cca)
#dfcp_mi_d4 = scaler.fit_transform(dfcp_mi_d4)

Step 9: Build and Test Models

This notebook uses a Random Forest model as an example.

Create Training and Test Sets

Using train-test split procedure from Scikit-Learn, creating x and y values for each dataset first.

#for imputed dataset
#change class to factor also if required
from sklearn.model_selection import train_test_split
x = dfcp_mi.drop(columns=['Diagnosis'])
y = dfcp_mi['Diagnosis'].astype('category')
np.random.seed(3) #for reproducibility
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.3)

Repeat for other datasets that will be used to train and test models.

#cca 
xcca = dfcp_cca.drop(columns=['Diagnosis'])
ycca = dfcp_cca['Diagnosis'].astype('category')
np.random.seed(3) #for reproducibility
xcca_train, xcca_test, ycca_train, ycca_test = train_test_split(xcca, ycca, test_size=0.3)

#delta4
xd4 = dfcp_mi_d4.drop(columns=['Diagnosis'])
yd4 = dfcp_mi_d4['Diagnosis'].astype('category')
np.random.seed(3) #for reproducibility
xd4_train, xd4_test, yd4_train, yd4_test = train_test_split(xd4, yd4, test_size=0.3)

Predict Against Test Data

Start with baseline model using iterative imputation.

from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import confusion_matrix
# random forest model creation
rfc = RandomForestClassifier()
rfc.fit(x_train,y_train)
RandomForestClassifier()
# predictions against test
np.random.seed(3)
predictions = rfc.predict(x_test)

#evaluate using confusion matrix
tn, fp, fn, tp = confusion_matrix(y_test, predictions).ravel()
sens_imp = tp/(tp+fn)
spec_imp = tn/(fp+tn)
acc_imp = (tp+tn)/(tp+tn+fp+fn)
miss_preds = fp+fn
print(f'Sensitivity for imputed model is {sens_imp}.')
Sensitivity for imputed model is 0.975.
print(f'Specificity for imputed model is {spec_imp}.')
Specificity for imputed model is 0.9615384615384616.
print(f'Accuracy for imputed model is {acc_imp}.')
Accuracy for imputed model is 0.9666666666666667.
print(f'The imputed model missclassified {miss_preds} observations in test.')
The imputed model missclassified 7 observations in test.

Repeat model tests with CCA data.

#define model
rfc1 = RandomForestClassifier()
rfc1.fit(xcca_train,ycca_train)
RandomForestClassifier()
# predictions against test
np.random.seed(3)
predictions_cca = rfc1.predict(xcca_test)

#evaluate using confusion matrix
tn1, fp1, fn1, tp1 = confusion_matrix(ycca_test, predictions_cca).ravel()

#Optional calculations for classification models
sens_cca = tp1/(tp1+fn1)
spec_cca = tn1/(fp1+tn1)
acc_cca = (tp1+tn1)/(tp1+tn1+fp1+fn1)
miss_cca = fp1+fn1
print(f'Sensitivity for CCA model is {sens_cca}.')
Sensitivity for CCA model is 0.935064935064935.
print(f'Specificity for CCA model is {spec_cca}.')
Specificity for CCA model is 0.96875.
print(f'Accuracy for CCA model is {acc_cca}.')
Accuracy for CCA model is 0.9560975609756097.
print(f'The CCA model missclassified {miss_cca} observations in test.')
The CCA model missclassified 9 observations in test.

Which Model Produced the Best Results?

The following code block captures the model which produced the best results, based on core metric (Sensitivity) as this is a minority classification problem.

# set model variable for best performance as MI or CCA
better_model = 'MI'

if better_model=="CCA" or  better_model=="MI":
  print(f"{better_model} is the better model based on this data experiment.")
else:
  print("Please enter either 'MI' or 'CCA' for this variable.")
MI is the better model based on this data experiment.

Does Highest Delta Adjustment Have Big Impact on Results?

#define and fit model
rfc4 = RandomForestClassifier()
rfc4.fit(xd4_train,yd4_train)
RandomForestClassifier()
#predict against test
np.random.seed(3)
predictions_d4 = rfc4.predict(xd4_test)

#evaluate using confusion matrix
tn4, fp4, fn4, tp4 = confusion_matrix(yd4_test, predictions_d4).ravel()

sens_d4 = tp4/(tp4+fn4) #tpr
spec_d4 = tn4/(fp4+tn4)
acc_d4 = (tp4+tn4)/(tp4+tn4+fp4+fn4)
miss_d4 = fp4+fn4
print(f'Sensitivity for delta 4 model is {sens_d4}.')
Sensitivity for delta 4 model is 0.9625.
print(f'Specificity for delta 4 model is {spec_d4}.')
Specificity for delta 4 model is 0.9615384615384616.
print(f'Accuracy for delta 4 model is {acc_d4}.')
Accuracy for delta 4 model is 0.9619047619047619.
print(f'The delta 4 model missclassified {miss_d4} observations in test.')
The delta 4 model missclassified 8 observations in test.

The impact result is captured as a Yes or No response in the code below.

delta_impact = 'No'

if delta_impact=="Yes" or  delta_impact=="No":
  print(f"{delta_impact} is the value for the delta_impact variable.")
else:
  print("Please enter either 'Yes' or 'No' for this variable.")
No is the value for the delta_impact variable.

Summary and Recommendations

The following code will print a summary, based on the variable information recorded in earlier code blocks.

Is data MCAR or MAR/MNAR?

Print of variable values set also.

print(f"Missing data level is: {missingness}.\n")
Missing data level is: 0.022889842632331903.
print(f"Data is more likely to be missing in some variables than others: {likelihood}.\n")
Data is more likely to be missing in some variables than others: Yes.
print(f"Little's MCAR Test p value result is: {mcar_presult}. \n")
Little's MCAR Test p value result is: 0.2133409. 
print(f"Data is missing from the dependent variable only: {dependent_only}. \n")
Data is missing from the dependent variable only: No. 
if likelihood=="No" and mcar_presult=="error":
  hypothesis="Missing data pattern provides some support for MCAR hypothesis. However, no result available for MCAR Test."
elif likelihood=="No" and mcar_presult<0.05:
  hypothesis="Hypothesis unproven. MCAR Test and missing data pattern indicate different missing data mechanisms."
elif likelihood=="No" and mcar_presult>=0.05:
  hypothesis="MCAR hypothesis supported by MCAR Test and missing data pattern."
elif likelihood=="Yes" and mcar_presult=="error":
  hypothesis="Missing data pattern provides some support for MAR/MNAR hypothesis. However, no result available for MCAR Test."
elif likelihood=="Yes" and mcar_presult<0.05:
  hypothesis="MAR/MNAR hypothesis supported by MCAR Test and missing data pattern."
elif likelihood=="Yes" and mcar_presult>=0.05:
  hypothesis="Hypothesis unproven. MCAR Test and missing data pattern indicate different missing data mechanisms."
else:
  hypothesis="No hypothesis found."
  
print(hypothesis)
Hypothesis unproven. MCAR Test and missing data pattern indicate different missing data mechanisms.

Consideration of Test Model Results in Data Experiment

if better_model=="MI" and approach=="Due to overall level of missing data (or level of missing data from dependent variable only) there is a risk that a poor model will be returned, regardless of method used, that would not generalise well on new data.":
  print("For this particular data experiment, Multiple Imputation produced a more effective model than complete case analysis. However, there is a risk that it would not generalise well on new data.")
  
elif better_model=="CCA" and approach=="Due to overall level of missing data (or level of missing data from dependent variable only) there is a risk that a poor model will be returned, regardless of method used, that would not generalise well on new data.":
  print("For this particular data experiment, complete case analysis produced the most effective model. However, there is a risk that it would not generalise well on new data.")
  
elif better_model=="MI" and approach=="As MCAR hypothesis is supported and missing data is less than 20%, complete case analysis is likely to produce unbiased results. However, multiple imputation may produce a more effective model in some circumstances.":
  print("For this particular data experiment, Multiple Imputation produced a more effective model than complete case analysis. However, complete case analysis would be likely to produce unbiased results also.")
  
elif better_model=="CCA" and approach=="As MCAR hypothesis is supported and missing data is less than 20%, complete case analysis is likely to produce unbiased results. However, multiple imputation may produce a more effective model in some circumstances.":
  print("For this particular data experiment, complete case analysis produced the most effective model. However, multiple imputation may produce a more effective model in some circumstances.")
  
elif better_model=="MI" and approach=="MCAR hypothesis is supported. However, as missing data is between 20-50%, multiple imputation is likely to produce a more effective model.":
   print("For this particular data experiment, Multiple Imputation produced a more effective model than complete case analysis. This result is expected, given variables provided.")
   
elif better_model=="CCA" and approach=="MCAR hypothesis is supported. However, as missing data is between 20-50%, multiple imputation is likely to produce a more effective model.":
   print("For this particular data experiment, complete case analysis produced the most effective model. However, caution should be noted over results due to high level of missingness.")
   
elif better_model=="MI" and approach=="MAR/MNAR hypothesis supported, or MCAR hypothesis unproven. However, as missing data is less than 20% and missing from the dependent variable only, complete case analysis may be the more reliable approach.":
   print("For this particular data experiment, Multiple Imputation produced a more effective model than complete case analysis. However, caution should be taken with how missing data in dependent variables is handled, as CCA may be more reliable.")
   
elif better_model=="CCA" and approach=="MAR/MNAR hypothesis supported, or MCAR hypothesis unproven. However, as missing data is less than 20% and missing from the dependent variable only, complete case analysis may be the more reliable approach.":
   print("For this particular data experiment, complete case analysis produced the most effective model. This should be a reliable approach for this missing data problem.")
   
elif better_model=="MI" and approach=="MAR/MNAR hypothesis supported, or MCAR hypothesis unproven. Also, as missing data is less than 50% and the delta sensitivity analysis suggests the results are relatively stable, multiple imputation is the recommended approach.":
   print("For this particular data experiment, Multiple Imputation produced a more effective model than complete case analysis. This should be a reliable approach for this missing data problem.")
   
elif better_model=="CCA" and approach=="MAR/MNAR hypothesis supported, or MCAR hypothesis unproven. Also, as missing data is less than 50% and the delta sensitivity analysis suggests the results are relatively stable, multiple imputation is the recommended approach.":
   print("For this particular data experiment, complete case analysis produced the most effective model. However, the MCAR hypothesis is either not supported or unclear so caution is advised on the results produced.")
   
elif better_model=="MI" and approach=="MAR/MNAR hypothesis supported, or MCAR hypothesis unproven. The delta sensitivity analysis suggests the results are not stable and indicative of MNAR data. Therefore, a pattern mixture model is recommended for further consideration. There are a couple of R mice package functions worth exploring further for this: mice.impute.ri and mice.impute.mnar.logreg (van Buuren et al., 2023).":
   print("For this particular data experiment, Multiple Imputation produced a more effective model than complete case analysis. However, it is recommended that a pattern mixture model is considered also.")
   
elif better_model=="CCA" and approach=="MAR/MNAR hypothesis supported, or MCAR hypothesis unproven. The delta sensitivity analysis suggests the results are not stable and indicative of MNAR data. Therefore, a pattern mixture model is recommended for further consideration. There are a couple of R mice package functions worth exploring further for this: mice.impute.ri and mice.impute.mnar.logreg (van Buuren et al., 2023).":
   print("For this particular data experiment, complete case analysis produced the most effective model. However, it is recommended that a pattern mixture model is considered also.")
   
else:
   print("No model evaluation found.")
   
For this particular data experiment, Multiple Imputation produced a more effective model than complete case analysis. This should be a reliable approach for this missing data problem.

–Last updated: August 2023 –

–End–

LS0tDQp0aXRsZTogJ1ZhYXIgUHl0aG9uIE5vdGVib29rOiBCcmVhc3QgQ2FuY2VyIFdpc2NvbnNpbiBPcmlnaW5hbCcNCmF1dGhvcjogIkFtYW5kYSBIYXJyaXMiDQpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sNCnBhcmFtczoNCiAgcHJpbnRjb2RlOiB0cnVlDQotLS0NCg0KVGhpcyBWYWFyIFtSIE1hcmtkb3duXShodHRwOi8vcm1hcmtkb3duLnJzdHVkaW8uY29tKSBOb3RlYm9vayB1c2luZyBQeXRob24gYWltcyB0byByZXR1cm46ICANCi0gIFdoZXRoZXIgZGF0YSBpcyBtb3JlIGxpa2VseSB0byBiZSBNQ0FSIG9yIE1BUi9NTkFSICANCi0gIEEgcmVjb21tZW5kZWQgYXBwcm9hY2ggZm9yIG1hbmFnaW5nIG1pc3NpbmcgZGF0YSB0aGF0IGNvbnNpZGVycyBzdGFiaWxpdHkgb2YgbW9kZWwgcmVzdWx0cyAgDQotICBFdmFsdWF0aW9uIG9mIGltcHV0YXRpb24gZWZmb3J0cyBiYXNlZCBvbiBkYXRhIG1vZGVsIGV4cGVyaW1lbnQgcmVzdWx0cyAgDQoNClRoaXMgcGFydGljdWxhciBWYWFyIG5vdGVib29rIHVzZXMgVUNJJ3MgQnJlYXN0IENhbmNlciBXaXNjb25zaW4gT3JpZ2luYWwgRGF0YSBhcyBhbiBleGVtcGxhci4NCg0KIyBTdGVwIDE6IFJlYWQgaW4gZGF0YSBhbmQgc2hvdyBkYXRhIHN1bW1hcnkgIA0KDQpgYGB7cn0NCmxpYnJhcnkocmV0aWN1bGF0ZSkNCnB5X2F2YWlsYWJsZShUUlVFKQ0KcHlfaW5zdGFsbCgicGFuZGFzIikNCmBgYA0KDQoNCmBgYHtweXRob259DQppbXBvcnQgcGFuZGFzIGFzIHBkDQoNCmRmID0gcGQucmVhZF9jc3YoJ2h0dHBzOi8vYXJjaGl2ZS5pY3MudWNpLmVkdS9tbC9tYWNoaW5lLWxlYXJuaW5nLWRhdGFiYXNlcy9icmVhc3QtY2FuY2VyLXdpc2NvbnNpbi9icmVhc3QtY2FuY2VyLXdpc2NvbnNpbi5kYXRhJywgc2VwPScsJywgaGVhZGVyPU5vbmUsIG5hX3ZhbHVlcz0nPycpDQpwcmludChkZikNCg0KYGBgDQpgYGB7cn0NCnB5X2luc3RhbGwoIm51bXB5IikNCmBgYA0KUHJpbnQgc3VtbWFyeSBzdGF0aXN0aWNzIGZvciBkYXRhZnJhbWUuDQpgYGB7cHl0aG9ufQ0KaW1wb3J0IG51bXB5IGFzIG5wDQpwcmludChkZi5kZXNjcmliZSgpKQ0KYGBgDQpDb3VudCBudW1iZXIgb2Ygb2JzZXJ2YXRpb25zIGluIGVhY2ggY2xhc3MgaWYgY2xhc3NpZmljYXRpb24gcHJvYmxlbS4NCmBgYHtweXRob259DQpkaWFnbm9zaXMgPSBkZi5ncm91cGJ5KDEwKQ0KZGlhZ25vc2lzLmNvdW50KCkNCmBgYA0KDQoNCiMgU3RlcCAyOiBUaWR5IERhdGENCmBgYHtweXRob259DQojcmVuYW1lIHZhcmlhYmxlcyB0byBzb21ldGhpbmcgbW9yZSBtZWFuaW5nZnVsIGluIGNvcHkgb2Ygb3JpZ2luYWwgZGYNCmRmY3AgPSBkZg0KY29scyA9IGRmLmNvbHVtbnMNCmRmLnJlbmFtZShjb2x1bW5zID0ge2NvbHNbMF06J3NhbXBsZUlEJyxjb2xzWzFdOidDbHVtcFRoaWNrbmVzcycsY29sc1syXTonQ2VsbFNpemUnLCBjb2xzWzNdOidDZWxsU2hhcGUnLGNvbHNbNF06J0FkaGVzaW9uJyxjb2xzWzVdOidTaW5nbGVFU2l6ZScsY29sc1s2XTonQmFyZU51Y2xlaScsIGNvbHNbN106J0JsYW5kQ2hyb21hdGluJywgY29sc1s4XTonTm9ybWFsTnVjbGVvbGknLCBjb2xzWzldOidNaXRvc2VzJywgY29sc1sxMF06J0RpYWdub3Npcyd9LCBpbnBsYWNlPVRydWUpDQoNCmBgYA0KDQpHZXQgdGhlIHR5cGUgZm9yIHZhcmlhYmxlcyBpbiB0aGUgZGF0YWZyYW1lLg0KYGBge3B5dGhvbn0NCmRmY3AuZHR5cGVzDQpgYGANCkNoZWNrIGZvciBhbmQgcmVtb3ZlIGFueSBkdXBsaWNhdGUgb2JzZXJ2YXRpb25zIGlmIHRoZXJlIGlzIGEgdW5pcXVlIGlkZW50aWZpZXIuIENvbW1lbnQgb3V0IGlmIG5vIGlkZW50aWZpZXIuDQpgYGB7cHl0aG9ufQ0KZHVwbGljYXRlcyA9IGRmY3AuZHVwbGljYXRlZChrZWVwPSdmaXJzdCcpDQpwcmludCgnRHVwbGljYXRlczonKQ0KcHJpbnQoZHVwbGljYXRlcykNCnByaW50KCdcbicpDQpwcmludCgnRGF0YUZyYW1lIGFmdGVyIGtlZXBpbmcgb25seSB0aGUgZmlyc3QgaW5zdGFuY2Ugb2YgdGhlIGR1cGxpY2F0ZSByb3dzOicpDQpkZmNwW35kdXBsaWNhdGVzXQ0KYGBgDQpSZW1vdmUgdW5pcXVlIGlkZW50aWZpZXIgZnJvbSBkYXRhZnJhbWUgYXMgdGhpcyB3aWxsIG5vdCBhZGQgYW55IHZhbHVlIHRvIG1vZGVsLg0KYGBge3B5dGhvbn0NCmRmY3AgPSBkZmNwLmRyb3AoY29sdW1ucz1bJ3NhbXBsZUlEJ10pDQpgYGANCiMgU3RlcCAzOiBWaXN1YWxpc2UgTWlzc2luZyBEYXRhIGFuZCBSdW4gTUNBUiBUZXN0DQpDb3VudCBtaXNzaW5nIHZhbHVlcyBpbiBlYWNoIGNvbHVtbi4gDQpgYGB7cHl0aG9ufQ0KZGZjcC5pc25hKCkuc3VtKCkNCmxlbihkZmNwKQ0KDQpgYGANCmBgYHtweXRob259DQptaXNzaW5nbmVzcyA9IDE2LzY5OQ0KYGBgDQoNClVzZSBzZWFib3JuIG1vZHVsZSB0byB2aXN1YWxpc2UgbWlzc2luZyBkYXRhLg0KDQpgYGB7cn0NCnB5X2luc3RhbGwoInNlYWJvcm4iKQ0KYGBgDQpgYGB7cHl0aG9ufQ0KaW1wb3J0IHNlYWJvcm4gYXMgc25zDQpgYGANCkluc3RhbGwgbWF0cGxvdGxpYiBtb2R1bGUgYWxzby4NCmBgYHtyfQ0KcHlfaW5zdGFsbCgibWF0cGxvdGxpYj09My42IikNCmBgYA0KSW1wb3J0IG1hdHBsb3RsaWIuDQpgYGB7cHl0aG9ufQ0KaW1wb3J0IG1hdHBsb3RsaWIucHlwbG90IGFzIHBsdA0KYGBgDQpWaXN1YWxpc2UgbWlzc2luZyBkYXRhIHBhdHRlcm5zIGluIG1hdHJpeC4NCg0KYGBge3B5dGhvbn0NCnBsdC5maWd1cmUoZmlnc2l6ZT0oMTAsNikpDQpzbnMuaGVhdG1hcChkZmNwLmlzbmEoKS50cmFuc3Bvc2UoKSwgY21hcD0iQmx1ZXMiLCBjYmFyX2t3cz17J2xhYmVsJzogJ01pc3NpbmcgRGF0YSd9KQ0KcGx0LnNob3coKQ0KYGBgDQoNClJ1biBNQ0FSIFRlc3QuDQpgYGB7cn0NCnB5X2luc3RhbGwoInNjaXB5IikNCmBgYA0KYGBge3J9DQpweV9pbnN0YWxsKCJyLW5hbmlhciIpDQpgYGANClRoZSBmb2xsb3dpbmcgY29kZSwgaW5jbHVkaW5nIExpdHRsZSdzIE1DQVIgVGVzdCwgaGFzIGJlZW4gcGVyZm9ybWVkIGluIFIsIGFzIG5vIGVxdWl2YWxlbnQgZXhpc3RzIGluIFB5dGhvbiB5ZXQgKEF1Z3VzdCAyMDIzKS4NCioqVGhlcmUgd2lsbCBiZSBhIHdhcm5pbmcgd2l0aCBNQ0FSIHRlc3QgaWYgbm9uLW51bWVyaWMgY29sdW1ucyBhcmUgcHJlc2VudC4gSWYgcC12YWx1ZSA+MC4wNSBsaWtlbHkgdG8gYmUgTUNBUiBhcyBub3Qgc3RhdGlzdGljYWxseSBzaWduaWZpY2FudC4qKiAgDQpUaGUgTUNBUiBUZXN0IG1heSBlcnJvciBpZiBkYXRhIGlzIHNpbmd1bGFyLCB0aGVyZSBpcyBubyBmaXggYXZhaWxhYmxlIGZvciB0aGlzIHlldCAoQXVndXN0IDIwMjMpLg0KYGBge1J9DQpsaWJyYXJ5KG5hbmlhcikNCmxpYnJhcnkocmV0aWN1bGF0ZSkNCm1jYXJfdGVzdChweSRkZmNwKQ0KYGBgDQoNCiMgU3RlcCA0OiBMb29rIGF0IE91dHB1dHMgZnJvbSBWaXN1YWxpc2luZyB0aGUgTWlzc2luZyBEYXRhIGFuZCBBbnN3ZXIgdGhlIEZvbGxvd2luZyBRdWVzdGlvbnMNCg0KIyMjIyBXaGF0IGlzIHRoZSBwLnZhbHVlIHJldHVybmVkIGJ5IHRoZSBNQ0FSIFRlc3Q/DQpJZiB0aGUgc3RhdGlzdGljIGlzIGhpZ2ggYW5kIHRoZSBwLnZhbHVlIDwwLjA1IGl0IGlzIGxpa2VseSB0byBiZSBNTkFSIG9yIE1BUiBhbmQgY2Fubm90IGJlIGlnbm9yZWQuIEEgcC12YWx1ZSA+MC4wNSBpbmRpY2F0ZXMgTUNBUiBidXQgb3RoZXIgaW5mb3JtYXRpb24gd2lsbCBiZSBjb25zaWRlcmVkIGFsc28uIFRoZSB2YWx1ZSBpcyBzZXQgYXMgYSB2YXJpYWJsZSBpbiB0aGUgZm9sbG93aW5nIGNvZGUuDQoNCmBgYHtweXRob259DQojc2V0IE1DQVIgVGVzdCBwLnZhbHVlIGFzIHZhcmlhYmxlDQptY2FyX3ByZXN1bHQgPSAwLjIxMzM0MDkNCg0KaWYgbWNhcl9wcmVzdWx0PT0iZXJyb3IiIG9yICBpc2luc3RhbmNlKG1jYXJfcHJlc3VsdCwgKGludCwgZmxvYXQsIGNvbXBsZXgpKToNCiAgcHJpbnQoZiJ7bWNhcl9wcmVzdWx0fSBpcyBNQ0FSIFRlc3QgcC52YWx1ZSB2YXJpYWJsZSB2YWx1ZS4iKQ0KZWxzZToNCiAgcHJpbnQoIlBsZWFzZSBlbnRlciBlaXRoZXIgYSBudW1iZXIgb3IgJ2Vycm9yJyBhcyB0aGUgbWNhcl9wcmVzdWx0IHZhcmlhYmxlIHZhbHVlLiIpDQogICAgDQpgYGANCg0KIyMjIyBJcyBkYXRhIG1vcmUgbGlrZWx5IHRvIGJlIG1pc3NpbmcgaW4gc29tZSB2YXJpYWJsZXM/DQpJZiBzbywgdGhpcyBpbmRpY2F0ZXMgdGhhdCB0aGUgZGF0YSBjb3VsZCBiZSBtaXNzaW5nIGF0IHJhbmRvbSAoTUFSKSBvciBtaXNzaW5nIG5vdCBhdCByYW5kb20gKE1OQVIpIGFuZCBjYW4gdGhlcmVmb3JlIG5vdCBiZSBpZ25vcmVkLiBZZXMgb3IgTm8gaXMgc2V0IGFzIGEgdmFyaWFibGUgaW4gdGhlIGZvbGxvd2luZyBjb2RlLg0KYGBge3B5dGhvbn0NCiNzZXQgbGlrZWxpaG9vZCB2YXJpYWJsZQ0KbGlrZWxpaG9vZCA9ICJZZXMiDQoNCmlmIGxpa2VsaWhvb2Q9PSJZZXMiIG9yICBsaWtlbGlob29kPT0iTm8iOg0KICBwcmludChmIntsaWtlbGlob29kfSBpcyBsaWtlbGlob29kIHZhcmlhYmxlIHZhbHVlLiIpDQplbHNlOg0KICBwcmludCgiUGxlYXNlIGVudGVyIGVpdGhlciAnWWVzJyBvciAnTm8nIGFzIHRoZSBsaWtlbGlob29kIHZhcmlhYmxlIHZhbHVlLiIpDQpgYGANCg0KIyMjIyBJcyBkYXRhIG1pc3NpbmcgZnJvbSB0aGUgZGVwZW5kZW50IHZhcmlhYmxlIG9ubHk/DQpJZiBzbywgdGhpcyBzdWdnZXN0cyB0aGF0IGNvbXBsZXRlIGNhc2UgYW5hbHlzaXMgbWF5IGJlIHRoZSBiZXN0IG9wdGlvbi4gSG93ZXZlciwgb3RoZXIgZmFjdG9ycyBuZWVkIHRvIGJlIGNvbnNpZGVyZWQgYWxzby4gWWVzIG9yIE5vIGlzIHNldCBhcyB2YXJpYWJsZSBpbiB0aGUgZm9sbG93aW5nIGNvZGUuDQpgYGB7cHl0aG9ufQ0KI3NldCBkZXBlbmRlbnQgbWlzc2luZ25lc3MgdmFyaWFibGUNCmRlcGVuZGVudF9vbmx5ID0gIk5vIg0KDQppZiBkZXBlbmRlbnRfb25seT09IlllcyIgb3IgIGRlcGVuZGVudF9vbmx5PT0iTm8iOg0KICBwcmludChmIntkZXBlbmRlbnRfb25seX0gaXMgZGVwZW5kZW50X29ubHkgdmFyaWFibGUgdmFsdWUuIikNCmVsc2U6DQogIHByaW50KCJQbGVhc2UgZW50ZXIgZWl0aGVyICdZZXMnIG9yICdObycgYXMgdGhlIGRlcGVuZGVudF9vbmx5IHZhcmlhYmxlIHZhbHVlLiIpDQogIA0KYGBgDQoNCiMjIyMgV2hhdCBwZXJjZW50YWdlIG9mIGRhdGEgaXMgbWlzc2luZz8NCklmIGl0J3MgYmV0d2VlbiAyMC01MCUgbXVsdGlwbGUgaW1wdXRhdGlvbiBpcyBsaWtlbHkgdG8gcmVzdWx0IGluIGEgbW9yZSBlZmZlY3RpdmUsIHVuYmlhc2VkIG1vZGVsLiBUaGUgdmFsdWUgaXMgc2V0IGFzIGEgdmFyaWFibGUgaW4gdGhlIGZvbGxvd2luZyBjb2RlLg0KYGBge3B5dGhvbn0NCiNwcmludCBtaXNzaW5nbmVzcyB2YXJpYWJsZSB2YWx1ZQ0KcHJpbnQobWlzc2luZ25lc3MpDQpgYGANCg0KSXQncyBnb29kIHByYWN0aWNlIHRvIHRlc3QgZGlmZmVyZW50IGFwcHJvYWNoZXMgYW5kIHRoZSBWYWFyIE5vdGVib29rcyB3aWxsIGNvbnNpZGVyIHRoZXNlLCBhcyB3ZWxsIGFzIHRoZSB2YXJpYWJsZSB2YWx1ZXMganVzdCBzZXQgaW4gcmVjb21tZW5kYXRpb25zLg0KDQoNCiMjIFZpc3VhbGlzZSBDb3JyZWxhdGlvbg0KVmlzdWFsaXNlIGNvcnJlbGF0aW9uIGZvciBudW1lcmljYWwgdmFyaWFibGVzLg0KYGBge3B5dGhvbn0NCm49MTAgI251bWJlciBvZiBjb2x1bW5zDQpuYW1lcyA9IFsnQ2x1bXBUaGlja25lc3MnLCdDZWxsU2l6ZScsJ0NlbGxTaGFwZScsJ0FkaGVzaW9uJywnU2luZ2xlRVNpemUnLCdCYXJlTnVjbGVpJywnQmxhbmRDaHJvbWF0aW4nLCdOb3JtYWxOdWNsZW9saScsJ01pdG9zZXMnLCdEaWFnbm9zaXMnXSAjY29sIG5hbWVzDQpmaWcgPSBwbHQubWF0c2hvdyhkZmNwLmNvcnIoKSwpDQpheCA9IHBsdC5nY2EoKQ0KDQpheC5zZXRfeHRpY2tzKG5wLmFyYW5nZShuKSkNCmF4LnNldF94dGlja2xhYmVscyhuYW1lcykNCmF4LnNldF95dGlja3MobnAuYXJhbmdlKG4pKQ0KYXguc2V0X3l0aWNrbGFiZWxzKG5hbWVzKQ0KDQpwbHQuc2V0cChbdGljay5sYWJlbDIgZm9yIHRpY2sgaW4gYXgueGF4aXMuZ2V0X21ham9yX3RpY2tzKCldLCByb3RhdGlvbj00MCwNCiAgICAgICAgIGhhPSJsZWZ0IiwgdmE9ImNlbnRlciIscm90YXRpb25fbW9kZT0iYW5jaG9yIikNCg0KcGx0LnNob3coKQ0KDQpgYGANCg0KVXNlIFNlYWJvcm4ncyBQYWlyR3JpZCB0byB2aXN1YWxpc2UgaW50ZXJhY3Rpb25zIGJldHdlZW4gdmFyaWFibGVzLg0KDQpgYGB7cHl0aG9ufQ0KI3VzZSBzZWFib3JuJ3MgUGFpckdyaWQgdG8gdmlzdWFsaXNlIGludGVyYWN0aW9ucyBiZXR3ZWVuIHZhcmlhYmxlcy4NCnBhaXJzID0gc25zLlBhaXJHcmlkKGRmY3AsIGh1ZT0iRGlhZ25vc2lzIikNCnBhaXJzLm1hcF9kaWFnKHNucy5oaXN0cGxvdCkNCnBhaXJzLm1hcF9vZmZkaWFnKHNucy5zY2F0dGVycGxvdCkNCnBhaXJzLmFkZF9sZWdlbmQoKQ0KcGx0LnNob3coKQ0KYGBgDQoNCg0KIyMgQ2FsY3VsYXRlIFNrZXcNCkluc3RhbGwgYW5kIGltcG9ydCBzY2lweS4NCmBgYHtyfQ0KcHlfaW5zdGFsbCgic2NpcHkiKQ0KYGBgDQpgYGB7cHl0aG9ufQ0KaW1wb3J0IHNjaXB5IGFzIHNjaQ0KZnJvbSBzY2lweS5zdGF0cyBpbXBvcnQgc2tldw0KYGBgDQoNCkNhbGN1bGF0ZSBhbmQgcHJpbnQgc2tld25lc3MuIEZvciB2YXJpYWJsZXMgd2l0aCBtaXNzaW5nIGRhdGEgJ25hbicgd2lsbCBiZSByZXR1cm5lZC4gDQoNCg0KYGBge3B5dGhvbn0NCnByaW50KHNrZXcoZGZjcCwgYXhpcyA9IDAsIGJpYXM9VHJ1ZSkpDQpgYGANCg0KIyBTdGVwIDU6IHdoaWNoIHZhcmlhYmxlcyBhbmQgdmFsdWVzIGFyZSBpbXBvcnRhbnQgZm9yIHByZWRpY3RpbmcgcHJvcG9ydGlvbiBvZiBtaXNzaW5nbmVzcz8NCk5hbmlhciAoUikgdXNlZCBmb3IgcHJvcF9taXNzIGZ1bmN0aW9ucywgdG8gYWRkIHRvIGRhdGFmcmFtZSB0ZW1wb3JhcmlseSBmb3IgdXNlIGluIHNpbXBsZSBkZWNpc2lvbiB0cmVlLg0KYGBge3J9DQpsaWJyYXJ5KHJwYXJ0KQ0KbGlicmFyeShycGFydC5wbG90KQ0KYGBgDQoNCg0KYGBge3J9DQpweSRkZmNwICU+JQ0KICBhZGRfcHJvcF9taXNzKCkgJT4lDQogIHJwYXJ0KHByb3BfbWlzc19hbGwgfiAuLCBkYXRhPS4pICU+JQ0KICBwcnAodHlwZT00LCBleHRyYSA9IDEwMSwgcm91bmRpbnQ9RkFMU0UsIHByZWZpeD0iUHJvcC5NaXNzID0gIikNCmBgYA0KDQojIyMjIFdoaWNoIG51bWVyaWNhbCB2YXJpYWJsZSB3aXRoIG1pc3NpbmcgZGF0YSBpcyBuZWFyZXN0IHRoZSB0b3Agb2YgdGhlIHRyZWU/DQpTZXQgdmFyaWFibGUgdmFsdWUgZm9yIG1pc3NpbmduZXNzIGluZmx1ZW5jZXIgaW4gY29kZSBiZWxvdy4NCg0KYGBge3B5dGhvbn0NCiNzZXQgdmFsdWUgZm9yIG1pc3NpbmduZXNzIGluZmx1ZW5jZXINCm1pc3NfaW5mbHVlbmNlcj0iQmFyZU51Y2xlaSINCmBgYA0KDQojIFN0ZXAgNjogQ3JlYXRlIENvbXBsZXRlIENhc2UgYW5kIEl0ZXJhdGl2ZSBJbXB1dGF0aW9uIERhdGFzZXRzDQpDcmVhdGUgY29tcGxldGUgZGF0YXNldCBieSBkZWxldGluZyByZWNvcmRzIHdpdGggbWlzc2luZyB2YWx1ZXMgYW5kIHJ1biBpdGVyYXRpdmUgY2hhaW5lZCBlcXVhdGlvbnMgd2hpY2ggcmV0dXJuIHNpbmdsZSBpbXB1dGF0aW9uIGluc3RlYWQgb2YgbXVsdGlwbGUuDQpgYGB7cHl0aG9ufQ0KI0NyZWF0ZSBDQ0EgZGF0YQ0KZGZjcF9jY2EgPSBkZmNwLmNvcHkoKQ0KZGZjcF9jY2EgPSBkZmNwX2NjYS5kcm9wbmEoKQ0KYGBgDQoNCmBgYHtweXRob259DQpmcm9tIHNrbGVhcm4uZXhwZXJpbWVudGFsIGltcG9ydCBlbmFibGVfaXRlcmF0aXZlX2ltcHV0ZXINCmZyb20gc2tsZWFybi5pbXB1dGUgaW1wb3J0IEl0ZXJhdGl2ZUltcHV0ZXINCmRmY3BfbWkgPSBJdGVyYXRpdmVJbXB1dGVyKHJhbmRvbV9zdGF0ZT0wLCBtYXhfaXRlcj01KS5maXRfdHJhbnNmb3JtKGRmY3ApDQoNCmBgYA0KQ2hlY2sgdGhhdCB0aGVyZSBpcyBubyBtaXNzaW5nIGRhdGEgaW4gbmV3IGRhdGFzZXRzLg0KYGBge3B5dGhvbn0NCiNjb252ZXJ0IGFycmF5IHRvIGRhdGFmcmFtZQ0KZGZjcF9taSA9IHBkLkRhdGFGcmFtZShkZmNwX21pKQ0KcHJpbnQoZGZjcF9jY2EuaXNuYSgpLnN1bSgpKQ0KcHJpbnQoZGZjcF9taS5pc25hKCkuc3VtKCkpDQpgYGANCkFkZCBjb2x1bW4gbmFtZXMgdG8gaW1wdXRlZCBkYXRhZnJhbWUuDQoNCmBgYHtweXRob259DQpkZmNwX2ltcCA9IGRmY3BfbWkNCmNvbHMgPSBkZmNwX2ltcC5jb2x1bW5zDQpkZmNwX2ltcC5yZW5hbWUoY29sdW1ucyA9IHtjb2xzWzBdOidDbHVtcFRoaWNrbmVzcycsY29sc1sxXTonQ2VsbFNpemUnLCBjb2xzWzJdOidDZWxsU2hhcGUnLGNvbHNbM106J0FkaGVzaW9uJyxjb2xzWzRdOidTaW5nbGVFU2l6ZScsY29sc1s1XTonQmFyZU51Y2xlaScsIGNvbHNbNl06J0JsYW5kQ2hyb21hdGluJywgY29sc1s3XTonTm9ybWFsTnVjbGVvbGknLCBjb2xzWzhdOidNaXRvc2VzJywgY29sc1s5XTonRGlhZ25vc2lzJ30sIGlucGxhY2U9VHJ1ZSkNCmBgYA0KDQojIFN0ZXAgNzogQ29uZHVjdCBNTkFSIFNlbnNpdGl2aXR5IFRlc3QNCg0KVXNlIHRoZSB2YXJpYWJsZSB3aXRoIG1pc3NpbmcgZGF0YSB3aGljaCBoYXMgdGhlIG1vc3QgaW5mbHVlbmNlIG9uIHByb3BvcnRpb24gb2YgbWlzc2luZ25lc3MsIGFjY29yZGluZyB0byBzdGVwIDUgYW5kIGxvb2sgYXQgdGhlIHJhbmdlIG9mIHZhbHVlcyBpbiB0aGlzIHZhcmlhYmxlLiBHZW5lcmF0ZSBpbXB1dGF0aW9ucyB1bmRlciBkZWx0YSBhZGp1c3RtZW50IHRvIGltaXRhdGUgZGV2aWF0aW9ucyBmcm9tIE1BUiAoR2luayBhbmQgVmFuIEJ1dXJlbiwgbm8gZGF0ZSkuIFRoZSBjb2RlIHVzZXMgMCBhbmQgYSByZWFzb25hYmxlIHJhbmdlIGJhc2VkIG9uIHRoZSB2YXJpYWJsZSByYW5nZSBhcyB0aGUgZGVsdGEgdmFsdWVzLg0KYGBge3B5dGhvbn0NCiNQdWxsIG91dCBpbmRleCBvZiBOYU4gZnJvbSBvcmlnaW5hbCBkYXRhDQpkZltkZlsnQmFyZU51Y2xlaSddLmlzbmEoKV0NCmBgYA0KYGBge3B5dGhvbn0NCiNzdWJzZXQgdGhlc2Ugcm93cyBhbmQgdmFyaWFibGUgaWRlbnRpZmllZCBmcm9tIGltcHV0ZWQgZGF0YSBzZXQNCmRlbHRhMCA9IGRmY3BfbWkuaWxvY1tbMjMsNDAsMTM5LDE0NSwxNTgsMTY0LDIzNSwyNDksMjc1LDI5MiwyOTQsMjk3LDMxNSwzMjEsNDExLDYxN10sWzVdXSANCmBgYA0KDQoNCg0KYGBge3B5dGhvbn0NCiNzZXQgZGVsdGEgdmFsdWVzIHRoZW4gc2V0IHVwcGVyIGNsaXANCiNjcmVhdGUgbmV3IHZhbHVlcyBieSBkZWx0YSBpbmNyZWFzZQ0KZGVsdGE9WzAsMSwyLDMsNF0NCmRlbHRhMSA9IGRlbHRhMC5jb3B5KCkNCmRlbHRhMS5sb2NbZGVsdGExWydCYXJlTnVjbGVpJ108MTAsICdCYXJlTnVjbGVpJ10rPTENCg0KYGBgDQoNClJlcGVhdCBmb3Igb3RoZXIgZGVsdGEgdmFsdWVzDQpgYGB7cHl0aG9ufQ0KZGVsdGEyID0gZGVsdGExLmNvcHkoKQ0KZGVsdGEyLmxvY1tkZWx0YTJbJ0JhcmVOdWNsZWknXTwxMCwgJ0JhcmVOdWNsZWknXSs9MQ0KZGVsdGEyLmNsaXAodXBwZXI9MTApDQoNCmRlbHRhMyA9IGRlbHRhMi5jb3B5KCkNCmRlbHRhMy5sb2NbZGVsdGEzWydCYXJlTnVjbGVpJ108MTAsICdCYXJlTnVjbGVpJ10rPTENCmRlbHRhMyA9IGRlbHRhMy5jbGlwKHVwcGVyPTEwKQ0KDQpkZWx0YTQgPSBkZWx0YTMuY29weSgpDQpkZWx0YTQubG9jW2RlbHRhNFsnQmFyZU51Y2xlaSddPDEwLCAnQmFyZU51Y2xlaSddKz0xDQpkZWx0YTQgPSBkZWx0YTQuY2xpcCh1cHBlcj0xMCkNCmBgYA0KUmVwbGFjZSBOYU4gd2l0aCBpbmNyZWFzZWQgdmFsdWVzIHRvIHRlc3Qgc2Vuc2l0aXZpdHkuIEV4YW1wbGUgYmVsb3cgdG8gY2hhbmdlIGJhc2VkIG9uIG91dHB1dHMgY3JlYXRlZCBhYm92ZS4NCg0KYGBge3B5dGhvbn0NCmRmY3BfbWlfZDEgPSBkZmNwLmNvcHkoKQ0KZGZjcF9taV9kMS5pbG9jW1syM10sWzVdXSA9IDguMTE2MzQ1DQpkZmNwX21pX2QxLmlsb2NbWzQwXSxbNV1dID0gNC42Mjc1OTUNCmRmY3BfbWlfZDEuaWxvY1tbMTM5XSxbNV1dID0gMi4xODY2MTkNCmRmY3BfbWlfZDEuaWxvY1tbMTQ1XSxbNV1dID0gMi41ODgyNzMNCmRmY3BfbWlfZDEuaWxvY1tbMTU4XSxbNV1dID0gMi4yNjIzNjANCmRmY3BfbWlfZDEuaWxvY1tbMTY0XSxbNV1dID0gMi40Nzk3ODMNCmRmY3BfbWlfZDEuaWxvY1tbMjM1XSxbNV1dID0gMi45OTI2NzMNCmRmY3BfbWlfZDEuaWxvY1tbMjQ5XSxbNV1dID0gMi40MjM1MDQNCmRmY3BfbWlfZDEuaWxvY1tbMjc1XSxbNV1dID0gMi42NDQ1NTINCmRmY3BfbWlfZDEuaWxvY1tbMjkyXSxbNV1dID0gNy4zMjUwNTINCmRmY3BfbWlfZDEuaWxvY1tbMjk0XSxbNV1dID0gMi4yMDg4MjcNCmRmY3BfbWlfZDEuaWxvY1tbMjk3XSxbNV1dID0gMi4wNjgxMTMNCmRmY3BfbWlfZDEuaWxvY1tbMzE1XSxbNV1dID0gMy4xNjg0NjgNCmRmY3BfbWlfZDEuaWxvY1tbMzIxXSxbNV1dID0gMi40MjM1MDQNCmRmY3BfbWlfZDEuaWxvY1tbNDExXSxbNV1dID0gMi4xODY2MTkNCmRmY3BfbWlfZDEuaWxvY1tbNjE3XSxbNV1dID0gMi4wMjgyMjENCmBgYA0KIFJlcGVhdCBmb3Igb3RoZXIgZGVsdGEgdmFsdWVzLg0KYGBge3B5dGhvbn0NCmRmY3BfbWlfZDIgPSBkZmNwX21pX2QxLmNvcHkoKQ0KZGZjcF9taV9kMi5pbG9jW1syM10sWzVdXSA9IDkuMTE2MzQ1DQpkZmNwX21pX2QyLmlsb2NbWzQwXSxbNV1dID0gNS42Mjc1OTUNCmRmY3BfbWlfZDIuaWxvY1tbMTM5XSxbNV1dID0gMy4xODY2MTkNCmRmY3BfbWlfZDIuaWxvY1tbMTQ1XSxbNV1dID0gMy41ODgyNzMNCmRmY3BfbWlfZDIuaWxvY1tbMTU4XSxbNV1dID0gMy4yNjIzNjANCmRmY3BfbWlfZDIuaWxvY1tbMTY0XSxbNV1dID0gMy40Nzk3ODMNCmRmY3BfbWlfZDIuaWxvY1tbMjM1XSxbNV1dID0gMy45OTI2NzMNCmRmY3BfbWlfZDIuaWxvY1tbMjQ5XSxbNV1dID0gMy40MjM1MDQNCmRmY3BfbWlfZDIuaWxvY1tbMjc1XSxbNV1dID0gMy42NDQ1NTINCmRmY3BfbWlfZDIuaWxvY1tbMjkyXSxbNV1dID0gOC4zMjUwNTINCmRmY3BfbWlfZDIuaWxvY1tbMjk0XSxbNV1dID0gMy4yMDg4MjcNCmRmY3BfbWlfZDIuaWxvY1tbMjk3XSxbNV1dID0gMy4wNjgxMTMNCmRmY3BfbWlfZDIuaWxvY1tbMzE1XSxbNV1dID0gNC4xNjg0NjgNCmRmY3BfbWlfZDIuaWxvY1tbMzIxXSxbNV1dID0gMy40MjM1MDQNCmRmY3BfbWlfZDIuaWxvY1tbNDExXSxbNV1dID0gMy4xODY2MTkNCmRmY3BfbWlfZDIuaWxvY1tbNjE3XSxbNV1dID0gMy4wMjgyMjENCmBgYA0KIA0KYGBge3B5dGhvbn0NCmRmY3BfbWlfZDMgPSBkZmNwX21pX2QyLmNvcHkoKQ0KZGZjcF9taV9kMy5pbG9jW1syM10sWzVdXSA9IDEwDQpkZmNwX21pX2QzLmlsb2NbWzQwXSxbNV1dID0gNi42Mjc1OTUNCmRmY3BfbWlfZDMuaWxvY1tbMTM5XSxbNV1dID0gNC4xODY2MTkNCmRmY3BfbWlfZDMuaWxvY1tbMTQ1XSxbNV1dID0gNC41ODgyNzMNCmRmY3BfbWlfZDMuaWxvY1tbMTU4XSxbNV1dID0gNC4yNjIzNjANCmRmY3BfbWlfZDMuaWxvY1tbMTY0XSxbNV1dID0gNC40Nzk3ODMNCmRmY3BfbWlfZDMuaWxvY1tbMjM1XSxbNV1dID0gNC45OTI2NzMNCmRmY3BfbWlfZDMuaWxvY1tbMjQ5XSxbNV1dID0gNC40MjM1MDQNCmRmY3BfbWlfZDMuaWxvY1tbMjc1XSxbNV1dID0gNC42NDQ1NTINCmRmY3BfbWlfZDMuaWxvY1tbMjkyXSxbNV1dID0gOS4zMjUwNTINCmRmY3BfbWlfZDMuaWxvY1tbMjk0XSxbNV1dID0gNC4yMDg4MjcNCmRmY3BfbWlfZDMuaWxvY1tbMjk3XSxbNV1dID0gNC4wNjgxMTMNCmRmY3BfbWlfZDMuaWxvY1tbMzE1XSxbNV1dID0gNS4xNjg0NjgNCmRmY3BfbWlfZDMuaWxvY1tbMzIxXSxbNV1dID0gNC40MjM1MDQNCmRmY3BfbWlfZDMuaWxvY1tbNDExXSxbNV1dID0gNC4xODY2MTkNCmRmY3BfbWlfZDMuaWxvY1tbNjE3XSxbNV1dID0gNC4wMjgyMjENCmBgYA0KDQpgYGB7cHl0aG9ufQ0KZGZjcF9taV9kNCA9IGRmY3BfbWlfZDMuY29weSgpDQpkZmNwX21pX2Q0Lmlsb2NbWzIzXSxbNV1dID0gMTANCmRmY3BfbWlfZDQuaWxvY1tbNDBdLFs1XV0gPSA3LjYyNzU5NQ0KZGZjcF9taV9kNC5pbG9jW1sxMzldLFs1XV0gPSA1LjE4NjYxOQ0KZGZjcF9taV9kNC5pbG9jW1sxNDVdLFs1XV0gPSA1LjU4ODI3Mw0KZGZjcF9taV9kNC5pbG9jW1sxNThdLFs1XV0gPSA1LjI2MjM2MA0KZGZjcF9taV9kNC5pbG9jW1sxNjRdLFs1XV0gPSA1LjQ3OTc4Mw0KZGZjcF9taV9kNC5pbG9jW1syMzVdLFs1XV0gPSA1Ljk5MjY3Mw0KZGZjcF9taV9kNC5pbG9jW1syNDldLFs1XV0gPSA1LjQyMzUwNA0KZGZjcF9taV9kNC5pbG9jW1syNzVdLFs1XV0gPSA1LjY0NDU1Mg0KZGZjcF9taV9kNC5pbG9jW1syOTJdLFs1XV0gPSAxMA0KZGZjcF9taV9kNC5pbG9jW1syOTRdLFs1XV0gPSA1LjIwODgyNw0KZGZjcF9taV9kNC5pbG9jW1syOTddLFs1XV0gPSA1LjA2ODExMw0KZGZjcF9taV9kNC5pbG9jW1szMTVdLFs1XV0gPSA2LjE2ODQ2OA0KZGZjcF9taV9kNC5pbG9jW1szMjFdLFs1XV0gPSA1LjQyMzUwNA0KZGZjcF9taV9kNC5pbG9jW1s0MTFdLFs1XV0gPSA1LjE4NjYxOQ0KZGZjcF9taV9kNC5pbG9jW1s2MTddLFs1XV0gPSA1LjAyODIyMQ0KYGBgDQoNCg0KIyMgT3B0aW9uYWwgU3RlcDogUmVtb3ZlIE91dGxpZXJzDQpUaGVyZSBjYW4gYmUgdmFsdWFibGUgaW5mb3JtYXRpb24gaW4gb3V0bGllcnMgYW5kIGl0IGlzIGdvb2QgcHJhY3RpY2UgdG8gcmVtb3ZlIG9idmlvdXMgZXJyb3JzIG9ubHkgLSBzdWNoIGFzIGltcG9zc2libGUgdmFsdWVzIGZvciBwYXJ0aWN1bGFyIHZhcmlhYmxlcy4gT3V0bGllcnMgY291bGQgYmUgYW4gYWNjdXJhdGUgcmVwcmVzZW50YXRpb24gb2YgdGhlIG5hdHVyYWwgdmFyaWFiaWxpdHkgaW4gZGF0YSBhbmQgcmVmbGVjdGl2ZSBvZiBkaWFnbm9zdGljIGNoYWxsZW5nZS4NCg0KDQojIFN0ZXAgODogU2VsZWN0IEZlYXR1cmVzDQpUaGlzIGNvZGUgaXMgYW4gZXhhbXBsZSBvZiBob3cgdG8gZGV0ZXJtaW5lIHdoaWNoIHZhcmlhYmxlcyBoYXZlIGFib3ZlIDAuOSBjb3JyZWxhdGlvbiwgYXMgaGlnaGx5LWNvcnJlbGF0ZWQgZmVhdHVyZXMgZG8gbm90IGdlbmVyYWxseSBpbXByb3ZlIG1vZGVscy4gVGhlbiwgcmVtb3ZlIGFueSB2YXJpYWJsZXMgaWRlbnRpZmllZCBmcm9tIGFsbCBkYXRhc2V0cy4NCmBgYHtweXRob259DQojc2VsZWN0IGhpZ2hseSBjb3JyZWxhdGVkIGZlYXR1cmVzDQojcmVtb3ZlIHRhcmdldCB2YXJpYWJsZXMgZnJvbSBjb25zaWRlcmF0aW9uDQpkZmNwX21pX2NvciA9IGRmY3BfbWkuY29weSgpDQpkZmNwX21pX2NvciA9IGRmY3BfbWlfY29yLmRyb3AoY29sdW1ucz1bJ0RpYWdub3NpcyddKQ0KY29yID0gZGZjcF9taV9jb3IuY29ycigpDQpwcmludChjb3IpDQpgYGANCg0KUmVtb3ZlIGZyb20gZGF0YXNldHMgaWYgb3ZlciAwLjkuDQoNCmBgYHtweXRob259DQpkZmNwX21pID0gZGZjcF9taS5kcm9wKGNvbHVtbnM9WydDZWxsU2l6ZSddKQ0KZGZjcF9taV9kMSA9IGRmY3BfbWlfZDEuZHJvcChjb2x1bW5zPVsnQ2VsbFNpemUnXSkNCmRmY3BfbWlfZDIgPSBkZmNwX21pX2QyLmRyb3AoY29sdW1ucz1bJ0NlbGxTaXplJ10pDQpkZmNwX21pX2QzID0gZGZjcF9taV9kMy5kcm9wKGNvbHVtbnM9WydDZWxsU2l6ZSddKQ0KZGZjcF9taV9kNCA9IGRmY3BfbWlfZDQuZHJvcChjb2x1bW5zPVsnQ2VsbFNpemUnXSkNCmRmY3BfY2NhID0gZGZjcF9jY2EuZHJvcChjb2x1bW5zPVsnQ2VsbFNpemUnXSkNCg0KYGBgDQoNCiMjIE9wdGlvbmFsIHN0ZXA6IFRyYW5zZm9ybSBkYXRhDQpTY2FsaW5nIGRhdGEgYWxsb3dzIG1vZGVscyB0byBjb21wYXJlIHJlbGF0aXZlIHJlbGF0aW9uc2hpcHMgYmV0d2VlbiBkYXRhIHBvaW50cyBtb3JlIGVmZmVjdGl2ZWx5LiBTb21lIGRhdGFzZXRzIHdpbGwgYWxyZWFkeSBiZSBzY2FsZWQgbGlrZSB0aGlzIG9uZS4NCg0KTG9nIHRyYW5zZm9ybWF0aW9uIGNhbiBiZSBoZWxwZnVsIGZvciBsaW5lYXIgbW9kZWxzIGZvciBleGFtcGxlLiANCmBgYHtweXRob259DQojZnJvbSBza2xlYXJuLnByZXByb2Nlc3NpbmcgaW1wb3J0IE1pbk1heFNjYWxlcg0KI3NjYWxlciA9IE1pbk1heFNjYWxlcigpDQojZGZjcF9taSA9IHNjYWxlci5maXRfdHJhbnNmb3JtKGRmY3BfbWkpDQojZGZjcF9jY2EgPSBzY2FsZXIuZml0X3RyYW5zZm9ybShkZmNwX2NjYSkNCiNkZmNwX21pX2Q0ID0gc2NhbGVyLmZpdF90cmFuc2Zvcm0oZGZjcF9taV9kNCkNCmBgYA0KDQojIFN0ZXAgOTogQnVpbGQgYW5kIFRlc3QgTW9kZWxzDQpUaGlzIG5vdGVib29rIHVzZXMgYSBSYW5kb20gRm9yZXN0IG1vZGVsIGFzIGFuIGV4YW1wbGUuIA0KDQojIyBDcmVhdGUgVHJhaW5pbmcgYW5kIFRlc3QgU2V0cw0KVXNpbmcgdHJhaW4tdGVzdCBzcGxpdCBwcm9jZWR1cmUgZnJvbSBTY2lraXQtTGVhcm4sIGNyZWF0aW5nIHggYW5kIHkgdmFsdWVzIGZvciBlYWNoIGRhdGFzZXQgZmlyc3QuDQpgYGB7cHl0aG9ufQ0KI2ZvciBpbXB1dGVkIGRhdGFzZXQNCiNjaGFuZ2UgY2xhc3MgdG8gZmFjdG9yIGFsc28gaWYgcmVxdWlyZWQNCmZyb20gc2tsZWFybi5tb2RlbF9zZWxlY3Rpb24gaW1wb3J0IHRyYWluX3Rlc3Rfc3BsaXQNCnggPSBkZmNwX21pLmRyb3AoY29sdW1ucz1bJ0RpYWdub3NpcyddKQ0KeSA9IGRmY3BfbWlbJ0RpYWdub3NpcyddLmFzdHlwZSgnY2F0ZWdvcnknKQ0KbnAucmFuZG9tLnNlZWQoMykgI2ZvciByZXByb2R1Y2liaWxpdHkNCnhfdHJhaW4sIHhfdGVzdCwgeV90cmFpbiwgeV90ZXN0ID0gdHJhaW5fdGVzdF9zcGxpdCh4LCB5LCB0ZXN0X3NpemU9MC4zKQ0KYGBgDQpSZXBlYXQgZm9yIG90aGVyIGRhdGFzZXRzIHRoYXQgd2lsbCBiZSB1c2VkIHRvIHRyYWluIGFuZCB0ZXN0IG1vZGVscy4NCmBgYHtweXRob259DQojY2NhIA0KeGNjYSA9IGRmY3BfY2NhLmRyb3AoY29sdW1ucz1bJ0RpYWdub3NpcyddKQ0KeWNjYSA9IGRmY3BfY2NhWydEaWFnbm9zaXMnXS5hc3R5cGUoJ2NhdGVnb3J5JykNCm5wLnJhbmRvbS5zZWVkKDMpICNmb3IgcmVwcm9kdWNpYmlsaXR5DQp4Y2NhX3RyYWluLCB4Y2NhX3Rlc3QsIHljY2FfdHJhaW4sIHljY2FfdGVzdCA9IHRyYWluX3Rlc3Rfc3BsaXQoeGNjYSwgeWNjYSwgdGVzdF9zaXplPTAuMykNCg0KI2RlbHRhNA0KeGQ0ID0gZGZjcF9taV9kNC5kcm9wKGNvbHVtbnM9WydEaWFnbm9zaXMnXSkNCnlkNCA9IGRmY3BfbWlfZDRbJ0RpYWdub3NpcyddLmFzdHlwZSgnY2F0ZWdvcnknKQ0KbnAucmFuZG9tLnNlZWQoMykgI2ZvciByZXByb2R1Y2liaWxpdHkNCnhkNF90cmFpbiwgeGQ0X3Rlc3QsIHlkNF90cmFpbiwgeWQ0X3Rlc3QgPSB0cmFpbl90ZXN0X3NwbGl0KHhkNCwgeWQ0LCB0ZXN0X3NpemU9MC4zKQ0KYGBgDQoNCiMjIFByZWRpY3QgQWdhaW5zdCBUZXN0IERhdGENClN0YXJ0IHdpdGggYmFzZWxpbmUgbW9kZWwgdXNpbmcgaXRlcmF0aXZlIGltcHV0YXRpb24uIA0KYGBge3B5dGhvbn0NCmZyb20gc2tsZWFybi5lbnNlbWJsZSBpbXBvcnQgUmFuZG9tRm9yZXN0Q2xhc3NpZmllcg0KZnJvbSBza2xlYXJuLm1ldHJpY3MgaW1wb3J0IGNvbmZ1c2lvbl9tYXRyaXgNCiMgcmFuZG9tIGZvcmVzdCBtb2RlbCBjcmVhdGlvbg0KcmZjID0gUmFuZG9tRm9yZXN0Q2xhc3NpZmllcigpDQpyZmMuZml0KHhfdHJhaW4seV90cmFpbikNCg0KIyBwcmVkaWN0aW9ucyBhZ2FpbnN0IHRlc3QNCm5wLnJhbmRvbS5zZWVkKDMpDQpwcmVkaWN0aW9ucyA9IHJmYy5wcmVkaWN0KHhfdGVzdCkNCg0KI2V2YWx1YXRlIHVzaW5nIGNvbmZ1c2lvbiBtYXRyaXgNCnRuLCBmcCwgZm4sIHRwID0gY29uZnVzaW9uX21hdHJpeCh5X3Rlc3QsIHByZWRpY3Rpb25zKS5yYXZlbCgpDQoNCmBgYA0KYGBge3B5dGhvbn0NCnNlbnNfaW1wID0gdHAvKHRwK2ZuKQ0Kc3BlY19pbXAgPSB0bi8oZnArdG4pDQphY2NfaW1wID0gKHRwK3RuKS8odHArdG4rZnArZm4pDQptaXNzX3ByZWRzID0gZnArZm4NCnByaW50KGYnU2Vuc2l0aXZpdHkgZm9yIGltcHV0ZWQgbW9kZWwgaXMge3NlbnNfaW1wfS4nKQ0KcHJpbnQoZidTcGVjaWZpY2l0eSBmb3IgaW1wdXRlZCBtb2RlbCBpcyB7c3BlY19pbXB9LicpDQpwcmludChmJ0FjY3VyYWN5IGZvciBpbXB1dGVkIG1vZGVsIGlzIHthY2NfaW1wfS4nKQ0KcHJpbnQoZidUaGUgaW1wdXRlZCBtb2RlbCBtaXNzY2xhc3NpZmllZCB7bWlzc19wcmVkc30gb2JzZXJ2YXRpb25zIGluIHRlc3QuJykNCmBgYA0KUmVwZWF0IG1vZGVsIHRlc3RzIHdpdGggQ0NBIGRhdGEuDQpgYGB7cHl0aG9ufQ0KI2RlZmluZSBtb2RlbA0KcmZjMSA9IFJhbmRvbUZvcmVzdENsYXNzaWZpZXIoKQ0KcmZjMS5maXQoeGNjYV90cmFpbix5Y2NhX3RyYWluKQ0KDQojIHByZWRpY3Rpb25zIGFnYWluc3QgdGVzdA0KbnAucmFuZG9tLnNlZWQoMykNCnByZWRpY3Rpb25zX2NjYSA9IHJmYzEucHJlZGljdCh4Y2NhX3Rlc3QpDQoNCiNldmFsdWF0ZSB1c2luZyBjb25mdXNpb24gbWF0cml4DQp0bjEsIGZwMSwgZm4xLCB0cDEgPSBjb25mdXNpb25fbWF0cml4KHljY2FfdGVzdCwgcHJlZGljdGlvbnNfY2NhKS5yYXZlbCgpDQoNCiNPcHRpb25hbCBjYWxjdWxhdGlvbnMgZm9yIGNsYXNzaWZpY2F0aW9uIG1vZGVscw0Kc2Vuc19jY2EgPSB0cDEvKHRwMStmbjEpDQpzcGVjX2NjYSA9IHRuMS8oZnAxK3RuMSkNCmFjY19jY2EgPSAodHAxK3RuMSkvKHRwMSt0bjErZnAxK2ZuMSkNCm1pc3NfY2NhID0gZnAxK2ZuMQ0KcHJpbnQoZidTZW5zaXRpdml0eSBmb3IgQ0NBIG1vZGVsIGlzIHtzZW5zX2NjYX0uJykNCnByaW50KGYnU3BlY2lmaWNpdHkgZm9yIENDQSBtb2RlbCBpcyB7c3BlY19jY2F9LicpDQpwcmludChmJ0FjY3VyYWN5IGZvciBDQ0EgbW9kZWwgaXMge2FjY19jY2F9LicpDQpwcmludChmJ1RoZSBDQ0EgbW9kZWwgbWlzc2NsYXNzaWZpZWQge21pc3NfY2NhfSBvYnNlcnZhdGlvbnMgaW4gdGVzdC4nKQ0KYGBgDQojIyMgV2hpY2ggTW9kZWwgUHJvZHVjZWQgdGhlIEJlc3QgUmVzdWx0cz8NClRoZSBmb2xsb3dpbmcgY29kZSBibG9jayBjYXB0dXJlcyB0aGUgbW9kZWwgd2hpY2ggcHJvZHVjZWQgdGhlIGJlc3QgcmVzdWx0cywgYmFzZWQgb24gY29yZSBtZXRyaWMgKFNlbnNpdGl2aXR5KSBhcyB0aGlzIGlzIGEgbWlub3JpdHkgY2xhc3NpZmljYXRpb24gcHJvYmxlbS4NCmBgYHtweXRob259DQojIHNldCBtb2RlbCB2YXJpYWJsZSBmb3IgYmVzdCBwZXJmb3JtYW5jZSBhcyBNSSBvciBDQ0ENCmJldHRlcl9tb2RlbCA9ICdNSScNCg0KaWYgYmV0dGVyX21vZGVsPT0iQ0NBIiBvciAgYmV0dGVyX21vZGVsPT0iTUkiOg0KICBwcmludChmIntiZXR0ZXJfbW9kZWx9IGlzIHRoZSBiZXR0ZXIgbW9kZWwgYmFzZWQgb24gdGhpcyBkYXRhIGV4cGVyaW1lbnQuIikNCmVsc2U6DQogIHByaW50KCJQbGVhc2UgZW50ZXIgZWl0aGVyICdNSScgb3IgJ0NDQScgZm9yIHRoaXMgdmFyaWFibGUuIikNCmBgYA0KDQojIyMgRG9lcyBIaWdoZXN0IERlbHRhIEFkanVzdG1lbnQgSGF2ZSBCaWcgSW1wYWN0IG9uIFJlc3VsdHM/DQpgYGB7cHl0aG9ufQ0KI2RlZmluZSBhbmQgZml0IG1vZGVsDQpyZmM0ID0gUmFuZG9tRm9yZXN0Q2xhc3NpZmllcigpDQpyZmM0LmZpdCh4ZDRfdHJhaW4seWQ0X3RyYWluKQ0KDQojcHJlZGljdCBhZ2FpbnN0IHRlc3QNCm5wLnJhbmRvbS5zZWVkKDMpDQpwcmVkaWN0aW9uc19kNCA9IHJmYzQucHJlZGljdCh4ZDRfdGVzdCkNCg0KI2V2YWx1YXRlIHVzaW5nIGNvbmZ1c2lvbiBtYXRyaXgNCnRuNCwgZnA0LCBmbjQsIHRwNCA9IGNvbmZ1c2lvbl9tYXRyaXgoeWQ0X3Rlc3QsIHByZWRpY3Rpb25zX2Q0KS5yYXZlbCgpDQoNCnNlbnNfZDQgPSB0cDQvKHRwNCtmbjQpICN0cHINCnNwZWNfZDQgPSB0bjQvKGZwNCt0bjQpDQphY2NfZDQgPSAodHA0K3RuNCkvKHRwNCt0bjQrZnA0K2ZuNCkNCm1pc3NfZDQgPSBmcDQrZm40DQpwcmludChmJ1NlbnNpdGl2aXR5IGZvciBkZWx0YSA0IG1vZGVsIGlzIHtzZW5zX2Q0fS4nKQ0KcHJpbnQoZidTcGVjaWZpY2l0eSBmb3IgZGVsdGEgNCBtb2RlbCBpcyB7c3BlY19kNH0uJykNCnByaW50KGYnQWNjdXJhY3kgZm9yIGRlbHRhIDQgbW9kZWwgaXMge2FjY19kNH0uJykNCnByaW50KGYnVGhlIGRlbHRhIDQgbW9kZWwgbWlzc2NsYXNzaWZpZWQge21pc3NfZDR9IG9ic2VydmF0aW9ucyBpbiB0ZXN0LicpDQpgYGANCg0KVGhlIGltcGFjdCByZXN1bHQgaXMgY2FwdHVyZWQgYXMgYSBZZXMgb3IgTm8gcmVzcG9uc2UgaW4gdGhlIGNvZGUgYmVsb3cuDQpgYGB7cHl0aG9ufQ0KZGVsdGFfaW1wYWN0ID0gJ05vJw0KDQppZiBkZWx0YV9pbXBhY3Q9PSJZZXMiIG9yICBkZWx0YV9pbXBhY3Q9PSJObyI6DQogIHByaW50KGYie2RlbHRhX2ltcGFjdH0gaXMgdGhlIHZhbHVlIGZvciB0aGUgZGVsdGFfaW1wYWN0IHZhcmlhYmxlLiIpDQplbHNlOg0KICBwcmludCgiUGxlYXNlIGVudGVyIGVpdGhlciAnWWVzJyBvciAnTm8nIGZvciB0aGlzIHZhcmlhYmxlLiIpDQpgYGANCg0KIyBTdW1tYXJ5IGFuZCBSZWNvbW1lbmRhdGlvbnMNClRoZSBmb2xsb3dpbmcgY29kZSB3aWxsIHByaW50IGEgc3VtbWFyeSwgYmFzZWQgb24gdGhlIHZhcmlhYmxlIGluZm9ybWF0aW9uIHJlY29yZGVkIGluIGVhcmxpZXIgY29kZSBibG9ja3MuIA0KDQojIyMgSXMgZGF0YSBNQ0FSIG9yIE1BUi9NTkFSPw0KUHJpbnQgb2YgdmFyaWFibGUgdmFsdWVzIHNldCBhbHNvLg0KDQpgYGB7cHl0aG9ufQ0KcHJpbnQoZiJNaXNzaW5nIGRhdGEgbGV2ZWwgaXM6IHttaXNzaW5nbmVzc30uXG4iKQ0KcHJpbnQoZiJEYXRhIGlzIG1vcmUgbGlrZWx5IHRvIGJlIG1pc3NpbmcgaW4gc29tZSB2YXJpYWJsZXMgdGhhbiBvdGhlcnM6IHtsaWtlbGlob29kfS5cbiIpDQpwcmludChmIkxpdHRsZSdzIE1DQVIgVGVzdCBwIHZhbHVlIHJlc3VsdCBpczoge21jYXJfcHJlc3VsdH0uIFxuIikNCnByaW50KGYiRGF0YSBpcyBtaXNzaW5nIGZyb20gdGhlIGRlcGVuZGVudCB2YXJpYWJsZSBvbmx5OiB7ZGVwZW5kZW50X29ubHl9LiBcbiIpDQpgYGANCmBgYHtweXRob259DQppZiBsaWtlbGlob29kPT0iTm8iIGFuZCBtY2FyX3ByZXN1bHQ9PSJlcnJvciI6DQogIGh5cG90aGVzaXM9Ik1pc3NpbmcgZGF0YSBwYXR0ZXJuIHByb3ZpZGVzIHNvbWUgc3VwcG9ydCBmb3IgTUNBUiBoeXBvdGhlc2lzLiBIb3dldmVyLCBubyByZXN1bHQgYXZhaWxhYmxlIGZvciBNQ0FSIFRlc3QuIg0KZWxpZiBsaWtlbGlob29kPT0iTm8iIGFuZCBtY2FyX3ByZXN1bHQ8MC4wNToNCiAgaHlwb3RoZXNpcz0iSHlwb3RoZXNpcyB1bnByb3Zlbi4gTUNBUiBUZXN0IGFuZCBtaXNzaW5nIGRhdGEgcGF0dGVybiBpbmRpY2F0ZSBkaWZmZXJlbnQgbWlzc2luZyBkYXRhIG1lY2hhbmlzbXMuIg0KZWxpZiBsaWtlbGlob29kPT0iTm8iIGFuZCBtY2FyX3ByZXN1bHQ+PTAuMDU6DQogIGh5cG90aGVzaXM9Ik1DQVIgaHlwb3RoZXNpcyBzdXBwb3J0ZWQgYnkgTUNBUiBUZXN0IGFuZCBtaXNzaW5nIGRhdGEgcGF0dGVybi4iDQplbGlmIGxpa2VsaWhvb2Q9PSJZZXMiIGFuZCBtY2FyX3ByZXN1bHQ9PSJlcnJvciI6DQogIGh5cG90aGVzaXM9Ik1pc3NpbmcgZGF0YSBwYXR0ZXJuIHByb3ZpZGVzIHNvbWUgc3VwcG9ydCBmb3IgTUFSL01OQVIgaHlwb3RoZXNpcy4gSG93ZXZlciwgbm8gcmVzdWx0IGF2YWlsYWJsZSBmb3IgTUNBUiBUZXN0LiINCmVsaWYgbGlrZWxpaG9vZD09IlllcyIgYW5kIG1jYXJfcHJlc3VsdDwwLjA1Og0KICBoeXBvdGhlc2lzPSJNQVIvTU5BUiBoeXBvdGhlc2lzIHN1cHBvcnRlZCBieSBNQ0FSIFRlc3QgYW5kIG1pc3NpbmcgZGF0YSBwYXR0ZXJuLiINCmVsaWYgbGlrZWxpaG9vZD09IlllcyIgYW5kIG1jYXJfcHJlc3VsdD49MC4wNToNCiAgaHlwb3RoZXNpcz0iSHlwb3RoZXNpcyB1bnByb3Zlbi4gTUNBUiBUZXN0IGFuZCBtaXNzaW5nIGRhdGEgcGF0dGVybiBpbmRpY2F0ZSBkaWZmZXJlbnQgbWlzc2luZyBkYXRhIG1lY2hhbmlzbXMuIg0KZWxzZToNCiAgaHlwb3RoZXNpcz0iTm8gaHlwb3RoZXNpcyBmb3VuZC4iDQoNCnByaW50KGh5cG90aGVzaXMpDQpgYGANCg0KDQoNCiMjIyBSZWNvbW1lbmRlZCBBcHByb2FjaCBmb3IgTWlzc2luZyBEYXRhDQpgYGB7cHl0aG9ufQ0KDQojZGVmaW5lIGNvbmRpdGlvbmFsIHN0YXRlbWVudHMNCmlmIG1pc3NpbmduZXNzPj0wLjU6DQogIGFwcHJvYWNoPSJEdWUgdG8gb3ZlcmFsbCBsZXZlbCBvZiBtaXNzaW5nIGRhdGEgKG9yIGxldmVsIG9mIG1pc3NpbmcgZGF0YSBmcm9tIGRlcGVuZGVudCB2YXJpYWJsZSBvbmx5KSB0aGVyZSBpcyBhIHJpc2sgdGhhdCBhIHBvb3IgbW9kZWwgd2lsbCBiZSByZXR1cm5lZCwgcmVnYXJkbGVzcyBvZiBtZXRob2QgdXNlZCwgdGhhdCB3b3VsZCBub3QgZ2VuZXJhbGlzZSB3ZWxsIG9uIG5ldyBkYXRhLiINCg0KZWxpZiBtaXNzaW5nbmVzcz4wLjIgYW5kIGRlcGVuZGVudF9vbmx5PT0nWWVzJzoNCiAgYXBwcm9hY2g9IkR1ZSB0byBvdmVyYWxsIGxldmVsIG9mIG1pc3NpbmcgZGF0YSAob3IgbGV2ZWwgb2YgbWlzc2luZyBkYXRhIGZyb20gZGVwZW5kZW50IHZhcmlhYmxlIG9ubHkpIHRoZXJlIGlzIGEgcmlzayB0aGF0IGEgcG9vciBtb2RlbCB3aWxsIGJlIHJldHVybmVkLCByZWdhcmRsZXNzIG9mIG1ldGhvZCB1c2VkLCB0aGF0IHdvdWxkIG5vdCBnZW5lcmFsaXNlIHdlbGwgb24gbmV3IGRhdGEuIg0KDQplbGlmIGh5cG90aGVzaXM9PSJNQ0FSIGh5cG90aGVzaXMgc3VwcG9ydGVkIGJ5IE1DQVIgVGVzdCBhbmQgbWlzc2luZyBkYXRhIHBhdHRlcm4uIiBhbmQgbWlzc2luZ25lc3M8PTAuMjoNCiAgYXBwcm9hY2g9IkFzIE1DQVIgaHlwb3RoZXNpcyBpcyBzdXBwb3J0ZWQgYW5kIG1pc3NpbmcgZGF0YSBpcyBsZXNzIHRoYW4gMjAlLCBjb21wbGV0ZSBjYXNlIGFuYWx5c2lzIGlzIGxpa2VseSB0byBwcm9kdWNlIHVuYmlhc2VkIHJlc3VsdHMuIEhvd2V2ZXIsIG11bHRpcGxlIGltcHV0YXRpb24gbWF5IHByb2R1Y2UgYSBtb3JlIGVmZmVjdGl2ZSBtb2RlbCBpbiBzb21lIGNpcmN1bXN0YW5jZXMuIg0KDQplbGlmIGh5cG90aGVzaXM9PSJNQ0FSIGh5cG90aGVzaXMgc3VwcG9ydGVkIGJ5IE1DQVIgVGVzdCBhbmQgbWlzc2luZyBkYXRhIHBhdHRlcm4uIiBhbmQgbWlzc2luZ25lc3M+MC4yIGFuZCBtaXNzaW5nbmVzczwwLjU6DQogIGFwcHJvYWNoPSJNQ0FSIGh5cG90aGVzaXMgaXMgc3VwcG9ydGVkLiBIb3dldmVyLCBhcyBtaXNzaW5nIGRhdGEgaXMgYmV0d2VlbiAyMC01MCUsIG11bHRpcGxlIGltcHV0YXRpb24gaXMgbGlrZWx5IHRvIHByb2R1Y2UgYSBtb3JlIGVmZmVjdGl2ZSBtb2RlbC4iDQoNCmVsaWYgaHlwb3RoZXNpcyE9Ik1DQVIgaHlwb3RoZXNpcyBzdXBwb3J0ZWQgYnkgTUNBUiBUZXN0IGFuZCBtaXNzaW5nIGRhdGEgcGF0dGVybi4iIGFuZCBtaXNzaW5nbmVzczw9MC4yIGFuZCBkZXBlbmRlbnRfb25seT09IlllcyI6DQogIGFwcHJvYWNoPSJNQVIvTU5BUiBoeXBvdGhlc2lzIHN1cHBvcnRlZCwgb3IgTUNBUiBoeXBvdGhlc2lzIHVucHJvdmVuLiBIb3dldmVyLCBhcyBtaXNzaW5nIGRhdGEgaXMgbGVzcyB0aGFuIDIwJSBhbmQgbWlzc2luZyBmcm9tIHRoZSBkZXBlbmRlbnQgdmFyaWFibGUgb25seSwgY29tcGxldGUgY2FzZSBhbmFseXNpcyBtYXkgYmUgdGhlIG1vcmUgcmVsaWFibGUgYXBwcm9hY2guIg0KDQplbGlmIGh5cG90aGVzaXMhPSJNQ0FSIGh5cG90aGVzaXMgc3VwcG9ydGVkIGJ5IE1DQVIgVGVzdCBhbmQgbWlzc2luZyBkYXRhIHBhdHRlcm4uIiBhbmQgbWlzc2luZ25lc3M8PTAuNSBhbmQgZGVsdGFfaW1wYWN0PT0iTm8iOg0KICBhcHByb2FjaD0iTUFSL01OQVIgaHlwb3RoZXNpcyBzdXBwb3J0ZWQsIG9yIE1DQVIgaHlwb3RoZXNpcyB1bnByb3Zlbi4gQWxzbywgYXMgbWlzc2luZyBkYXRhIGlzIGxlc3MgdGhhbiA1MCUgYW5kIHRoZSBkZWx0YSBzZW5zaXRpdml0eSBhbmFseXNpcyBzdWdnZXN0cyB0aGUgcmVzdWx0cyBhcmUgcmVsYXRpdmVseSBzdGFibGUsIG11bHRpcGxlIGltcHV0YXRpb24gaXMgdGhlIHJlY29tbWVuZGVkIGFwcHJvYWNoLiINCg0KZWxpZiBoeXBvdGhlc2lzIT0iTUNBUiBoeXBvdGhlc2lzIHN1cHBvcnRlZCBieSBNQ0FSIFRlc3QgYW5kIG1pc3NpbmcgZGF0YSBwYXR0ZXJuLiIgYW5kIGRlbHRhX2ltcGFjdD09IlllcyI6DQogIGFwcHJvYWNoPSJNQVIvTU5BUiBoeXBvdGhlc2lzIHN1cHBvcnRlZCwgb3IgTUNBUiBoeXBvdGhlc2lzIHVucHJvdmVuLiBUaGUgZGVsdGEgc2Vuc2l0aXZpdHkgYW5hbHlzaXMgc3VnZ2VzdHMgdGhlIHJlc3VsdHMgYXJlIG5vdCBzdGFibGUgYW5kIGluZGljYXRpdmUgb2YgTU5BUiBkYXRhLiBUaGVyZWZvcmUsIGEgcGF0dGVybiBtaXh0dXJlIG1vZGVsIGlzIHJlY29tbWVuZGVkIGZvciBmdXJ0aGVyIGNvbnNpZGVyYXRpb24uIFRoZXJlIGFyZSBhIGNvdXBsZSBvZiBSIG1pY2UgcGFja2FnZSBmdW5jdGlvbnMgd29ydGggZXhwbG9yaW5nIGZ1cnRoZXIgZm9yIHRoaXM6IG1pY2UuaW1wdXRlLnJpIGFuZCBtaWNlLmltcHV0ZS5tbmFyLmxvZ3JlZyAodmFuIEJ1dXJlbiBldCBhbC4sIDIwMjMpLiINCg0KZWxzZToNCiAgYXBwcm9hY2g9Ik5vIGFwcHJvYWNoIGZvdW5kLiINCg0KcHJpbnQoYXBwcm9hY2gpDQpgYGANCg0KIyMjIENvbnNpZGVyYXRpb24gb2YgVGVzdCBNb2RlbCBSZXN1bHRzIGluIERhdGEgRXhwZXJpbWVudA0KYGBge3B5dGhvbn0NCmlmIGJldHRlcl9tb2RlbD09Ik1JIiBhbmQgYXBwcm9hY2g9PSJEdWUgdG8gb3ZlcmFsbCBsZXZlbCBvZiBtaXNzaW5nIGRhdGEgKG9yIGxldmVsIG9mIG1pc3NpbmcgZGF0YSBmcm9tIGRlcGVuZGVudCB2YXJpYWJsZSBvbmx5KSB0aGVyZSBpcyBhIHJpc2sgdGhhdCBhIHBvb3IgbW9kZWwgd2lsbCBiZSByZXR1cm5lZCwgcmVnYXJkbGVzcyBvZiBtZXRob2QgdXNlZCwgdGhhdCB3b3VsZCBub3QgZ2VuZXJhbGlzZSB3ZWxsIG9uIG5ldyBkYXRhLiI6DQogIHByaW50KCJGb3IgdGhpcyBwYXJ0aWN1bGFyIGRhdGEgZXhwZXJpbWVudCwgTXVsdGlwbGUgSW1wdXRhdGlvbiBwcm9kdWNlZCBhIG1vcmUgZWZmZWN0aXZlIG1vZGVsIHRoYW4gY29tcGxldGUgY2FzZSBhbmFseXNpcy4gSG93ZXZlciwgdGhlcmUgaXMgYSByaXNrIHRoYXQgaXQgd291bGQgbm90IGdlbmVyYWxpc2Ugd2VsbCBvbiBuZXcgZGF0YS4iKQ0KDQplbGlmIGJldHRlcl9tb2RlbD09IkNDQSIgYW5kIGFwcHJvYWNoPT0iRHVlIHRvIG92ZXJhbGwgbGV2ZWwgb2YgbWlzc2luZyBkYXRhIChvciBsZXZlbCBvZiBtaXNzaW5nIGRhdGEgZnJvbSBkZXBlbmRlbnQgdmFyaWFibGUgb25seSkgdGhlcmUgaXMgYSByaXNrIHRoYXQgYSBwb29yIG1vZGVsIHdpbGwgYmUgcmV0dXJuZWQsIHJlZ2FyZGxlc3Mgb2YgbWV0aG9kIHVzZWQsIHRoYXQgd291bGQgbm90IGdlbmVyYWxpc2Ugd2VsbCBvbiBuZXcgZGF0YS4iOg0KICBwcmludCgiRm9yIHRoaXMgcGFydGljdWxhciBkYXRhIGV4cGVyaW1lbnQsIGNvbXBsZXRlIGNhc2UgYW5hbHlzaXMgcHJvZHVjZWQgdGhlIG1vc3QgZWZmZWN0aXZlIG1vZGVsLiBIb3dldmVyLCB0aGVyZSBpcyBhIHJpc2sgdGhhdCBpdCB3b3VsZCBub3QgZ2VuZXJhbGlzZSB3ZWxsIG9uIG5ldyBkYXRhLiIpDQoNCmVsaWYgYmV0dGVyX21vZGVsPT0iTUkiIGFuZCBhcHByb2FjaD09IkFzIE1DQVIgaHlwb3RoZXNpcyBpcyBzdXBwb3J0ZWQgYW5kIG1pc3NpbmcgZGF0YSBpcyBsZXNzIHRoYW4gMjAlLCBjb21wbGV0ZSBjYXNlIGFuYWx5c2lzIGlzIGxpa2VseSB0byBwcm9kdWNlIHVuYmlhc2VkIHJlc3VsdHMuIEhvd2V2ZXIsIG11bHRpcGxlIGltcHV0YXRpb24gbWF5IHByb2R1Y2UgYSBtb3JlIGVmZmVjdGl2ZSBtb2RlbCBpbiBzb21lIGNpcmN1bXN0YW5jZXMuIjoNCiAgcHJpbnQoIkZvciB0aGlzIHBhcnRpY3VsYXIgZGF0YSBleHBlcmltZW50LCBNdWx0aXBsZSBJbXB1dGF0aW9uIHByb2R1Y2VkIGEgbW9yZSBlZmZlY3RpdmUgbW9kZWwgdGhhbiBjb21wbGV0ZSBjYXNlIGFuYWx5c2lzLiBIb3dldmVyLCBjb21wbGV0ZSBjYXNlIGFuYWx5c2lzIHdvdWxkIGJlIGxpa2VseSB0byBwcm9kdWNlIHVuYmlhc2VkIHJlc3VsdHMgYWxzby4iKQ0KICANCmVsaWYgYmV0dGVyX21vZGVsPT0iQ0NBIiBhbmQgYXBwcm9hY2g9PSJBcyBNQ0FSIGh5cG90aGVzaXMgaXMgc3VwcG9ydGVkIGFuZCBtaXNzaW5nIGRhdGEgaXMgbGVzcyB0aGFuIDIwJSwgY29tcGxldGUgY2FzZSBhbmFseXNpcyBpcyBsaWtlbHkgdG8gcHJvZHVjZSB1bmJpYXNlZCByZXN1bHRzLiBIb3dldmVyLCBtdWx0aXBsZSBpbXB1dGF0aW9uIG1heSBwcm9kdWNlIGEgbW9yZSBlZmZlY3RpdmUgbW9kZWwgaW4gc29tZSBjaXJjdW1zdGFuY2VzLiI6DQogIHByaW50KCJGb3IgdGhpcyBwYXJ0aWN1bGFyIGRhdGEgZXhwZXJpbWVudCwgY29tcGxldGUgY2FzZSBhbmFseXNpcyBwcm9kdWNlZCB0aGUgbW9zdCBlZmZlY3RpdmUgbW9kZWwuIEhvd2V2ZXIsIG11bHRpcGxlIGltcHV0YXRpb24gbWF5IHByb2R1Y2UgYSBtb3JlIGVmZmVjdGl2ZSBtb2RlbCBpbiBzb21lIGNpcmN1bXN0YW5jZXMuIikNCg0KZWxpZiBiZXR0ZXJfbW9kZWw9PSJNSSIgYW5kIGFwcHJvYWNoPT0iTUNBUiBoeXBvdGhlc2lzIGlzIHN1cHBvcnRlZC4gSG93ZXZlciwgYXMgbWlzc2luZyBkYXRhIGlzIGJldHdlZW4gMjAtNTAlLCBtdWx0aXBsZSBpbXB1dGF0aW9uIGlzIGxpa2VseSB0byBwcm9kdWNlIGEgbW9yZSBlZmZlY3RpdmUgbW9kZWwuIjoNCiAgIHByaW50KCJGb3IgdGhpcyBwYXJ0aWN1bGFyIGRhdGEgZXhwZXJpbWVudCwgTXVsdGlwbGUgSW1wdXRhdGlvbiBwcm9kdWNlZCBhIG1vcmUgZWZmZWN0aXZlIG1vZGVsIHRoYW4gY29tcGxldGUgY2FzZSBhbmFseXNpcy4gVGhpcyByZXN1bHQgaXMgZXhwZWN0ZWQsIGdpdmVuIHZhcmlhYmxlcyBwcm92aWRlZC4iKQ0KICAgDQplbGlmIGJldHRlcl9tb2RlbD09IkNDQSIgYW5kIGFwcHJvYWNoPT0iTUNBUiBoeXBvdGhlc2lzIGlzIHN1cHBvcnRlZC4gSG93ZXZlciwgYXMgbWlzc2luZyBkYXRhIGlzIGJldHdlZW4gMjAtNTAlLCBtdWx0aXBsZSBpbXB1dGF0aW9uIGlzIGxpa2VseSB0byBwcm9kdWNlIGEgbW9yZSBlZmZlY3RpdmUgbW9kZWwuIjoNCiAgIHByaW50KCJGb3IgdGhpcyBwYXJ0aWN1bGFyIGRhdGEgZXhwZXJpbWVudCwgY29tcGxldGUgY2FzZSBhbmFseXNpcyBwcm9kdWNlZCB0aGUgbW9zdCBlZmZlY3RpdmUgbW9kZWwuIEhvd2V2ZXIsIGNhdXRpb24gc2hvdWxkIGJlIG5vdGVkIG92ZXIgcmVzdWx0cyBkdWUgdG8gaGlnaCBsZXZlbCBvZiBtaXNzaW5nbmVzcy4iKQ0KICAgDQplbGlmIGJldHRlcl9tb2RlbD09Ik1JIiBhbmQgYXBwcm9hY2g9PSJNQVIvTU5BUiBoeXBvdGhlc2lzIHN1cHBvcnRlZCwgb3IgTUNBUiBoeXBvdGhlc2lzIHVucHJvdmVuLiBIb3dldmVyLCBhcyBtaXNzaW5nIGRhdGEgaXMgbGVzcyB0aGFuIDIwJSBhbmQgbWlzc2luZyBmcm9tIHRoZSBkZXBlbmRlbnQgdmFyaWFibGUgb25seSwgY29tcGxldGUgY2FzZSBhbmFseXNpcyBtYXkgYmUgdGhlIG1vcmUgcmVsaWFibGUgYXBwcm9hY2guIjoNCiAgIHByaW50KCJGb3IgdGhpcyBwYXJ0aWN1bGFyIGRhdGEgZXhwZXJpbWVudCwgTXVsdGlwbGUgSW1wdXRhdGlvbiBwcm9kdWNlZCBhIG1vcmUgZWZmZWN0aXZlIG1vZGVsIHRoYW4gY29tcGxldGUgY2FzZSBhbmFseXNpcy4gSG93ZXZlciwgY2F1dGlvbiBzaG91bGQgYmUgdGFrZW4gd2l0aCBob3cgbWlzc2luZyBkYXRhIGluIGRlcGVuZGVudCB2YXJpYWJsZXMgaXMgaGFuZGxlZCwgYXMgQ0NBIG1heSBiZSBtb3JlIHJlbGlhYmxlLiIpDQogICANCmVsaWYgYmV0dGVyX21vZGVsPT0iQ0NBIiBhbmQgYXBwcm9hY2g9PSJNQVIvTU5BUiBoeXBvdGhlc2lzIHN1cHBvcnRlZCwgb3IgTUNBUiBoeXBvdGhlc2lzIHVucHJvdmVuLiBIb3dldmVyLCBhcyBtaXNzaW5nIGRhdGEgaXMgbGVzcyB0aGFuIDIwJSBhbmQgbWlzc2luZyBmcm9tIHRoZSBkZXBlbmRlbnQgdmFyaWFibGUgb25seSwgY29tcGxldGUgY2FzZSBhbmFseXNpcyBtYXkgYmUgdGhlIG1vcmUgcmVsaWFibGUgYXBwcm9hY2guIjoNCiAgIHByaW50KCJGb3IgdGhpcyBwYXJ0aWN1bGFyIGRhdGEgZXhwZXJpbWVudCwgY29tcGxldGUgY2FzZSBhbmFseXNpcyBwcm9kdWNlZCB0aGUgbW9zdCBlZmZlY3RpdmUgbW9kZWwuIFRoaXMgc2hvdWxkIGJlIGEgcmVsaWFibGUgYXBwcm9hY2ggZm9yIHRoaXMgbWlzc2luZyBkYXRhIHByb2JsZW0uIikNCiAgIA0KZWxpZiBiZXR0ZXJfbW9kZWw9PSJNSSIgYW5kIGFwcHJvYWNoPT0iTUFSL01OQVIgaHlwb3RoZXNpcyBzdXBwb3J0ZWQsIG9yIE1DQVIgaHlwb3RoZXNpcyB1bnByb3Zlbi4gQWxzbywgYXMgbWlzc2luZyBkYXRhIGlzIGxlc3MgdGhhbiA1MCUgYW5kIHRoZSBkZWx0YSBzZW5zaXRpdml0eSBhbmFseXNpcyBzdWdnZXN0cyB0aGUgcmVzdWx0cyBhcmUgcmVsYXRpdmVseSBzdGFibGUsIG11bHRpcGxlIGltcHV0YXRpb24gaXMgdGhlIHJlY29tbWVuZGVkIGFwcHJvYWNoLiI6DQogICBwcmludCgiRm9yIHRoaXMgcGFydGljdWxhciBkYXRhIGV4cGVyaW1lbnQsIE11bHRpcGxlIEltcHV0YXRpb24gcHJvZHVjZWQgYSBtb3JlIGVmZmVjdGl2ZSBtb2RlbCB0aGFuIGNvbXBsZXRlIGNhc2UgYW5hbHlzaXMuIFRoaXMgc2hvdWxkIGJlIGEgcmVsaWFibGUgYXBwcm9hY2ggZm9yIHRoaXMgbWlzc2luZyBkYXRhIHByb2JsZW0uIikNCiAgIA0KZWxpZiBiZXR0ZXJfbW9kZWw9PSJDQ0EiIGFuZCBhcHByb2FjaD09Ik1BUi9NTkFSIGh5cG90aGVzaXMgc3VwcG9ydGVkLCBvciBNQ0FSIGh5cG90aGVzaXMgdW5wcm92ZW4uIEFsc28sIGFzIG1pc3NpbmcgZGF0YSBpcyBsZXNzIHRoYW4gNTAlIGFuZCB0aGUgZGVsdGEgc2Vuc2l0aXZpdHkgYW5hbHlzaXMgc3VnZ2VzdHMgdGhlIHJlc3VsdHMgYXJlIHJlbGF0aXZlbHkgc3RhYmxlLCBtdWx0aXBsZSBpbXB1dGF0aW9uIGlzIHRoZSByZWNvbW1lbmRlZCBhcHByb2FjaC4iOg0KICAgcHJpbnQoIkZvciB0aGlzIHBhcnRpY3VsYXIgZGF0YSBleHBlcmltZW50LCBjb21wbGV0ZSBjYXNlIGFuYWx5c2lzIHByb2R1Y2VkIHRoZSBtb3N0IGVmZmVjdGl2ZSBtb2RlbC4gSG93ZXZlciwgdGhlIE1DQVIgaHlwb3RoZXNpcyBpcyBlaXRoZXIgbm90IHN1cHBvcnRlZCBvciB1bmNsZWFyIHNvIGNhdXRpb24gaXMgYWR2aXNlZCBvbiB0aGUgcmVzdWx0cyBwcm9kdWNlZC4iKQ0KICAgDQplbGlmIGJldHRlcl9tb2RlbD09Ik1JIiBhbmQgYXBwcm9hY2g9PSJNQVIvTU5BUiBoeXBvdGhlc2lzIHN1cHBvcnRlZCwgb3IgTUNBUiBoeXBvdGhlc2lzIHVucHJvdmVuLiBUaGUgZGVsdGEgc2Vuc2l0aXZpdHkgYW5hbHlzaXMgc3VnZ2VzdHMgdGhlIHJlc3VsdHMgYXJlIG5vdCBzdGFibGUgYW5kIGluZGljYXRpdmUgb2YgTU5BUiBkYXRhLiBUaGVyZWZvcmUsIGEgcGF0dGVybiBtaXh0dXJlIG1vZGVsIGlzIHJlY29tbWVuZGVkIGZvciBmdXJ0aGVyIGNvbnNpZGVyYXRpb24uIFRoZXJlIGFyZSBhIGNvdXBsZSBvZiBSIG1pY2UgcGFja2FnZSBmdW5jdGlvbnMgd29ydGggZXhwbG9yaW5nIGZ1cnRoZXIgZm9yIHRoaXM6IG1pY2UuaW1wdXRlLnJpIGFuZCBtaWNlLmltcHV0ZS5tbmFyLmxvZ3JlZyAodmFuIEJ1dXJlbiBldCBhbC4sIDIwMjMpLiI6DQogICBwcmludCgiRm9yIHRoaXMgcGFydGljdWxhciBkYXRhIGV4cGVyaW1lbnQsIE11bHRpcGxlIEltcHV0YXRpb24gcHJvZHVjZWQgYSBtb3JlIGVmZmVjdGl2ZSBtb2RlbCB0aGFuIGNvbXBsZXRlIGNhc2UgYW5hbHlzaXMuIEhvd2V2ZXIsIGl0IGlzIHJlY29tbWVuZGVkIHRoYXQgYSBwYXR0ZXJuIG1peHR1cmUgbW9kZWwgaXMgY29uc2lkZXJlZCBhbHNvLiIpDQogICANCmVsaWYgYmV0dGVyX21vZGVsPT0iQ0NBIiBhbmQgYXBwcm9hY2g9PSJNQVIvTU5BUiBoeXBvdGhlc2lzIHN1cHBvcnRlZCwgb3IgTUNBUiBoeXBvdGhlc2lzIHVucHJvdmVuLiBUaGUgZGVsdGEgc2Vuc2l0aXZpdHkgYW5hbHlzaXMgc3VnZ2VzdHMgdGhlIHJlc3VsdHMgYXJlIG5vdCBzdGFibGUgYW5kIGluZGljYXRpdmUgb2YgTU5BUiBkYXRhLiBUaGVyZWZvcmUsIGEgcGF0dGVybiBtaXh0dXJlIG1vZGVsIGlzIHJlY29tbWVuZGVkIGZvciBmdXJ0aGVyIGNvbnNpZGVyYXRpb24uIFRoZXJlIGFyZSBhIGNvdXBsZSBvZiBSIG1pY2UgcGFja2FnZSBmdW5jdGlvbnMgd29ydGggZXhwbG9yaW5nIGZ1cnRoZXIgZm9yIHRoaXM6IG1pY2UuaW1wdXRlLnJpIGFuZCBtaWNlLmltcHV0ZS5tbmFyLmxvZ3JlZyAodmFuIEJ1dXJlbiBldCBhbC4sIDIwMjMpLiI6DQogICBwcmludCgiRm9yIHRoaXMgcGFydGljdWxhciBkYXRhIGV4cGVyaW1lbnQsIGNvbXBsZXRlIGNhc2UgYW5hbHlzaXMgcHJvZHVjZWQgdGhlIG1vc3QgZWZmZWN0aXZlIG1vZGVsLiBIb3dldmVyLCBpdCBpcyByZWNvbW1lbmRlZCB0aGF0IGEgcGF0dGVybiBtaXh0dXJlIG1vZGVsIGlzIGNvbnNpZGVyZWQgYWxzby4iKQ0KICAgDQplbHNlOg0KICAgcHJpbnQoIk5vIG1vZGVsIGV2YWx1YXRpb24gZm91bmQuIikNCg0KYGBgDQoNCg0KLS1MYXN0IHVwZGF0ZWQ6IEF1Z3VzdCAyMDIzIC0tICANCg0KLS1FbmQtLQ0K