1. PREPARE

When we left off the first case study, we saw that our model was pretty accurate. We can and will do better in terms of accuracy. But, the accuracy value we found also raises a broader question: How good was our model in terms of making predictions?

While accuracy is an easy to understand and interpret value, it provides a limited answer to the above question about how good our model was in terms of making predictions.

In this learning and case study, we explore a variety of ways to understand how good of a predictive model ours is. We do this through a variety of means:

  • Other statistics, such as sensitivity and specificity

  • Tables–particularly, a confusion matrix

Our driving question for this case study, then, is: How good is our model at making predictions?

We’ll use the Open University Learning Analytics Dataset (OULAD) again–this time, adding another data source, one on students’ performance on assessments.

Like in the first learning lab, we’ll first load several packages – {tidyverse}, {tidymodels}, and {janitor}. Make sure these are installed (via install.packages()). Note that if they’re already installed, you don’t need to install them again.

library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.4     ✔ readr     2.1.5
## ✔ forcats   1.0.0     ✔ stringr   1.5.1
## ✔ ggplot2   3.5.0     ✔ tibble    3.2.1
## ✔ lubridate 1.9.3     ✔ tidyr     1.3.1
## ✔ purrr     1.0.2     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(tidymodels)
## ── Attaching packages ────────────────────────────────────── tidymodels 1.2.0 ──
## ✔ broom        1.0.5      ✔ rsample      1.2.1 
## ✔ dials        1.2.1      ✔ tune         1.2.0 
## ✔ infer        1.0.7      ✔ workflows    1.1.4 
## ✔ modeldata    1.3.0      ✔ workflowsets 1.1.0 
## ✔ parsnip      1.2.1      ✔ yardstick    1.3.1 
## ✔ recipes      1.0.10     
## ── Conflicts ───────────────────────────────────────── tidymodels_conflicts() ──
## ✖ scales::discard() masks purrr::discard()
## ✖ dplyr::filter()   masks stats::filter()
## ✖ recipes::fixed()  masks stringr::fixed()
## ✖ dplyr::lag()      masks stats::lag()
## ✖ yardstick::spec() masks readr::spec()
## ✖ recipes::step()   masks stats::step()
## • Learn how to get started at https://www.tidymodels.org/start/
library(janitor)
## 
## Attaching package: 'janitor'
## 
## The following objects are masked from 'package:stats':
## 
##     chisq.test, fisher.test
library(yardstick)
library(dplyr)

2. WRANGLE

Like in the code-along for the overview presentation, let’s take a look at the data and do some processing of it.

Your Turn

We’ll load the students file together; you’ll write code to read the assessments file, which is named “oulad-assessments.csv”. Please assign the name assessments to the loaded assessments file.

assessments <- read.csv("data/oulad-assessments.csv")

3. EXPLORE

Your Turn

In the last lab, we used the count() function. Let’s practice that again, by counting the number of of assessments of different types. If you need, recall that the data dictionary is here. Note what the different types of assessments mean.

##view(assessments)

assessments %>% 
    count(assessment_type) # number of each assessment type
##   assessment_type     n
## 1             CMA 70527
## 2            Exam  4959
## 3             TMA 98426
  • Three types of assessments exist: Tutor Marked Assessment (TMA), Computer Marked Assessment (CMA) and Final Exam (Exam).

Your Turn

We’ll now use another function–like count(), from the {tidyverse}. Specifically, we’ll use the distinct() function. This returns the unique (or distinct) values for a specified variable. Learn more about distinct() here. Below, find the distinct assessment IDs.

assessments %>%
  distinct(id_assessment)
##     id_assessment
## 1            1752
## 2            1753
## 3            1754
## 4            1755
## 5            1756
## 6            1758
## 7            1759
## 8            1760
## 9            1761
## 10           1762
## 11          14984
## 12          14985
## 13          14986
## 14          14987
## 15          14988
## 16          14989
## 17          14991
## 18          14992
## 19          14993
## 20          14994
## 21          14995
## 22          14996
## 23          14997
## 24          14998
## 25          14999
## 26          15000
## 27          15001
## 28          15003
## 29          15004
## 30          15005
## 31          15006
## 32          15007
## 33          15008
## 34          15009
## 35          15010
## 36          15011
## 37          15012
## 38          15013
## 39          15015
## 40          15016
## 41          15017
## 42          15018
## 43          15019
## 44          15020
## 45          15021
## 46          15022
## 47          15023
## 48          15024
## 49          24282
## 50          24283
## 51          24284
## 52          24285
## 53          24286
## 54          24287
## 55          24288
## 56          24289
## 57          24290
## 58          24291
## 59          24292
## 60          24293
## 61          24294
## 62          24295
## 63          24296
## 64          24297
## 65          24298
## 66          24299
## 67          25334
## 68          25335
## 69          25336
## 70          25337
## 71          25338
## 72          25339
## 73          25340
## 74          25341
## 75          25342
## 76          25343
## 77          25344
## 78          25345
## 79          25346
## 80          25347
## 81          25348
## 82          25349
## 83          25350
## 84          25351
## 85          25352
## 86          25353
## 87          25354
## 88          25355
## 89          25356
## 90          25357
## 91          25358
## 92          25359
## 93          25360
## 94          25361
## 95          25362
## 96          25363
## 97          25364
## 98          25365
## 99          25366
## 100         25367
## 101         25368
## 102         30709
## 103         30710
## 104         30711
## 105         30712
## 106         30714
## 107         30715
## 108         30716
## 109         30717
## 110         30719
## 111         30720
## 112         30721
## 113         30722
## 114         34860
## 115         34861
## 116         34862
## 117         34863
## 118         34864
## 119         34865
## 120         34866
## 121         34867
## 122         34868
## 123         34869
## 124         34870
## 125         34871
## 126         34873
## 127         34874
## 128         34875
## 129         34876
## 130         34877
## 131         34878
## 132         34879
## 133         34880
## 134         34881
## 135         34882
## 136         34883
## 137         34884
## 138         34886
## 139         34887
## 140         34888
## 141         34889
## 142         34890
## 143         34891
## 144         34892
## 145         34893
## 146         34894
## 147         34895
## 148         34896
## 149         34897
## 150         34899
## 151         34900
## 152         34901
## 153         34902
## 154         34903
## 155         34904
## 156         34905
## 157         34906
## 158         34907
## 159         34908
## 160         34909
## 161         34910
## 162         37415
## 163         37416
## 164         37417
## 165         37418
## 166         37419
## 167         37420
## 168         37421
## 169         37422
## 170         37423
## 171         37425
## 172         37426
## 173         37427
## 174         37428
## 175         37429
## 176         37430
## 177         37431
## 178         37432
## 179         37433
## 180         37435
## 181         37436
## 182         37437
## 183         37438
## 184         37439
## 185         37440
## 186         37441
## 187         37442
## 188         37443

Let’s explore the assessments data a bit.

We might be interested in how many assessments there are per course. To calculate that, we need to count() several variables at once; when doing this, count() tabulates the number of unique combinations of the variables passed to it.

assessments %>% 
    count(assessment_type, code_module, code_presentation)
##    assessment_type code_module code_presentation    n
## 1              CMA         BBB             2013B 5049
## 2              CMA         BBB             2013J 6416
## 3              CMA         BBB             2014B 4493
## 4              CMA         CCC             2014B 3920
## 5              CMA         CCC             2014J 5846
## 6              CMA         DDD             2013B 5252
## 7              CMA         FFF             2013B 6681
## 8              CMA         FFF             2013J 8847
## 9              CMA         FFF             2014B 5549
## 10             CMA         FFF             2014J 8915
## 11             CMA         GGG             2013J 3749
## 12             CMA         GGG             2014B 3063
## 13             CMA         GGG             2014J 2747
## 14            Exam         CCC             2014B  747
## 15            Exam         CCC             2014J 1168
## 16            Exam         DDD             2013B  602
## 17            Exam         DDD             2013J  968
## 18            Exam         DDD             2014B  524
## 19            Exam         DDD             2014J  950
## 20             TMA         AAA             2013J 1633
## 21             TMA         AAA             2014J 1516
## 22             TMA         BBB             2013B 6207
## 23             TMA         BBB             2013J 7959
## 24             TMA         BBB             2014B 5500
## 25             TMA         BBB             2014J 7408
## 26             TMA         CCC             2014B 2822
## 27             TMA         CCC             2014J 4437
## 28             TMA         DDD             2013B 4519
## 29             TMA         DDD             2013J 6968
## 30             TMA         DDD             2014B 4018
## 31             TMA         DDD             2014J 7063
## 32             TMA         EEE             2013J 2884
## 33             TMA         EEE             2014B 1780
## 34             TMA         EEE             2014J 3229
## 35             TMA         FFF             2013B 5514
## 36             TMA         FFF             2013J 7393
## 37             TMA         FFF             2014B 4647
## 38             TMA         FFF             2014J 7269
## 39             TMA         GGG             2013J 2201
## 40             TMA         GGG             2014B 1833
## 41             TMA         GGG             2014J 1626

Let’s explore the dates assignments were submitted a bit – using the summarize() function:

assessments %>% 
    summarize(mean_date = mean(date, na.rm = TRUE), # find the mean date for assignments
              median_date = median(date, na.rm = TRUE), # find the median
              sd_date = sd(date, na.rm = TRUE), # find the sd
              min_date = min(date, na.rm = TRUE), # find the min
              max_date = max(date, na.rm = TRUE)) # find the mad
##   mean_date median_date  sd_date min_date max_date
## 1  130.6056         129 78.02517       12      261

What can we take from this? It looks like, on average, the average (mean and median) date assignments were due was around 130 – 130 days after the start of the course. The first assignment seems to have been due 12 days into the course, and the last 261 days after.

Crucially, though, these dates vary by course. Thus, we need to first group the data by course. Let’s use a different function this time – quantile(), and calculate the first quantile value. Our reasoning is that we want to be able to act to support students, and if we wait until after the average assignment is due, then that might be too late. Whereas the first quantile comes approximately one-quarter through the semester — with, therefore, more time to intervene.

assessments %>% 
    group_by(code_module, code_presentation) %>% # first, group by course (module: course; presentation: semester)
    summarize(mean_date = mean(date, na.rm = TRUE),
              median_date = median(date, na.rm = TRUE),
              sd_date = sd(date, na.rm = TRUE),
              min_date = min(date, na.rm = TRUE),
              max_date = max(date, na.rm = TRUE),
              first_quantile = quantile(date, probs = .25, na.rm = TRUE)) # find the first (25%) quantile
## `summarise()` has grouped output by 'code_module'. You can override using the
## `.groups` argument.
## # A tibble: 22 × 8
## # Groups:   code_module [7]
##    code_module code_presentation mean_date median_date sd_date min_date max_date
##    <chr>       <chr>                 <dbl>       <dbl>   <dbl>    <int>    <int>
##  1 AAA         2013J                 109.          117    71.3       19      215
##  2 AAA         2014J                 109.          117    71.5       19      215
##  3 BBB         2013B                 104.           89    55.5       19      187
##  4 BBB         2013J                 112.           96    61.6       19      208
##  5 BBB         2014B                  98.9          82    58.6       12      194
##  6 BBB         2014J                  99.1         110    65.2       19      201
##  7 CCC         2014B                  98.4         102    68.0       18      207
##  8 CCC         2014J                 104.          109    70.8       18      214
##  9 DDD         2013B                 104.           81    66.0       23      240
## 10 DDD         2013J                 118.           88    77.9       25      261
## # ℹ 12 more rows
## # ℹ 1 more variable: first_quantile <dbl>

Alright, this is a bit complicated, but we can actually work with this data. Let’s use just a portion of the above code, assigning it the name code_module_dates.

code_module_dates <- assessments %>% 
    group_by(code_module, code_presentation) %>% 
    summarize(quantile_cutoff_date = quantile(date, probs = .25, na.rm = TRUE))
## `summarise()` has grouped output by 'code_module'. You can override using the
## `.groups` argument.

Your Turn

Let’s take a look at what we just created; type code_module_dates below:

code_module_dates
## # A tibble: 22 × 3
## # Groups:   code_module [7]
##    code_module code_presentation quantile_cutoff_date
##    <chr>       <chr>                            <dbl>
##  1 AAA         2013J                               54
##  2 AAA         2014J                               54
##  3 BBB         2013B                               54
##  4 BBB         2013J                               54
##  5 BBB         2014B                               47
##  6 BBB         2014J                               54
##  7 CCC         2014B                               32
##  8 CCC         2014J                               32
##  9 DDD         2013B                               51
## 10 DDD         2013J                               53
## # ℹ 12 more rows

What have we created? We found the date that is one-quarter of the way through the semester (in terms of the dates assignments are due).

Your Turn

We can thus use this to group and calculate students’ performance on assignments through this point. To do this, we need to use a join function — left_join(), in particular. Please use left_join() to join together assessments and code_module_dates, with assessments being the “left” data frame, and code_module_dates the right. Save the output of the join the name assessments_joined.

##view(code_module_dates)

assessments_joined <- merge(x = assessments, y = code_module_dates,by=c("code_module","code_presentation"),all.x=TRUE)

##view(assessments_joined)

We’re almost there! The next few lines filter the assessments data so it only includes assessments before our cutoff date.

assessments_filtered <- assessments_joined %>% 
    filter(date < quantile_cutoff_date) # filter the data so only assignments before the cutoff date are included

##view(assessments_filtered)

Finally, we’ll find the average score for each student.

assessments_summarized <- assessments_filtered %>% 
    mutate(weighted_score = score * weight) %>% # create a new variable that accounts for the "weight" (comparable to points) given each assignment
    group_by(id_student) %>% 
    summarize(mean_weighted_score = mean(weighted_score)) 

##view(assessments_summarized)

As a point of reflection here, note how much work we’ve done before we’ve gotten to the machine learning steps. Though a challenge, this is typical for both machine learning and more traditional statistical models: a lot of the work is in the preparation and data wrangling stages.

Let’s copy the code below that we used to process the students data (when processing the pass and imd_band variables).

students <- read_csv("data/oulad-students.csv")
## Rows: 32593 Columns: 15
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (9): code_module, code_presentation, gender, region, highest_education, ...
## dbl (6): id_student, num_of_prev_attempts, studied_credits, module_presentat...
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
students <- students %>% 
    mutate(pass = ifelse(final_result == "Pass", 1, 0)) %>% # creates a dummy code
    mutate(pass = as.factor(pass)) # makes the variable a factor, helping later steps

students <- students %>% 
    mutate(imd_band = factor(imd_band, levels = c("0-10%",
                                                  "10-20%",
                                                  "20-30%",
                                                  "30-40%",
                                                  "40-50%",
                                                  "50-60%",
                                                  "60-70%",
                                                  "70-80%",
                                                  "80-90%",
                                                  "90-100%"))) %>% # this creates a factor with ordered levels
    mutate(imd_band = as.integer(imd_band)) # this changes the levels into integers based on the order of the factor levels

Your Turn

Finally, let’s join together students and assessments_summarized, assigning the joined the name students_and_assessments.

##view(students)
##view(assessments_summarized)

## this is an inner join, using left join there were student_id's that were not in the assessments_summarized table
students_and_assessments <- merge(x = students, y = assessments_summarized, by="id_student")

##view(students_and_assessments)

Big picture, we’ve added another measure – another feature – that we can use to make predictions: students’ performance on assessments 1/4 of the way through the course.

We’re now ready to proceed to our machine learning steps!

The problem we will be working on - predicting students who pass, based on data from the first one-third of the semester - has an analog in a recent paper by Ryan Baker and colleagues:

In Baker et al. (2020), the authors create a precision-recall (also known as sensitivity) graph - one that demonstrates the trade-off between optimizing these two statistics. Review their paper - especially the results section - to see how they discuss these two statistics.

Baker, R. S., Berning, A. W., Gowda, S. M., Zhang, S., & Hawn, A. (2020). Predicting K-12 dropout. Journal of Education for Students Placed at Risk, 25(1), 28-54.

Please review this paper before proceeding, focusing on how they describe

4. MODEL

Step 1. Split data

This is identical to what we did in the first learning lab, using the students_and_assessments data. We’ll also create a testing data set we’ll use later.

set.seed(20230712)

students_and_assessments <- students_and_assessments %>% 
    drop_na(mean_weighted_score)

train_test_split <- initial_split(students_and_assessments, prop = .50, strata = "pass")
data_train <- training(train_test_split)
data_test <- testing(train_test_split)

Step 2: Engineer features and write down the recipe

We’ll also specify the recipe, adding our mean_weighted_score variable we calculated above as well as variables we used in LL1 (case study and badge). Note how we dummy code two variables using step_dummy() (described further in the first learning lab).

my_rec <- recipe(pass ~ disability +
                     date_registration + 
                     gender +
                     code_module +
                     mean_weighted_score, 
                 data = data_train) %>% 
    step_dummy(disability) %>% 
    step_dummy(gender) %>%  
    step_dummy(code_module)

Step 3: Specify the model and workflow

These steps are also the same as in LL1.

# specify model
my_mod <-
    logistic_reg() %>% 
    set_engine("glm") %>% # generalized linear model
    set_mode("classification") # since we are predicting a dichotomous outcome, specify classification; for a number, specify regression

# specify workflow
my_wf <-
    workflow() %>% # create a workflow
    add_model(my_mod) %>% # add the model we wrote above
    add_recipe(my_rec) # add our recipe we wrote above

Step 4: Fit model

Finally, we’ll fit our model.

fitted_model <- fit(my_wf, data = data_train)

Finally, we’ll use the last_fit function, but we’ll add something a little different - we’ll add the metric set here. To the above, we’ll add another common metric - Cohen’s Kappa, which is similar to accuracy, but which accounts for chance agreement.

To do so, we have to specify which metrics we want to use using the metric_set() function (see all of the available options here). Please specify six metrics in the metric set – accuracy, sensitivity, specificity, ppv (recall), npv, and kappa. Assign the name class_metrics to the output of your use of the metric_set() function.

Your Turn

class_metrics <- metric_set(yardstick::accuracy, sensitivity, specificity, ppv, npv, kap)

Then, please use last_fit, looking to how we did this in the last learning lab for a guide on how to specify the argument nts. To the arguments, add metrics = class_metrics.

final_fit <- last_fit(my_wf, train_test_split, metrics = class_metrics)

We’re now ready to move on to interpreting the accuracy of our model.

Step 5: Interpret accuracy

A confusion matrix and true and false positives and negatives

Let’s start with a simple confusion matrix. The confusion matrix is a 2 x 2 table with values (cells in the table) representing one of four conditions, elaborated below. You’ll fill in the last two columns in a few moments.

Statistic How to Find Interpretation Value %
True positive (TP) Truth: 1, Prediction: 1 Proportion of the population that is affected by a condition and correctly tested positive
True negative (TN) Truth: 0, Prediction: 0 Proportion of the population that is not affected by a condition and correctly tested negative
False positive (FP) Truth: 0, Prediction: 1 Proportion of the population that is not affected by a condition and incorrectly tested positive
False negative (FN) Truth: 1, Prediction: 0 Proportion of the population that is affected by a condition and incorrectly tested positive.

To create a confusion matrix, run collect_predictions(), which does what it suggests - it gathers together the model’s test set predictions. Pass the last_fit object to this function below.

Your Turn

predictions <- collect_predictions(final_fit)
predictions
## # A tibble: 12,318 × 5
##    .pred_class id                .row pass  .config             
##    <fct>       <chr>            <int> <fct> <chr>               
##  1 1           train/test split     1 1     Preprocessor1_Model1
##  2 0           train/test split     3 0     Preprocessor1_Model1
##  3 1           train/test split     4 1     Preprocessor1_Model1
##  4 1           train/test split     5 0     Preprocessor1_Model1
##  5 0           train/test split     6 1     Preprocessor1_Model1
##  6 1           train/test split     7 0     Preprocessor1_Model1
##  7 1           train/test split    11 1     Preprocessor1_Model1
##  8 1           train/test split    12 1     Preprocessor1_Model1
##  9 0           train/test split    14 0     Preprocessor1_Model1
## 10 1           train/test split    17 0     Preprocessor1_Model1
## # ℹ 12,308 more rows

Take a look at the columns. You’ll need to provide the predictions (created with collect_predictions()) and then pipe that to conf_mat(), to which you provide the names of a) the predictions and b) the known values for the test set. Some code to get you started is below.

# Calculate confusion matrix
confusion_matrix <- predictions %>%
  conf_mat(.pred_class, pass)
  ##conf_mat(truth = truth, estimate =.pred_class)
##confusion_matrix


# Total number of data points in the test data set
total <- sum(confusion_matrix$table)

# Extract TP, TN, FP, FN from the confusion matrix
TP <- confusion_matrix$table[2, 2]  # True Positives
TN <- confusion_matrix$table[1, 1]  # True Negatives
FP <- confusion_matrix$table[1, 2]  # False Positives
FN <- confusion_matrix$table[2, 1]  # False Negatives


# Calculate percentages
TP_percentage <- (TP / total) * 100
TN_percentage <- (TN / total) * 100
FP_percentage <- (FP / total) * 100
FN_percentage <- (FN / total) * 100


# Calculate accuracy (sum of TP and TN percentages)
accuracy <- (TP_percentage + TN_percentage)


# Fill in the values and percentages
confusion_matrix$Value <- c(TP, TN, FP, FN)
confusion_matrix$Percentage <- c(TP_percentage, TN_percentage, FP_percentage, FN_percentage)


# Confusion matrix with filled values and percentages
str(confusion_matrix)
## List of 3
##  $ table     : 'table' num [1:2, 1:2] 4589 3346 2012 2366
##   ..- attr(*, "dimnames")=List of 2
##   .. ..$ Prediction: chr [1:2] "0" "1"
##   .. ..$ Truth     : chr [1:2] "0" "1"
##  $ Value     : num [1:4] 2366 4589 2012 3346
##  $ Percentage: num [1:4] 19.2 37.3 16.3 27.2
##  - attr(*, "class")= chr "conf_mat"
view(confusion_matrix)
print(confusion_matrix)
##           Truth
## Prediction    0    1
##          0 4589 2012
##          1 3346 2366
# Print accuracy
cat("Accuracy:", accuracy, "%\n")
## Accuracy: 56.48502 %

You should see a confusion matrix output. If so, nice work! Please fill in the Value and Percentage columns in the table above (with TP, TN, FP, and FN), entering the appropriate values and then converting those into a percentage based on the total number of data points in the test data set. The accuracy can be interpreted as the sum of the true positive and true negative percentages. So, what’s the accuracy? Add that below as a percentage.

  • Accuracy: 56.48502 %

Other measures of predictive accuracy

Here’s where things get interesting: There are other statistics that have different denominators. Recall from the overview presentation that we can slice and dice the confusion matrix to calculate statistics that give us insights into the predictive utility of the model. Based on the above Values for TP, TN, FP, and FN you interpreted a few moments ago, add the Statistic Values for sensitivity, specificity, precision, and negative predictive value below to three decimal points.

Your Turn

Statistic Equation Interpretation Question Answered Statistic Values
Sensitivity (AKA recall) TP / (TP + FN) Proportion of those who are affected by a condition and correctly tested positive Out of all the actual positives, how many did we correctly predict?
Specificity TN / (FP + TN) Proportion of those who are not affected by a condition and correctly tested negative; Out of all the actual negatives, how many did we correctly predict?
Precision (AKA Positive Predictive Value) TP / (TP + FP) Proportion of those who tested positive who are affected by a condition Out of all the instances we predicted as positive, how many are actually positive?
Negative Predictive Value TN / (TN + FN) Proportion of those who tested positive who are not affected by a condition; the probability that a negative test is correct Out of all the instances we predicted as negative, how many are actually negative?

So, what does this hard-won by output tell us? Let’s interpret each statistic carefully in the table below. Please add the value and provide a substantive interpretation. One is provided to get you started.

Your Turn

Statistic Substantive Interpretation
Accuracy The model has an accuracy of 56.5%.This measures the overall correctness of the model’s predictions.
Sensitivity (AKA recall) The model correctly predicts about 2/3 of students who do not pass correctly (as not passing). This is pretty good, but it could be better.
Specificity
Precision (AKA Positive Predictive Value) 54%. Measures the accuracy of positive predictions made by the model.
Negative Predictive Value 57.8%. Measures the proportion of true negative predictions out of all the instances predicted as negative by the model.In other words, NPV measures the model’s ability to correctly identify negative instances. A high NPV indicates that the model is good at avoiding false negatives, meaning it correctly identifies instances that do not belong to the positive class.
sensitivity_calc <- round(TP / (TP + FN),3)
sensitivity_calc
## [1] 0.414
precision_calc <- round(TP / (TP + FP),3)
precision_calc
## [1] 0.54
npd_value <- round(TN / (TN + FN),3)
npd_value
## [1] 0.578
specificity_calc <- round(TN / (FP + TN),3)
specificity_calc
## [1] 0.695

This process might suggest to you how a “good” model isn’t necessarily one that is the most accurate, but instead is one that has good values for statistics that matter given our particular question and context.

Recall that Baker and colleagues sought to balance between precision and recall (specificity). Please briefly discuss how well our model does this; is it better suited to correctly identifying “positive” pass cases (sensitivity) or “negatively” identifying students who do not pass (specificity)?

  • Sensitivity(positive pass cases) = 41.4% Proportion of those who are affected by a condition and correctly tested positive. Specificity(negative pass cases) = 69.5% Proportion of those who are not affected by a condition and correctly tested negative. The model is better suited at correctly identifying specificty or those who are not affected by a condition and correclty tested negative. ## 5. COMMUNICATE

Quickly calculating metrics

After all of this work, we can calculate the above much more easily given how we setup our metrics (using metric_set() earlier, such as when we want to efficiently communicate the results of our analysis to our audience? Below, use collect_metrics() with the final_fit object, noting that in addition to the four metrics we calculated manually just a few moments ago, we are also provided with the accuracy and Cohen’s Kappa values.

Your Turn

collect_metrics(final_fit)
## # A tibble: 6 × 4
##   .metric     .estimator .estimate .config             
##   <chr>       <chr>          <dbl> <chr>               
## 1 accuracy    binary         0.565 Preprocessor1_Model1
## 2 sensitivity binary         0.695 Preprocessor1_Model1
## 3 specificity binary         0.414 Preprocessor1_Model1
## 4 ppv         binary         0.578 Preprocessor1_Model1
## 5 npv         binary         0.540 Preprocessor1_Model1
## 6 kap         binary         0.111 Preprocessor1_Model1

Having invested in understanding the terminology of machine learning metrics, we’ll use this “shortcut” (using collect_metrics() going forward.

🧶 Knit & Check ✅

Congratulations - you’ve completed this case study! Go over to Lab 2 badge activity.

LS0tDQp0aXRsZTogJ0xhYiAyIENhc2UgU3R1ZHk6IEludGVycHJldGF0aW9uJw0KYXV0aG9yOiAiQXVzdGluIEhhbm5vbGQiDQpkYXRlOiAiYHIgZm9ybWF0KFN5cy5EYXRlKCksJyVCICVlLCAlWScpYCINCm91dHB1dDoNCiAgaHRtbF9kb2N1bWVudDoNCiAgICB0b2M6IHllcw0KICAgIHRvY19kZXB0aDogNA0KICAgIHRvY19mbG9hdDogeWVzDQogICAgY29kZV9mb2xkaW5nOiBzaG93DQogICAgY29kZV9kb3dubG9hZDogVFJVRQ0KZWRpdG9yX29wdGlvbnM6DQogIG1hcmtkb3duOg0KICAgIHdyYXA6IDcyDQojI2JpYmxpb2dyYXBoeTogbGl0L3JlZmVyZW5jZXMuYmliDQotLS0NCg0KYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9DQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IFRSVUUpDQpgYGANCg0KIyMgMS4gUFJFUEFSRQ0KDQpXaGVuIHdlIGxlZnQgb2ZmIHRoZSBmaXJzdCBjYXNlIHN0dWR5LCB3ZSBzYXcgdGhhdCBvdXIgbW9kZWwgd2FzICpwcmV0dHkNCmFjY3VyYXRlKi4gV2UgY2FuIGFuZCB3aWxsIGRvIGJldHRlciBpbiB0ZXJtcyBvZiBhY2N1cmFjeS4gQnV0LCB0aGUNCmFjY3VyYWN5IHZhbHVlIHdlIGZvdW5kIGFsc28gcmFpc2VzIGEgYnJvYWRlciBxdWVzdGlvbjogSG93IGdvb2Qgd2FzIG91cg0KbW9kZWwgaW4gdGVybXMgb2YgbWFraW5nIHByZWRpY3Rpb25zPw0KDQpXaGlsZSBhY2N1cmFjeSBpcyBhbiBlYXN5IHRvIHVuZGVyc3RhbmQgYW5kIGludGVycHJldCB2YWx1ZSwgaXQgcHJvdmlkZXMNCmEgbGltaXRlZCBhbnN3ZXIgdG8gdGhlIGFib3ZlIHF1ZXN0aW9uIGFib3V0IGhvdyBnb29kIG91ciBtb2RlbCB3YXMgaW4NCnRlcm1zIG9mIG1ha2luZyBwcmVkaWN0aW9ucy4NCg0KSW4gdGhpcyBsZWFybmluZyBhbmQgY2FzZSBzdHVkeSwgd2UgZXhwbG9yZSBhIHZhcmlldHkgb2Ygd2F5cyB0bw0KdW5kZXJzdGFuZCBob3cgZ29vZCBvZiBhIHByZWRpY3RpdmUgbW9kZWwgb3VycyBpcy4gV2UgZG8gdGhpcyB0aHJvdWdoIGENCnZhcmlldHkgb2YgbWVhbnM6DQoNCi0gICBPdGhlciAqKnN0YXRpc3RpY3MqKiwgc3VjaCBhcyBzZW5zaXRpdml0eSBhbmQgc3BlY2lmaWNpdHkNCg0KLSAgICoqVGFibGVzKiotLXBhcnRpY3VsYXJseSwgYSBjb25mdXNpb24gbWF0cml4DQoNCk91ciBkcml2aW5nIHF1ZXN0aW9uIGZvciB0aGlzIGNhc2Ugc3R1ZHksIHRoZW4sIGlzOiAqSG93IGdvb2QgaXMgb3VyDQptb2RlbCBhdCBtYWtpbmcgcHJlZGljdGlvbnM/Kg0KDQpXZSdsbCB1c2UgdGhlIFtPcGVuIFVuaXZlcnNpdHkgTGVhcm5pbmcgQW5hbHl0aWNzIERhdGFzZXQNCihPVUxBRCldKGh0dHBzOi8vYW5hbHlzZS5rbWkub3Blbi5hYy51ay9vcGVuX2RhdGFzZXQpIGFnYWluLS10aGlzIHRpbWUsDQphZGRpbmcgYW5vdGhlciBkYXRhIHNvdXJjZSwgb25lIG9uIHN0dWRlbnRzJyBwZXJmb3JtYW5jZSBvbiBhc3Nlc3NtZW50cy4NCg0KTGlrZSBpbiB0aGUgZmlyc3QgbGVhcm5pbmcgbGFiLCB3ZSdsbCBmaXJzdCBsb2FkIHNldmVyYWwgcGFja2FnZXMgLS0NCnt0aWR5dmVyc2V9LCB7dGlkeW1vZGVsc30sIGFuZCB7amFuaXRvcn0uIE1ha2Ugc3VyZSB0aGVzZSBhcmUgaW5zdGFsbGVkDQoodmlhIGBpbnN0YWxsLnBhY2thZ2VzKClgKS4gTm90ZSB0aGF0IGlmIHRoZXkncmUgYWxyZWFkeSBpbnN0YWxsZWQsIHlvdQ0KZG9uJ3QgbmVlZCB0byBpbnN0YWxsIHRoZW0gYWdhaW4uDQoNCmBgYHtyfQ0KbGlicmFyeSh0aWR5dmVyc2UpDQpsaWJyYXJ5KHRpZHltb2RlbHMpDQpsaWJyYXJ5KGphbml0b3IpDQpsaWJyYXJ5KHlhcmRzdGljaykNCmxpYnJhcnkoZHBseXIpDQpgYGANCg0KIyMgMi4gV1JBTkdMRQ0KDQpMaWtlIGluIHRoZSBjb2RlLWFsb25nIGZvciB0aGUgb3ZlcnZpZXcgcHJlc2VudGF0aW9uLCBsZXQncyB0YWtlIGEgbG9vaw0KYXQgdGhlIGRhdGEgYW5kIGRvIHNvbWUgcHJvY2Vzc2luZyBvZiBpdC4NCg0KIyMjIyBbWW91ciBUdXJuXXtzdHlsZT0iY29sb3I6IGdyZWVuOyJ9IOKktQ0KDQpXZSdsbCBsb2FkIHRoZSBzdHVkZW50cyBmaWxlIHRvZ2V0aGVyOyB5b3UnbGwgd3JpdGUgY29kZSB0byByZWFkIHRoZQ0KYXNzZXNzbWVudHMgZmlsZSwgd2hpY2ggaXMgbmFtZWQgIm91bGFkLWFzc2Vzc21lbnRzLmNzdiIuIFBsZWFzZSBhc3NpZ24NCnRoZSBuYW1lIGBhc3Nlc3NtZW50c2AgdG8gdGhlIGxvYWRlZCBhc3Nlc3NtZW50cyBmaWxlLg0KDQpgYGB7cn0NCmFzc2Vzc21lbnRzIDwtIHJlYWQuY3N2KCJkYXRhL291bGFkLWFzc2Vzc21lbnRzLmNzdiIpDQpgYGANCg0KIyMgMy4gRVhQTE9SRQ0KDQojIyMjIFtZb3VyIFR1cm5de3N0eWxlPSJjb2xvcjogZ3JlZW47In0g4qS1DQoNCkluIHRoZSBsYXN0IGxhYiwgd2UgdXNlZCB0aGUgYGNvdW50KClgIGZ1bmN0aW9uLiBMZXQncyBwcmFjdGljZQ0KdGhhdCBhZ2FpbiwgYnkgY291bnRpbmcgdGhlIG51bWJlciBvZiBvZiBhc3Nlc3NtZW50cyBvZiBkaWZmZXJlbnQgdHlwZXMuDQpJZiB5b3UgbmVlZCwgcmVjYWxsIHRoYXQgdGhlIGRhdGEgZGljdGlvbmFyeSBpcw0KW2hlcmVdKGh0dHBzOi8vYW5hbHlzZS5rbWkub3Blbi5hYy51ay9vcGVuX2RhdGFzZXQpLiBOb3RlIHdoYXQgdGhlDQpkaWZmZXJlbnQgdHlwZXMgb2YgYXNzZXNzbWVudHMgbWVhbi4NCg0KYGBge3J9DQojI3ZpZXcoYXNzZXNzbWVudHMpDQoNCmFzc2Vzc21lbnRzICU+JSANCiAgICBjb3VudChhc3Nlc3NtZW50X3R5cGUpICMgbnVtYmVyIG9mIGVhY2ggYXNzZXNzbWVudCB0eXBlDQoNCg0KYGBgDQoNCi0gICBUaHJlZSB0eXBlcyBvZiBhc3Nlc3NtZW50cyBleGlzdDogVHV0b3IgTWFya2VkIEFzc2Vzc21lbnQgKFRNQSksIENvbXB1dGVyIE1hcmtlZCBBc3Nlc3NtZW50IChDTUEpIGFuZCBGaW5hbCBFeGFtIChFeGFtKS4NCg0KIyMjIyBbWW91ciBUdXJuXXtzdHlsZT0iY29sb3I6IGdyZWVuOyJ9IOKktQ0KDQpXZSdsbCBub3cgdXNlIGFub3RoZXIgZnVuY3Rpb24tLWxpa2UgYGNvdW50KClgLCBmcm9tIHRoZSB7dGlkeXZlcnNlfS4NClNwZWNpZmljYWxseSwgd2UnbGwgdXNlIHRoZSBgZGlzdGluY3QoKWAgZnVuY3Rpb24uIFRoaXMgcmV0dXJucyB0aGUNCnVuaXF1ZSAob3IgZGlzdGluY3QpIHZhbHVlcyBmb3IgYSBzcGVjaWZpZWQgdmFyaWFibGUuIExlYXJuIG1vcmUgYWJvdXQNCmBkaXN0aW5jdCgpYA0KW2hlcmVdKGh0dHBzOi8vZHBseXIudGlkeXZlcnNlLm9yZy9yZWZlcmVuY2UvZGlzdGluY3QuaHRtbCkuIEJlbG93LCBmaW5kDQp0aGUgZGlzdGluY3QgYXNzZXNzbWVudCBJRHMuDQoNCmBgYHtyfQ0KYXNzZXNzbWVudHMgJT4lDQogIGRpc3RpbmN0KGlkX2Fzc2Vzc21lbnQpDQpgYGANCg0KTGV0J3MgZXhwbG9yZSB0aGUgYXNzZXNzbWVudHMgZGF0YSBhIGJpdC4NCg0KV2UgbWlnaHQgYmUgaW50ZXJlc3RlZCBpbiBob3cgbWFueSBhc3Nlc3NtZW50cyB0aGVyZSBhcmUgcGVyIGNvdXJzZS4gVG8NCmNhbGN1bGF0ZSB0aGF0LCB3ZSBuZWVkIHRvIGBjb3VudCgpYCBzZXZlcmFsIHZhcmlhYmxlcyBhdCBvbmNlOyB3aGVuDQpkb2luZyB0aGlzLCBgY291bnQoKWAgdGFidWxhdGVzIHRoZSBudW1iZXIgb2YgdW5pcXVlIGNvbWJpbmF0aW9ucyBvZiB0aGUNCnZhcmlhYmxlcyBwYXNzZWQgdG8gaXQuDQoNCmBgYHtyfQ0KYXNzZXNzbWVudHMgJT4lIA0KICAgIGNvdW50KGFzc2Vzc21lbnRfdHlwZSwgY29kZV9tb2R1bGUsIGNvZGVfcHJlc2VudGF0aW9uKQ0KYGBgDQoNCkxldCdzIGV4cGxvcmUgdGhlIGRhdGVzIGFzc2lnbm1lbnRzIHdlcmUgc3VibWl0dGVkIGEgYml0IC0tIHVzaW5nIHRoZQ0KYHN1bW1hcml6ZSgpYCBmdW5jdGlvbjoNCg0KYGBge3J9DQphc3Nlc3NtZW50cyAlPiUgDQogICAgc3VtbWFyaXplKG1lYW5fZGF0ZSA9IG1lYW4oZGF0ZSwgbmEucm0gPSBUUlVFKSwgIyBmaW5kIHRoZSBtZWFuIGRhdGUgZm9yIGFzc2lnbm1lbnRzDQogICAgICAgICAgICAgIG1lZGlhbl9kYXRlID0gbWVkaWFuKGRhdGUsIG5hLnJtID0gVFJVRSksICMgZmluZCB0aGUgbWVkaWFuDQogICAgICAgICAgICAgIHNkX2RhdGUgPSBzZChkYXRlLCBuYS5ybSA9IFRSVUUpLCAjIGZpbmQgdGhlIHNkDQogICAgICAgICAgICAgIG1pbl9kYXRlID0gbWluKGRhdGUsIG5hLnJtID0gVFJVRSksICMgZmluZCB0aGUgbWluDQogICAgICAgICAgICAgIG1heF9kYXRlID0gbWF4KGRhdGUsIG5hLnJtID0gVFJVRSkpICMgZmluZCB0aGUgbWFkDQpgYGANCg0KV2hhdCBjYW4gd2UgdGFrZSBmcm9tIHRoaXM/IEl0IGxvb2tzIGxpa2UsIG9uIGF2ZXJhZ2UsIHRoZSBhdmVyYWdlIChtZWFuDQphbmQgbWVkaWFuKSBkYXRlIGFzc2lnbm1lbnRzIHdlcmUgZHVlIHdhcyBhcm91bmQgMTMwIC0tIDEzMCBkYXlzIGFmdGVyDQp0aGUgc3RhcnQgb2YgdGhlIGNvdXJzZS4gVGhlIGZpcnN0IGFzc2lnbm1lbnQgc2VlbXMgdG8gaGF2ZSBiZWVuIGR1ZSAxMg0KZGF5cyBpbnRvIHRoZSBjb3Vyc2UsIGFuZCB0aGUgbGFzdCAyNjEgZGF5cyBhZnRlci4NCg0KQ3J1Y2lhbGx5LCB0aG91Z2gsIHRoZXNlIGRhdGVzIHZhcnkgYnkgY291cnNlLiBUaHVzLCB3ZSBuZWVkIHRvIGZpcnN0DQpncm91cCB0aGUgZGF0YSBieSBjb3Vyc2UuIExldCdzIHVzZSBhIGRpZmZlcmVudCBmdW5jdGlvbiB0aGlzIHRpbWUgLS0NCmBxdWFudGlsZSgpYCwgYW5kIGNhbGN1bGF0ZSB0aGUgZmlyc3QgcXVhbnRpbGUgdmFsdWUuIE91ciByZWFzb25pbmcgaXMNCnRoYXQgd2Ugd2FudCB0byBiZSBhYmxlIHRvIGFjdCB0byBzdXBwb3J0IHN0dWRlbnRzLCBhbmQgaWYgd2Ugd2FpdCB1bnRpbA0KYWZ0ZXIgdGhlIGF2ZXJhZ2UgYXNzaWdubWVudCBpcyBkdWUsIHRoZW4gdGhhdCBtaWdodCBiZSB0b28gbGF0ZS4NCldoZXJlYXMgdGhlIGZpcnN0IHF1YW50aWxlIGNvbWVzIGFwcHJveGltYXRlbHkgb25lLXF1YXJ0ZXIgdGhyb3VnaCB0aGUNCnNlbWVzdGVyIC0tLSB3aXRoLCB0aGVyZWZvcmUsIG1vcmUgdGltZSB0byBpbnRlcnZlbmUuDQoNCmBgYHtyfQ0KYXNzZXNzbWVudHMgJT4lIA0KICAgIGdyb3VwX2J5KGNvZGVfbW9kdWxlLCBjb2RlX3ByZXNlbnRhdGlvbikgJT4lICMgZmlyc3QsIGdyb3VwIGJ5IGNvdXJzZSAobW9kdWxlOiBjb3Vyc2U7IHByZXNlbnRhdGlvbjogc2VtZXN0ZXIpDQogICAgc3VtbWFyaXplKG1lYW5fZGF0ZSA9IG1lYW4oZGF0ZSwgbmEucm0gPSBUUlVFKSwNCiAgICAgICAgICAgICAgbWVkaWFuX2RhdGUgPSBtZWRpYW4oZGF0ZSwgbmEucm0gPSBUUlVFKSwNCiAgICAgICAgICAgICAgc2RfZGF0ZSA9IHNkKGRhdGUsIG5hLnJtID0gVFJVRSksDQogICAgICAgICAgICAgIG1pbl9kYXRlID0gbWluKGRhdGUsIG5hLnJtID0gVFJVRSksDQogICAgICAgICAgICAgIG1heF9kYXRlID0gbWF4KGRhdGUsIG5hLnJtID0gVFJVRSksDQogICAgICAgICAgICAgIGZpcnN0X3F1YW50aWxlID0gcXVhbnRpbGUoZGF0ZSwgcHJvYnMgPSAuMjUsIG5hLnJtID0gVFJVRSkpICMgZmluZCB0aGUgZmlyc3QgKDI1JSkgcXVhbnRpbGUNCmBgYA0KDQpBbHJpZ2h0LCB0aGlzIGlzIGEgYml0IGNvbXBsaWNhdGVkLCBidXQgd2UgY2FuIGFjdHVhbGx5IHdvcmsgd2l0aCB0aGlzDQpkYXRhLiBMZXQncyB1c2UganVzdCBhIHBvcnRpb24gb2YgdGhlIGFib3ZlIGNvZGUsIGFzc2lnbmluZyBpdCB0aGUgbmFtZQ0KYGNvZGVfbW9kdWxlX2RhdGVzYC4NCg0KYGBge3J9DQpjb2RlX21vZHVsZV9kYXRlcyA8LSBhc3Nlc3NtZW50cyAlPiUgDQogICAgZ3JvdXBfYnkoY29kZV9tb2R1bGUsIGNvZGVfcHJlc2VudGF0aW9uKSAlPiUgDQogICAgc3VtbWFyaXplKHF1YW50aWxlX2N1dG9mZl9kYXRlID0gcXVhbnRpbGUoZGF0ZSwgcHJvYnMgPSAuMjUsIG5hLnJtID0gVFJVRSkpDQpgYGANCg0KIyMjIyBbWW91ciBUdXJuXXtzdHlsZT0iY29sb3I6IGdyZWVuOyJ9IOKktQ0KDQpMZXQncyB0YWtlIGEgbG9vayBhdCB3aGF0IHdlIGp1c3QgY3JlYXRlZDsgdHlwZSBgY29kZV9tb2R1bGVfZGF0ZXNgDQpiZWxvdzoNCg0KYGBge3J9DQpjb2RlX21vZHVsZV9kYXRlcw0KYGBgDQoNCldoYXQgaGF2ZSB3ZSBjcmVhdGVkPyBXZSBmb3VuZCB0aGUgZGF0ZSB0aGF0IGlzIG9uZS1xdWFydGVyIG9mIHRoZSB3YXkNCnRocm91Z2ggdGhlIHNlbWVzdGVyIChpbiB0ZXJtcyBvZiB0aGUgZGF0ZXMgYXNzaWdubWVudHMgYXJlIGR1ZSkuDQoNCiMjIyMgW1lvdXIgVHVybl17c3R5bGU9ImNvbG9yOiBncmVlbjsifSDipLUNCg0KV2UgY2FuIHRodXMgdXNlIHRoaXMgdG8gZ3JvdXAgYW5kIGNhbGN1bGF0ZSBzdHVkZW50cycgcGVyZm9ybWFuY2Ugb24NCmFzc2lnbm1lbnRzIHRocm91Z2ggdGhpcyBwb2ludC4gVG8gZG8gdGhpcywgd2UgbmVlZCB0byB1c2UgYSBqb2luDQpmdW5jdGlvbiAtLS0gYGxlZnRfam9pbigpYCwgaW4gcGFydGljdWxhci4gUGxlYXNlIHVzZSBgbGVmdF9qb2luKClgIHRvDQpqb2luIHRvZ2V0aGVyIGBhc3Nlc3NtZW50c2AgYW5kIGBjb2RlX21vZHVsZV9kYXRlc2AsIHdpdGggYGFzc2Vzc21lbnRzYA0KYmVpbmcgdGhlICJsZWZ0IiBkYXRhIGZyYW1lLCBhbmQgYGNvZGVfbW9kdWxlX2RhdGVzYCB0aGUgcmlnaHQuIFNhdmUgdGhlDQpvdXRwdXQgb2YgdGhlIGpvaW4gdGhlIG5hbWUgYGFzc2Vzc21lbnRzX2pvaW5lZGAuDQoNCmBgYHtyfQ0KIyN2aWV3KGNvZGVfbW9kdWxlX2RhdGVzKQ0KDQphc3Nlc3NtZW50c19qb2luZWQgPC0gbWVyZ2UoeCA9IGFzc2Vzc21lbnRzLCB5ID0gY29kZV9tb2R1bGVfZGF0ZXMsYnk9YygiY29kZV9tb2R1bGUiLCJjb2RlX3ByZXNlbnRhdGlvbiIpLGFsbC54PVRSVUUpDQoNCiMjdmlldyhhc3Nlc3NtZW50c19qb2luZWQpDQpgYGANCg0KV2UncmUgYWxtb3N0IHRoZXJlISBUaGUgbmV4dCBmZXcgbGluZXMgZmlsdGVyIHRoZSBhc3Nlc3NtZW50cyBkYXRhIHNvIGl0DQpvbmx5IGluY2x1ZGVzIGFzc2Vzc21lbnRzIGJlZm9yZSBvdXIgY3V0b2ZmIGRhdGUuDQoNCmBgYHtyfQ0KYXNzZXNzbWVudHNfZmlsdGVyZWQgPC0gYXNzZXNzbWVudHNfam9pbmVkICU+JSANCiAgICBmaWx0ZXIoZGF0ZSA8IHF1YW50aWxlX2N1dG9mZl9kYXRlKSAjIGZpbHRlciB0aGUgZGF0YSBzbyBvbmx5IGFzc2lnbm1lbnRzIGJlZm9yZSB0aGUgY3V0b2ZmIGRhdGUgYXJlIGluY2x1ZGVkDQoNCiMjdmlldyhhc3Nlc3NtZW50c19maWx0ZXJlZCkNCmBgYA0KDQpGaW5hbGx5LCB3ZSdsbCBmaW5kIHRoZSBhdmVyYWdlIHNjb3JlIGZvciBlYWNoIHN0dWRlbnQuDQoNCmBgYHtyfQ0KYXNzZXNzbWVudHNfc3VtbWFyaXplZCA8LSBhc3Nlc3NtZW50c19maWx0ZXJlZCAlPiUgDQogICAgbXV0YXRlKHdlaWdodGVkX3Njb3JlID0gc2NvcmUgKiB3ZWlnaHQpICU+JSAjIGNyZWF0ZSBhIG5ldyB2YXJpYWJsZSB0aGF0IGFjY291bnRzIGZvciB0aGUgIndlaWdodCIgKGNvbXBhcmFibGUgdG8gcG9pbnRzKSBnaXZlbiBlYWNoIGFzc2lnbm1lbnQNCiAgICBncm91cF9ieShpZF9zdHVkZW50KSAlPiUgDQogICAgc3VtbWFyaXplKG1lYW5fd2VpZ2h0ZWRfc2NvcmUgPSBtZWFuKHdlaWdodGVkX3Njb3JlKSkgDQoNCiMjdmlldyhhc3Nlc3NtZW50c19zdW1tYXJpemVkKQ0KYGBgDQoNCkFzIGEgcG9pbnQgb2YgcmVmbGVjdGlvbiBoZXJlLCBub3RlIGhvdyBtdWNoIHdvcmsgd2UndmUgZG9uZSBiZWZvcmUNCndlJ3ZlIGdvdHRlbiB0byB0aGUgbWFjaGluZSBsZWFybmluZyBzdGVwcy4gVGhvdWdoIGEgY2hhbGxlbmdlLCB0aGlzIGlzDQp0eXBpY2FsIGZvciBib3RoIG1hY2hpbmUgbGVhcm5pbmcgYW5kIG1vcmUgdHJhZGl0aW9uYWwgc3RhdGlzdGljYWwNCm1vZGVsczogYSBsb3Qgb2YgdGhlIHdvcmsgaXMgaW4gdGhlIHByZXBhcmF0aW9uIGFuZCBkYXRhIHdyYW5nbGluZw0Kc3RhZ2VzLg0KDQpMZXQncyBjb3B5IHRoZSBjb2RlIGJlbG93IHRoYXQgd2UgdXNlZCB0byBwcm9jZXNzIHRoZSBzdHVkZW50cyBkYXRhDQood2hlbiBwcm9jZXNzaW5nIHRoZSBgcGFzc2AgYW5kIGBpbWRfYmFuZGAgdmFyaWFibGVzKS4NCg0KYGBge3J9DQpzdHVkZW50cyA8LSByZWFkX2NzdigiZGF0YS9vdWxhZC1zdHVkZW50cy5jc3YiKQ0KDQpzdHVkZW50cyA8LSBzdHVkZW50cyAlPiUgDQogICAgbXV0YXRlKHBhc3MgPSBpZmVsc2UoZmluYWxfcmVzdWx0ID09ICJQYXNzIiwgMSwgMCkpICU+JSAjIGNyZWF0ZXMgYSBkdW1teSBjb2RlDQogICAgbXV0YXRlKHBhc3MgPSBhcy5mYWN0b3IocGFzcykpICMgbWFrZXMgdGhlIHZhcmlhYmxlIGEgZmFjdG9yLCBoZWxwaW5nIGxhdGVyIHN0ZXBzDQoNCnN0dWRlbnRzIDwtIHN0dWRlbnRzICU+JSANCiAgICBtdXRhdGUoaW1kX2JhbmQgPSBmYWN0b3IoaW1kX2JhbmQsIGxldmVscyA9IGMoIjAtMTAlIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIjEwLTIwJSIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICIyMC0zMCUiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiMzAtNDAlIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIjQwLTUwJSIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICI1MC02MCUiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiNjAtNzAlIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIjcwLTgwJSIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICI4MC05MCUiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiOTAtMTAwJSIpKSkgJT4lICMgdGhpcyBjcmVhdGVzIGEgZmFjdG9yIHdpdGggb3JkZXJlZCBsZXZlbHMNCiAgICBtdXRhdGUoaW1kX2JhbmQgPSBhcy5pbnRlZ2VyKGltZF9iYW5kKSkgIyB0aGlzIGNoYW5nZXMgdGhlIGxldmVscyBpbnRvIGludGVnZXJzIGJhc2VkIG9uIHRoZSBvcmRlciBvZiB0aGUgZmFjdG9yIGxldmVscw0KYGBgDQoNCiMjIyMgW1lvdXIgVHVybl17c3R5bGU9ImNvbG9yOiBncmVlbjsifSDipLUNCg0KRmluYWxseSwgbGV0J3Mgam9pbiB0b2dldGhlciBgc3R1ZGVudHNgIGFuZCBgYXNzZXNzbWVudHNfc3VtbWFyaXplZGAsDQphc3NpZ25pbmcgdGhlIGpvaW5lZCB0aGUgbmFtZSBgc3R1ZGVudHNfYW5kX2Fzc2Vzc21lbnRzYC4NCg0KYGBge3J9DQojI3ZpZXcoc3R1ZGVudHMpDQojI3ZpZXcoYXNzZXNzbWVudHNfc3VtbWFyaXplZCkNCg0KIyMgdGhpcyBpcyBhbiBpbm5lciBqb2luLCB1c2luZyBsZWZ0IGpvaW4gdGhlcmUgd2VyZSBzdHVkZW50X2lkJ3MgdGhhdCB3ZXJlIG5vdCBpbiB0aGUgYXNzZXNzbWVudHNfc3VtbWFyaXplZCB0YWJsZQ0Kc3R1ZGVudHNfYW5kX2Fzc2Vzc21lbnRzIDwtIG1lcmdlKHggPSBzdHVkZW50cywgeSA9IGFzc2Vzc21lbnRzX3N1bW1hcml6ZWQsIGJ5PSJpZF9zdHVkZW50IikNCg0KIyN2aWV3KHN0dWRlbnRzX2FuZF9hc3Nlc3NtZW50cykNCg0KYGBgDQoNCkJpZyBwaWN0dXJlLCB3ZSd2ZSBhZGRlZCBhbm90aGVyIG1lYXN1cmUgLS0gYW5vdGhlciBmZWF0dXJlIC0tIHRoYXQgd2UNCmNhbiB1c2UgdG8gbWFrZSBwcmVkaWN0aW9uczogc3R1ZGVudHMnIHBlcmZvcm1hbmNlIG9uIGFzc2Vzc21lbnRzIDEvNCBvZg0KdGhlIHdheSB0aHJvdWdoIHRoZSBjb3Vyc2UuDQoNCldlJ3JlIG5vdyByZWFkeSB0byBwcm9jZWVkIHRvIG91ciBtYWNoaW5lIGxlYXJuaW5nIHN0ZXBzIQ0KDQpUaGUgcHJvYmxlbSB3ZSB3aWxsIGJlIHdvcmtpbmcgb24gLSBwcmVkaWN0aW5nIHN0dWRlbnRzIHdobyBwYXNzLCBiYXNlZA0Kb24gZGF0YSBmcm9tIHRoZSBmaXJzdCBvbmUtdGhpcmQgb2YgdGhlIHNlbWVzdGVyIC0gaGFzIGFuIGFuYWxvZyBpbiBhDQpyZWNlbnQgcGFwZXIgYnkgUnlhbiBCYWtlciBhbmQgY29sbGVhZ3VlczoNCg0KSW4gW0Jha2VyIGV0IGFsLg0KKDIwMjApXShodHRwczovL3d3dy50YW5kZm9ubGluZS5jb20vZG9pL2Z1bGwvMTAuMTA4MC8xMDgyNDY2OS4yMDE5LjE2NzAwNjUpLA0KdGhlIGF1dGhvcnMgY3JlYXRlIGEgcHJlY2lzaW9uLXJlY2FsbCAoYWxzbyBrbm93biBhcyBzZW5zaXRpdml0eSkNCmdyYXBoIC0gb25lIHRoYXQgZGVtb25zdHJhdGVzIHRoZSB0cmFkZS1vZmYgYmV0d2VlbiBvcHRpbWl6aW5nIHRoZXNlIHR3bw0Kc3RhdGlzdGljcy4gUmV2aWV3IHRoZWlyIHBhcGVyIC0gZXNwZWNpYWxseSB0aGUgcmVzdWx0cyBzZWN0aW9uIC0gdG8gc2VlDQpob3cgdGhleSBkaXNjdXNzIHRoZXNlIHR3byBzdGF0aXN0aWNzLg0KDQo+IEJha2VyLCBSLiBTLiwgQmVybmluZywgQS4gVy4sIEdvd2RhLCBTLiBNLiwgWmhhbmcsIFMuLCAmIEhhd24sIEEuDQo+ICgyMDIwKS4gUHJlZGljdGluZyBLLTEyIGRyb3BvdXQuICpKb3VybmFsIG9mIEVkdWNhdGlvbiBmb3IgU3R1ZGVudHMNCj4gUGxhY2VkIGF0IFJpc2ssIDI1KigxKSwgMjgtNTQuDQoNClBsZWFzZSByZXZpZXcgdGhpcyBwYXBlciBiZWZvcmUgcHJvY2VlZGluZywgZm9jdXNpbmcgb24gaG93IHRoZXkNCmRlc2NyaWJlDQoNCiMjIDQuIE1PREVMDQoNCiMjIyBTdGVwIDEuIFNwbGl0IGRhdGENCg0KVGhpcyBpcyBpZGVudGljYWwgdG8gd2hhdCB3ZSBkaWQgaW4gdGhlIGZpcnN0IGxlYXJuaW5nIGxhYiwgdXNpbmcgdGhlDQpgc3R1ZGVudHNfYW5kX2Fzc2Vzc21lbnRzYCBkYXRhLiBXZSdsbCBhbHNvIGNyZWF0ZSBhIHRlc3RpbmcgZGF0YSBzZXQNCndlJ2xsIHVzZSBsYXRlci4NCg0KYGBge3J9DQpzZXQuc2VlZCgyMDIzMDcxMikNCg0Kc3R1ZGVudHNfYW5kX2Fzc2Vzc21lbnRzIDwtIHN0dWRlbnRzX2FuZF9hc3Nlc3NtZW50cyAlPiUgDQogICAgZHJvcF9uYShtZWFuX3dlaWdodGVkX3Njb3JlKQ0KDQp0cmFpbl90ZXN0X3NwbGl0IDwtIGluaXRpYWxfc3BsaXQoc3R1ZGVudHNfYW5kX2Fzc2Vzc21lbnRzLCBwcm9wID0gLjUwLCBzdHJhdGEgPSAicGFzcyIpDQpkYXRhX3RyYWluIDwtIHRyYWluaW5nKHRyYWluX3Rlc3Rfc3BsaXQpDQpkYXRhX3Rlc3QgPC0gdGVzdGluZyh0cmFpbl90ZXN0X3NwbGl0KQ0KYGBgDQoNCiMjIyBTdGVwIDI6IEVuZ2luZWVyIGZlYXR1cmVzIGFuZCB3cml0ZSBkb3duIHRoZSByZWNpcGUNCg0KV2UnbGwgYWxzbyBzcGVjaWZ5IHRoZSByZWNpcGUsIGFkZGluZyBvdXIgYG1lYW5fd2VpZ2h0ZWRfc2NvcmVgIHZhcmlhYmxlDQp3ZSBjYWxjdWxhdGVkIGFib3ZlIGFzIHdlbGwgYXMgdmFyaWFibGVzIHdlIHVzZWQgaW4gTEwxIChjYXNlIHN0dWR5IGFuZA0KYmFkZ2UpLiBOb3RlIGhvdyB3ZSBkdW1teSBjb2RlIHR3byB2YXJpYWJsZXMgdXNpbmcgYHN0ZXBfZHVtbXkoKWANCihkZXNjcmliZWQgZnVydGhlciBpbiB0aGUgZmlyc3QgbGVhcm5pbmcgbGFiKS4NCg0KYGBge3J9DQpteV9yZWMgPC0gcmVjaXBlKHBhc3MgfiBkaXNhYmlsaXR5ICsNCiAgICAgICAgICAgICAgICAgICAgIGRhdGVfcmVnaXN0cmF0aW9uICsgDQogICAgICAgICAgICAgICAgICAgICBnZW5kZXIgKw0KICAgICAgICAgICAgICAgICAgICAgY29kZV9tb2R1bGUgKw0KICAgICAgICAgICAgICAgICAgICAgbWVhbl93ZWlnaHRlZF9zY29yZSwgDQogICAgICAgICAgICAgICAgIGRhdGEgPSBkYXRhX3RyYWluKSAlPiUgDQogICAgc3RlcF9kdW1teShkaXNhYmlsaXR5KSAlPiUgDQogICAgc3RlcF9kdW1teShnZW5kZXIpICU+JSAgDQogICAgc3RlcF9kdW1teShjb2RlX21vZHVsZSkNCg0KYGBgDQoNCiMjIyBTdGVwIDM6IFNwZWNpZnkgdGhlIG1vZGVsIGFuZCB3b3JrZmxvdw0KDQpUaGVzZSBzdGVwcyBhcmUgYWxzbyB0aGUgc2FtZSBhcyBpbiBMTDEuDQoNCmBgYHtyfQ0KIyBzcGVjaWZ5IG1vZGVsDQpteV9tb2QgPC0NCiAgICBsb2dpc3RpY19yZWcoKSAlPiUgDQogICAgc2V0X2VuZ2luZSgiZ2xtIikgJT4lICMgZ2VuZXJhbGl6ZWQgbGluZWFyIG1vZGVsDQogICAgc2V0X21vZGUoImNsYXNzaWZpY2F0aW9uIikgIyBzaW5jZSB3ZSBhcmUgcHJlZGljdGluZyBhIGRpY2hvdG9tb3VzIG91dGNvbWUsIHNwZWNpZnkgY2xhc3NpZmljYXRpb247IGZvciBhIG51bWJlciwgc3BlY2lmeSByZWdyZXNzaW9uDQoNCiMgc3BlY2lmeSB3b3JrZmxvdw0KbXlfd2YgPC0NCiAgICB3b3JrZmxvdygpICU+JSAjIGNyZWF0ZSBhIHdvcmtmbG93DQogICAgYWRkX21vZGVsKG15X21vZCkgJT4lICMgYWRkIHRoZSBtb2RlbCB3ZSB3cm90ZSBhYm92ZQ0KICAgIGFkZF9yZWNpcGUobXlfcmVjKSAjIGFkZCBvdXIgcmVjaXBlIHdlIHdyb3RlIGFib3ZlDQpgYGANCg0KIyMjIFN0ZXAgNDogRml0IG1vZGVsDQoNCkZpbmFsbHksIHdlJ2xsIGZpdCBvdXIgbW9kZWwuDQoNCmBgYHtyfQ0KZml0dGVkX21vZGVsIDwtIGZpdChteV93ZiwgZGF0YSA9IGRhdGFfdHJhaW4pDQoNCmBgYA0KDQpGaW5hbGx5LCB3ZSdsbCB1c2UgdGhlIGBsYXN0X2ZpdGAgZnVuY3Rpb24sIGJ1dCB3ZSdsbCBhZGQgc29tZXRoaW5nIGENCmxpdHRsZSBkaWZmZXJlbnQgLSB3ZSdsbCBhZGQgdGhlIG1ldHJpYyBzZXQgaGVyZS4gVG8gdGhlIGFib3ZlLCB3ZSdsbA0KYWRkIGFub3RoZXIgY29tbW9uIG1ldHJpYyAtIENvaGVuJ3MgS2FwcGEsIHdoaWNoIGlzIHNpbWlsYXIgdG8gYWNjdXJhY3ksDQpidXQgd2hpY2ggYWNjb3VudHMgZm9yIGNoYW5jZSBhZ3JlZW1lbnQuDQoNClRvIGRvIHNvLCB3ZSBoYXZlIHRvIHNwZWNpZnkgKndoaWNoKiBtZXRyaWNzIHdlIHdhbnQgdG8gdXNlIHVzaW5nIHRoZQ0KYG1ldHJpY19zZXQoKWAgZnVuY3Rpb24gKHNlZSBhbGwgb2YgdGhlIGF2YWlsYWJsZSBvcHRpb25zDQpbaGVyZV0oaHR0cHM6Ly95YXJkc3RpY2sudGlkeW1vZGVscy5vcmcvYXJ0aWNsZXMvbWV0cmljLXR5cGVzLmh0bWwpKS4NClBsZWFzZSBzcGVjaWZ5IHNpeCBtZXRyaWNzIGluIHRoZSBtZXRyaWMgc2V0IC0tIGFjY3VyYWN5LCBzZW5zaXRpdml0eSwNCnNwZWNpZmljaXR5LCBwcHYgKHJlY2FsbCksIG5wdiwgYW5kIGthcHBhLiBBc3NpZ24gdGhlIG5hbWUNCmBjbGFzc19tZXRyaWNzYCB0byB0aGUgb3V0cHV0IG9mIHlvdXIgdXNlIG9mIHRoZSBgbWV0cmljX3NldCgpYA0KZnVuY3Rpb24uDQoNCiMjIyMgW1lvdXIgVHVybl17c3R5bGU9ImNvbG9yOiBncmVlbjsifSDipLUNCg0KYGBge3J9DQoNCmNsYXNzX21ldHJpY3MgPC0gbWV0cmljX3NldCh5YXJkc3RpY2s6OmFjY3VyYWN5LCBzZW5zaXRpdml0eSwgc3BlY2lmaWNpdHksIHBwdiwgbnB2LCBrYXApDQoNCmBgYA0KDQpUaGVuLCBwbGVhc2UgdXNlIGBsYXN0X2ZpdGAsIGxvb2tpbmcgdG8gaG93IHdlIGRpZCB0aGlzIGluIHRoZSBsYXN0DQpsZWFybmluZyBsYWIgZm9yIGEgZ3VpZGUgb24gaG93IHRvIHNwZWNpZnkgdGhlIGFyZ3VtZW50IG50cy4gVG8gdGhlDQphcmd1bWVudHMsIGFkZCBgbWV0cmljcyA9IGNsYXNzX21ldHJpY3NgLg0KDQpgYGB7cn0NCmZpbmFsX2ZpdCA8LSBsYXN0X2ZpdChteV93ZiwgdHJhaW5fdGVzdF9zcGxpdCwgbWV0cmljcyA9IGNsYXNzX21ldHJpY3MpDQoNCmBgYA0KDQpXZSdyZSBub3cgcmVhZHkgdG8gbW92ZSBvbiB0byBpbnRlcnByZXRpbmcgdGhlIGFjY3VyYWN5IG9mIG91ciBtb2RlbC4NCg0KIyMjIFN0ZXAgNTogSW50ZXJwcmV0IGFjY3VyYWN5DQoNCiMjIyBBIGNvbmZ1c2lvbiBtYXRyaXggYW5kIHRydWUgYW5kIGZhbHNlIHBvc2l0aXZlcyBhbmQgbmVnYXRpdmVzDQoNCkxldCdzIHN0YXJ0IHdpdGggYSBzaW1wbGUgY29uZnVzaW9uIG1hdHJpeC4gVGhlIGNvbmZ1c2lvbiBtYXRyaXggaXMgYSAyDQp4IDIgdGFibGUgd2l0aCB2YWx1ZXMgKGNlbGxzIGluIHRoZSB0YWJsZSkgcmVwcmVzZW50aW5nIG9uZSBvZiBmb3VyDQpjb25kaXRpb25zLCBlbGFib3JhdGVkIGJlbG93LiBZb3UnbGwgZmlsbCBpbiB0aGUgbGFzdCB0d28gY29sdW1ucyBpbiBhDQpmZXcgbW9tZW50cy4NCg0KfCAgICAgIFN0YXRpc3RpYyAgICAgIHwgICAgICAgSG93IHRvIEZpbmQgICAgICAgfCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEludGVycHJldGF0aW9uICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfCBWYWx1ZSB8IFwlICB8DQp8Oi0tLS0tLS0tLS0tLS0tLS0tLS06fDotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLTp8Oi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLTp8Oi0tLS0tOnw6LS0tOnwNCnwgVHJ1ZSBwb3NpdGl2ZSAoVFApICB8IFRydXRoOiAxLCBQcmVkaWN0aW9uOiAxIHwgICAgUHJvcG9ydGlvbiBvZiB0aGUgcG9wdWxhdGlvbiB0aGF0IGlzIGFmZmVjdGVkIGJ5IGEgY29uZGl0aW9uIGFuZCBjb3JyZWN0bHkgdGVzdGVkIHBvc2l0aXZlICAgIHwgICAgICAgfCAgICAgfA0KfCBUcnVlIG5lZ2F0aXZlIChUTikgIHwgVHJ1dGg6IDAsIFByZWRpY3Rpb246IDAgfCAgUHJvcG9ydGlvbiBvZiB0aGUgcG9wdWxhdGlvbiB0aGF0IGlzIG5vdCBhZmZlY3RlZCBieSBhIGNvbmRpdGlvbiBhbmQgY29ycmVjdGx5IHRlc3RlZCBuZWdhdGl2ZSAgfCAgICAgICB8ICAgICB8DQp8IEZhbHNlIHBvc2l0aXZlIChGUCkgfCBUcnV0aDogMCwgUHJlZGljdGlvbjogMSB8IFByb3BvcnRpb24gb2YgdGhlIHBvcHVsYXRpb24gdGhhdCBpcyBub3QgYWZmZWN0ZWQgYnkgYSBjb25kaXRpb24gYW5kIGluY29ycmVjdGx5IHRlc3RlZCBwb3NpdGl2ZSB8ICAgICAgIHwgICAgIHwNCnwgRmFsc2UgbmVnYXRpdmUgKEZOKSB8IFRydXRoOiAxLCBQcmVkaWN0aW9uOiAwIHwgIFByb3BvcnRpb24gb2YgdGhlIHBvcHVsYXRpb24gdGhhdCBpcyBhZmZlY3RlZCBieSBhIGNvbmRpdGlvbiBhbmQgaW5jb3JyZWN0bHkgdGVzdGVkIHBvc2l0aXZlLiAgIHwgICAgICAgfCAgICAgfA0KDQpUbyBjcmVhdGUgYSBjb25mdXNpb24gbWF0cml4LCBydW4gYGNvbGxlY3RfcHJlZGljdGlvbnMoKWAsIHdoaWNoIGRvZXMNCndoYXQgaXQgc3VnZ2VzdHMgLSBpdCBnYXRoZXJzIHRvZ2V0aGVyIHRoZSBtb2RlbCdzIHRlc3Qgc2V0IHByZWRpY3Rpb25zLg0KUGFzcyB0aGUgYGxhc3RfZml0YCBvYmplY3QgdG8gdGhpcyBmdW5jdGlvbiBiZWxvdy4NCg0KIyMjIyBbWW91ciBUdXJuXXtzdHlsZT0iY29sb3I6IGdyZWVuOyJ9IOKktQ0KDQpgYGB7cn0NCg0KcHJlZGljdGlvbnMgPC0gY29sbGVjdF9wcmVkaWN0aW9ucyhmaW5hbF9maXQpDQpwcmVkaWN0aW9ucw0KYGBgDQoNClRha2UgYSBsb29rIGF0IHRoZSBjb2x1bW5zLiBZb3UnbGwgbmVlZCB0byBwcm92aWRlIHRoZSBwcmVkaWN0aW9ucw0KKGNyZWF0ZWQgd2l0aCBgY29sbGVjdF9wcmVkaWN0aW9ucygpYCkgYW5kIHRoZW4gcGlwZSB0aGF0IHRvDQpgY29uZl9tYXQoKWAsIHRvIHdoaWNoIHlvdSBwcm92aWRlIHRoZSBuYW1lcyBvZiBhKSB0aGUgcHJlZGljdGlvbnMgYW5kDQpiKSB0aGUga25vd24gdmFsdWVzIGZvciB0aGUgdGVzdCBzZXQuIFNvbWUgY29kZSB0byBnZXQgeW91IHN0YXJ0ZWQgaXMNCmJlbG93Lg0KDQpgYGB7cn0NCiMgQ2FsY3VsYXRlIGNvbmZ1c2lvbiBtYXRyaXgNCmNvbmZ1c2lvbl9tYXRyaXggPC0gcHJlZGljdGlvbnMgJT4lDQogIGNvbmZfbWF0KC5wcmVkX2NsYXNzLCBwYXNzKQ0KICAjI2NvbmZfbWF0KHRydXRoID0gdHJ1dGgsIGVzdGltYXRlID0ucHJlZF9jbGFzcykNCiMjY29uZnVzaW9uX21hdHJpeA0KDQoNCiMgVG90YWwgbnVtYmVyIG9mIGRhdGEgcG9pbnRzIGluIHRoZSB0ZXN0IGRhdGEgc2V0DQp0b3RhbCA8LSBzdW0oY29uZnVzaW9uX21hdHJpeCR0YWJsZSkNCg0KIyBFeHRyYWN0IFRQLCBUTiwgRlAsIEZOIGZyb20gdGhlIGNvbmZ1c2lvbiBtYXRyaXgNClRQIDwtIGNvbmZ1c2lvbl9tYXRyaXgkdGFibGVbMiwgMl0gICMgVHJ1ZSBQb3NpdGl2ZXMNClROIDwtIGNvbmZ1c2lvbl9tYXRyaXgkdGFibGVbMSwgMV0gICMgVHJ1ZSBOZWdhdGl2ZXMNCkZQIDwtIGNvbmZ1c2lvbl9tYXRyaXgkdGFibGVbMSwgMl0gICMgRmFsc2UgUG9zaXRpdmVzDQpGTiA8LSBjb25mdXNpb25fbWF0cml4JHRhYmxlWzIsIDFdICAjIEZhbHNlIE5lZ2F0aXZlcw0KDQoNCiMgQ2FsY3VsYXRlIHBlcmNlbnRhZ2VzDQpUUF9wZXJjZW50YWdlIDwtIChUUCAvIHRvdGFsKSAqIDEwMA0KVE5fcGVyY2VudGFnZSA8LSAoVE4gLyB0b3RhbCkgKiAxMDANCkZQX3BlcmNlbnRhZ2UgPC0gKEZQIC8gdG90YWwpICogMTAwDQpGTl9wZXJjZW50YWdlIDwtIChGTiAvIHRvdGFsKSAqIDEwMA0KDQoNCiMgQ2FsY3VsYXRlIGFjY3VyYWN5IChzdW0gb2YgVFAgYW5kIFROIHBlcmNlbnRhZ2VzKQ0KYWNjdXJhY3kgPC0gKFRQX3BlcmNlbnRhZ2UgKyBUTl9wZXJjZW50YWdlKQ0KDQoNCiMgRmlsbCBpbiB0aGUgdmFsdWVzIGFuZCBwZXJjZW50YWdlcw0KY29uZnVzaW9uX21hdHJpeCRWYWx1ZSA8LSBjKFRQLCBUTiwgRlAsIEZOKQ0KY29uZnVzaW9uX21hdHJpeCRQZXJjZW50YWdlIDwtIGMoVFBfcGVyY2VudGFnZSwgVE5fcGVyY2VudGFnZSwgRlBfcGVyY2VudGFnZSwgRk5fcGVyY2VudGFnZSkNCg0KDQojIENvbmZ1c2lvbiBtYXRyaXggd2l0aCBmaWxsZWQgdmFsdWVzIGFuZCBwZXJjZW50YWdlcw0Kc3RyKGNvbmZ1c2lvbl9tYXRyaXgpDQp2aWV3KGNvbmZ1c2lvbl9tYXRyaXgpDQpwcmludChjb25mdXNpb25fbWF0cml4KQ0KDQoNCiMgUHJpbnQgYWNjdXJhY3kNCmNhdCgiQWNjdXJhY3k6IiwgYWNjdXJhY3ksICIlXG4iKQ0KDQoNCg0KYGBgDQoNCllvdSBzaG91bGQgc2VlIGEgY29uZnVzaW9uIG1hdHJpeCBvdXRwdXQuIElmIHNvLCBuaWNlIHdvcmshIFBsZWFzZSBmaWxsDQppbiB0aGUgKipWYWx1ZSoqIGFuZCAqKlBlcmNlbnRhZ2UqKiBjb2x1bW5zIGluIHRoZSB0YWJsZSBhYm92ZSAod2l0aCBUUCwNClROLCBGUCwgYW5kIEZOKSwgZW50ZXJpbmcgdGhlIGFwcHJvcHJpYXRlIHZhbHVlcyBhbmQgdGhlbiBjb252ZXJ0aW5nDQp0aG9zZSBpbnRvIGEgcGVyY2VudGFnZSBiYXNlZCBvbiB0aGUgdG90YWwgbnVtYmVyIG9mIGRhdGEgcG9pbnRzIGluIHRoZQ0KdGVzdCBkYXRhIHNldC4gVGhlIGFjY3VyYWN5IGNhbiBiZSBpbnRlcnByZXRlZCBhcyB0aGUgc3VtIG9mIHRoZSB0cnVlDQpwb3NpdGl2ZSBhbmQgdHJ1ZSBuZWdhdGl2ZSBwZXJjZW50YWdlcy4gU28sIHdoYXQncyB0aGUgYWNjdXJhY3k/IEFkZA0KdGhhdCBiZWxvdyBhcyBhIHBlcmNlbnRhZ2UuDQoNCi0gICBBY2N1cmFjeTogNTYuNDg1MDIgJQ0KDQojIyMgT3RoZXIgbWVhc3VyZXMgb2YgcHJlZGljdGl2ZSBhY2N1cmFjeQ0KDQpIZXJlJ3Mgd2hlcmUgdGhpbmdzIGdldCBpbnRlcmVzdGluZzogVGhlcmUgYXJlIG90aGVyIHN0YXRpc3RpY3MgdGhhdA0KaGF2ZSBkaWZmZXJlbnQgZGVub21pbmF0b3JzLiBSZWNhbGwgZnJvbSB0aGUgb3ZlcnZpZXcgcHJlc2VudGF0aW9uIHRoYXQNCndlIGNhbiBzbGljZSBhbmQgZGljZSB0aGUgY29uZnVzaW9uIG1hdHJpeCB0byBjYWxjdWxhdGUgc3RhdGlzdGljcyB0aGF0DQpnaXZlIHVzIGluc2lnaHRzIGludG8gdGhlIHByZWRpY3RpdmUgdXRpbGl0eSBvZiB0aGUgbW9kZWwuIEJhc2VkIG9uIHRoZQ0KYWJvdmUgKipWYWx1ZXMqKiBmb3IgVFAsIFROLCBGUCwgYW5kIEZOIHlvdSBpbnRlcnByZXRlZCBhIGZldyBtb21lbnRzDQphZ28sIGFkZCB0aGUgKipTdGF0aXN0aWMgVmFsdWVzKiogZm9yIHNlbnNpdGl2aXR5LCBzcGVjaWZpY2l0eSwNCnByZWNpc2lvbiwgYW5kIG5lZ2F0aXZlIHByZWRpY3RpdmUgdmFsdWUgYmVsb3cgdG8gdGhyZWUgZGVjaW1hbCBwb2ludHMuDQoNCiMjIyMgW1lvdXIgVHVybl17c3R5bGU9ImNvbG9yOiBncmVlbjsifSDipLUNCg0KfCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfCAgICAgICAgICAgICAgICB8ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB8ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfCAgICAgICAgICAgICAgICAgICAgICB8DQp8Oi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLTp8Oi0tLS0tLS0tLS0tLS0tOnw6LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tOnw6LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLTp8Oi0tLS0tLS0tLS0tLS0tLS0tLS0tOnwNCnwgICAgICAgICAgICAgICAgICoqU3RhdGlzdGljKiogICAgICAgICAgICAgICAgIHwgICoqRXF1YXRpb24qKiAgfCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAqKkludGVycHJldGF0aW9uKiogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAqKlF1ZXN0aW9uIEFuc3dlcmVkKiogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHwgKipTdGF0aXN0aWMgVmFsdWVzKiogfA0KfCAgICAgICAgICoqU2Vuc2l0aXZpdHkqKiAoQUtBIHJlY2FsbCkgICAgICAgICAgfCBUUCAvIChUUCArIEZOKSB8ICAgICAgICAgICAgICAgICAgICAgICBQcm9wb3J0aW9uIG9mIHRob3NlIHdobyBhcmUgYWZmZWN0ZWQgYnkgYSBjb25kaXRpb24gYW5kIGNvcnJlY3RseSB0ZXN0ZWQgcG9zaXRpdmUgICAgICAgICAgICAgICAgICAgICAgICB8ICAgICAgICBPdXQgb2YgYWxsIHRoZSBhY3R1YWwgcG9zaXRpdmVzLCBob3cgbWFueSBkaWQgd2UgY29ycmVjdGx5IHByZWRpY3Q/ICAgICAgICAgfCAgICAgICAgICAgICAgICAgICAgICB8DQp8ICAgICAgICAgICAgICAgICoqU3BlY2lmaWNpdHkqKiAgICAgICAgICAgICAgICB8IFROIC8gKEZQICsgVE4pIHwgICAgICAgICAgICAgICAgICAgICBQcm9wb3J0aW9uIG9mIHRob3NlIHdobyBhcmUgbm90IGFmZmVjdGVkIGJ5IGEgY29uZGl0aW9uIGFuZCBjb3JyZWN0bHkgdGVzdGVkIG5lZ2F0aXZlOyAgICAgICAgICAgICAgICAgICAgIHwgICAgICAgIE91dCBvZiBhbGwgdGhlIGFjdHVhbCBuZWdhdGl2ZXMsIGhvdyBtYW55IGRpZCB3ZSBjb3JyZWN0bHkgcHJlZGljdD8gICAgICAgICB8ICAgICAgICAgICAgICAgICAgICAgIHwNCnwgKipQcmVjaXNpb24qKiAoQUtBIFBvc2l0aXZlIFByZWRpY3RpdmUgVmFsdWUpIHwgVFAgLyAoVFAgKyBGUCkgfCAgICAgICAgICAgICAgICAgICAgICAgICAgICBQcm9wb3J0aW9uIG9mIHRob3NlIHdobyB0ZXN0ZWQgcG9zaXRpdmUgd2hvIGFyZSBhZmZlY3RlZCBieSBhIGNvbmRpdGlvbiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfCBPdXQgb2YgYWxsIHRoZSBpbnN0YW5jZXMgd2UgcHJlZGljdGVkIGFzIHBvc2l0aXZlLCBob3cgbWFueSBhcmUgYWN0dWFsbHkgcG9zaXRpdmU/IHwgICAgICAgICAgICAgICAgICAgICAgfA0KfCAgICAgICAgICoqTmVnYXRpdmUgUHJlZGljdGl2ZSBWYWx1ZSoqICAgICAgICAgfCBUTiAvIChUTiArIEZOKSB8IFByb3BvcnRpb24gb2YgdGhvc2Ugd2hvIHRlc3RlZCBwb3NpdGl2ZSB3aG8gYXJlIG5vdCBhZmZlY3RlZCBieSBhIGNvbmRpdGlvbjsgKnRoZSBwcm9iYWJpbGl0eSB0aGF0IGEgbmVnYXRpdmUgdGVzdCBpcyBjb3JyZWN0KiB8IE91dCBvZiBhbGwgdGhlIGluc3RhbmNlcyB3ZSBwcmVkaWN0ZWQgYXMgbmVnYXRpdmUsIGhvdyBtYW55IGFyZSBhY3R1YWxseSBuZWdhdGl2ZT8gfCAgICAgICAgICAgICAgICAgICAgICB8DQoNClNvLCB3aGF0IGRvZXMgdGhpcyBoYXJkLXdvbiBieSBvdXRwdXQgdGVsbCB1cz8gTGV0J3MgaW50ZXJwcmV0IGVhY2gNCnN0YXRpc3RpYyBjYXJlZnVsbHkgaW4gdGhlIHRhYmxlIGJlbG93LiBQbGVhc2UgYWRkIHRoZSB2YWx1ZSBhbmQgcHJvdmlkZQ0KYSAqc3Vic3RhbnRpdmUgaW50ZXJwcmV0YXRpb24qLiBPbmUgaXMgcHJvdmlkZWQgdG8gZ2V0IHlvdSBzdGFydGVkLg0KDQojIyMjIFtZb3VyIFR1cm5de3N0eWxlPSJjb2xvcjogZ3JlZW47In0g4qS1DQoNCnwgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHwNCnwtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tfC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLXwNCnwgKipTdGF0aXN0aWMqKiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfCAqKlN1YnN0YW50aXZlIEludGVycHJldGF0aW9uKiogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHwNCnwgQWNjdXJhY3kgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfCBUaGUgbW9kZWwgaGFzIGFuIGFjY3VyYWN5IG9mIDU2LjUlLlRoaXMgbWVhc3VyZXMgdGhlIG92ZXJhbGwgY29ycmVjdG5lc3Mgb2YgdGhlIG1vZGVsJ3MgcHJlZGljdGlvbnMuICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB8DQp8IFNlbnNpdGl2aXR5IChBS0EgcmVjYWxsKSAgICAgICAgICAgICAgICAgIHwgVGhlIG1vZGVsIGNvcnJlY3RseSBwcmVkaWN0cyBhYm91dCAyLzMgb2Ygc3R1ZGVudHMgd2hvIGRvIG5vdCBwYXNzIGNvcnJlY3RseSAoYXMgbm90IHBhc3NpbmcpLiBUaGlzIGlzIHByZXR0eSBnb29kLCBidXQgaXQgY291bGQgYmUgYmV0dGVyLiB8DQp8IFNwZWNpZmljaXR5ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHwgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB8DQp8IFByZWNpc2lvbiAoQUtBIFBvc2l0aXZlIFByZWRpY3RpdmUgVmFsdWUpIHw1NCUuIE1lYXN1cmVzIHRoZSBhY2N1cmFjeSBvZiBwb3NpdGl2ZSBwcmVkaWN0aW9ucyBtYWRlIGJ5IHRoZSBtb2RlbC4gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB8DQp8IE5lZ2F0aXZlIFByZWRpY3RpdmUgVmFsdWUgICAgICAgICAgICAgICAgIHwgNTcuOCUuIE1lYXN1cmVzIHRoZSBwcm9wb3J0aW9uIG9mIHRydWUgbmVnYXRpdmUgcHJlZGljdGlvbnMgb3V0IG9mIGFsbCB0aGUgaW5zdGFuY2VzIHByZWRpY3RlZCBhcyBuZWdhdGl2ZSBieSB0aGUgbW9kZWwuSW4gb3RoZXIgd29yZHMsIE5QViBtZWFzdXJlcyB0aGUgbW9kZWwncyBhYmlsaXR5IHRvIGNvcnJlY3RseSBpZGVudGlmeSBuZWdhdGl2ZSBpbnN0YW5jZXMuIEEgaGlnaCBOUFYgaW5kaWNhdGVzIHRoYXQgdGhlIG1vZGVsIGlzIGdvb2QgYXQgYXZvaWRpbmcgZmFsc2UgbmVnYXRpdmVzLCBtZWFuaW5nIGl0IGNvcnJlY3RseSBpZGVudGlmaWVzIGluc3RhbmNlcyB0aGF0IGRvIG5vdCBiZWxvbmcgdG8gdGhlIHBvc2l0aXZlIGNsYXNzLiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgDQoNCmBgYHtyfQ0Kc2Vuc2l0aXZpdHlfY2FsYyA8LSByb3VuZChUUCAvIChUUCArIEZOKSwzKQ0Kc2Vuc2l0aXZpdHlfY2FsYw0KDQpwcmVjaXNpb25fY2FsYyA8LSByb3VuZChUUCAvIChUUCArIEZQKSwzKQ0KcHJlY2lzaW9uX2NhbGMNCg0KbnBkX3ZhbHVlIDwtIHJvdW5kKFROIC8gKFROICsgRk4pLDMpDQpucGRfdmFsdWUNCg0Kc3BlY2lmaWNpdHlfY2FsYyA8LSByb3VuZChUTiAvIChGUCArIFROKSwzKQ0Kc3BlY2lmaWNpdHlfY2FsYw0KDQpgYGANCg0KDQpUaGlzIHByb2Nlc3MgbWlnaHQgc3VnZ2VzdCB0byB5b3UgaG93IGEgImdvb2QiIG1vZGVsIGlzbid0IG5lY2Vzc2FyaWx5DQpvbmUgdGhhdCBpcyB0aGUgbW9zdCBhY2N1cmF0ZSwgYnV0IGluc3RlYWQgaXMgb25lIHRoYXQgaGFzIGdvb2QgdmFsdWVzDQpmb3Igc3RhdGlzdGljcyB0aGF0IG1hdHRlciBnaXZlbiBvdXIgcGFydGljdWxhciBxdWVzdGlvbiBhbmQgY29udGV4dC4NCg0KUmVjYWxsIHRoYXQgQmFrZXIgYW5kIGNvbGxlYWd1ZXMgc291Z2h0IHRvIGJhbGFuY2UgYmV0d2VlbiBwcmVjaXNpb24gYW5kDQpyZWNhbGwgKHNwZWNpZmljaXR5KS4gUGxlYXNlIGJyaWVmbHkgZGlzY3VzcyBob3cgd2VsbCBvdXIgbW9kZWwgZG9lcw0KdGhpczsgaXMgaXQgYmV0dGVyIHN1aXRlZCB0byBjb3JyZWN0bHkgaWRlbnRpZnlpbmcgInBvc2l0aXZlIiBwYXNzIGNhc2VzDQooc2Vuc2l0aXZpdHkpIG9yICJuZWdhdGl2ZWx5IiBpZGVudGlmeWluZyBzdHVkZW50cyB3aG8gZG8gbm90IHBhc3MNCihzcGVjaWZpY2l0eSk/DQoNCi0gICBTZW5zaXRpdml0eShwb3NpdGl2ZSBwYXNzIGNhc2VzKSA9IDQxLjQlIFByb3BvcnRpb24gb2YgdGhvc2Ugd2hvIGFyZSBhZmZlY3RlZCBieSBhIGNvbmRpdGlvbiBhbmQgY29ycmVjdGx5IHRlc3RlZCBwb3NpdGl2ZS4NCiAgICBTcGVjaWZpY2l0eShuZWdhdGl2ZSBwYXNzIGNhc2VzKSA9IDY5LjUlIFByb3BvcnRpb24gb2YgdGhvc2Ugd2hvIGFyZSBub3QgYWZmZWN0ZWQgYnkgYSBjb25kaXRpb24gYW5kIGNvcnJlY3RseSB0ZXN0ZWQgbmVnYXRpdmUuDQogICAgVGhlIG1vZGVsIGlzIGJldHRlciBzdWl0ZWQgYXQgY29ycmVjdGx5IGlkZW50aWZ5aW5nIHNwZWNpZmljdHkgb3IgdGhvc2Ugd2hvIGFyZSBub3QgYWZmZWN0ZWQgYnkgYSBjb25kaXRpb24gYW5kIGNvcnJlY2x0eSB0ZXN0ZWQgbmVnYXRpdmUuDQojIyA1LiBDT01NVU5JQ0FURQ0KDQojIyMgUXVpY2tseSBjYWxjdWxhdGluZyBtZXRyaWNzDQoNCkFmdGVyIGFsbCBvZiB0aGlzIHdvcmssIHdlICpjYW4qIGNhbGN1bGF0ZSB0aGUgYWJvdmUgbXVjaCBtb3JlIGVhc2lseQ0KZ2l2ZW4gaG93IHdlIHNldHVwIG91ciBtZXRyaWNzICh1c2luZyBgbWV0cmljX3NldCgpYCBlYXJsaWVyLCBzdWNoIGFzDQp3aGVuIHdlIHdhbnQgdG8gZWZmaWNpZW50bHkgY29tbXVuaWNhdGUgdGhlIHJlc3VsdHMgb2Ygb3VyIGFuYWx5c2lzIHRvDQpvdXIgYXVkaWVuY2U/IEJlbG93LCB1c2UgYGNvbGxlY3RfbWV0cmljcygpYCB3aXRoIHRoZSBgZmluYWxfZml0YA0Kb2JqZWN0LCBub3RpbmcgdGhhdCBpbiBhZGRpdGlvbiB0byB0aGUgZm91ciBtZXRyaWNzIHdlIGNhbGN1bGF0ZWQNCm1hbnVhbGx5IGp1c3QgYSBmZXcgbW9tZW50cyBhZ28sIHdlIGFyZSBhbHNvIHByb3ZpZGVkIHdpdGggdGhlIGFjY3VyYWN5DQphbmQgQ29oZW4ncyBLYXBwYSB2YWx1ZXMuDQoNCiMjIyMgW1lvdXIgVHVybl17c3R5bGU9ImNvbG9yOiBncmVlbjsifSDipLUNCg0KYGBge3J9DQpjb2xsZWN0X21ldHJpY3MoZmluYWxfZml0KQ0KYGBgDQoNCkhhdmluZyBpbnZlc3RlZCBpbiB1bmRlcnN0YW5kaW5nIHRoZSB0ZXJtaW5vbG9neSBvZiBtYWNoaW5lIGxlYXJuaW5nDQptZXRyaWNzLCB3ZSdsbCB1c2UgdGhpcyAic2hvcnRjdXQiICh1c2luZyBgY29sbGVjdF9tZXRyaWNzKClgIGdvaW5nDQpmb3J3YXJkLg0KDQojIyMg8J+ntiBLbml0ICYgQ2hlY2sg4pyFDQoNCkNvbmdyYXR1bGF0aW9ucyAtIHlvdSd2ZSBjb21wbGV0ZWQgdGhpcyBjYXNlIHN0dWR5ISBHbyBvdmVyIHRvIExhYiAyIGJhZGdlIGFjdGl2aXR5Lg0K