diff --git a/celltype_annotation/analysis/copykat-rms-exploration.Rmd b/celltype_annotation/analysis/copykat-rms-exploration.Rmd index 67bf704..129454b 100644 --- a/celltype_annotation/analysis/copykat-rms-exploration.Rmd +++ b/celltype_annotation/analysis/copykat-rms-exploration.Rmd @@ -10,21 +10,34 @@ params: local_data_dir: "../data" library_id: "SCPCL000488" sample_id: "SCPCS000262" + threads: 16 --- -Placeholder for text about what this notebook does. +This notebook explores performance of the [`copyKat`](https://github.com/navinlabcode/copykat) package for calling tumor vs. normal cells in pediatric cancer scRNA-seq data. +Specifically, we look at a library from the `SCPCP000005` (RMS) project, where the submitter has annotated cell types which we can consider "ground truth" for the purposes of this exploration. +We'll assess agreement between the submitter tumor/normal classification and what `copyKat` infers as tumor/normal. + ## Set Up ```{r} suppressPackageStartupMessages({ library(SingleCellExperiment) - library(ggplot2) library(copykat) # https://github.com/navinlabcode/copykat }) -theme_set(theme_bw()) + +# define output path and SCE file +output_dir <- here::here("celltype_annotation", + "analysis", + "copykat-results") +fs::dir_create(output_dir) + +copykat_sce <- file.path(output_dir, + glue::glue("{params$library}_copykat_sce.rds")) ``` +In the next chunk, we obtain the _submitter-annotated_ SCE from S3, if it is not already locally present. +We'll wait to read it in when actually running `copyKat`, mostly for memory reasons. ```{r} @@ -46,32 +59,164 @@ if (!(file.exists(local_submitter_sce_path))) { sync_call <- glue::glue('op run -- aws s3 sync {params$s3_submitter_data_dir} {params$local_data_dir}/{params$sample_id} --exclude "*" --include "{submitter_annotated_sce_file}"') system(sync_call, ignore.stdout = TRUE) } - -# read in sce -sce <- readr::read_rds(local_submitter_sce_path) ``` ## Run CopyKAT -First, we'll run `CopyKAT` on the SCE file. - +Now, we are ready to run `copyKat` on the SCE, using the _raw expression matrix_ as described in the [GitHub `README` file](https://github.com/navinlabcode/copykat/blob/master/README.md). -To run `CopyKAT`, we'll need to pull out the _raw expression matrix_, as described: https://github.com/navinlabcode/copykat#step-2-prepare-the-readcount-input-file. +Of note, the `README` includes this caveat about the `"distance"` parameter: -Some notes on the distance parameter: > One struggling observation is that none of one clustering method could fit all datasets. In this version, I add a distance parameters for clustering that include "euclidean" distance and correlational distance, ie. 1-"pearson" and "spearman" similarity. In general, corretional distances tend to favor noisy data, while euclidean distance tends to favor data with larger CN segments. -This step below takes _quite a while_ to run, so we'll want to save it to a file probably. -It took nearly an hour. Should probably be run on the server with as many cores as possible. +Therefore we'll compare `"euclidian"`, `"spearman"`, and `"pearson"` values for [this parameter](https://github.com/navinlabcode/copykat/blob/b795ff793522499f814f6ae282aad1aab790902f/R/copykat.R#L251-L255). + + ```{r message=FALSE, warning=FALSE} -rms_copykat_result <- copykat( - rawmat = as.matrix(counts(sce)), - id.type = "E", # we have Ensembl gene ids, not the default gene symbols ("S") - sam.name = params$library_id, # sample name - n.cores = 2 -) +# Only run copykat if the final SCE does not exist +if (file.exists(copykat_sce)) { + + sce <- readr::read_rds(copykat_sce) + +} else { + + # Otherwise, read in initial SCE and run copyKat + sce <- readr::read_rds(local_submitter_sce_path) + + purrr::walk( + c("spearman", "pearson", "euclidean"), + \(x) { + + copykat_result <- copykat( + rawmat = as.matrix(counts(sce)), + id.type = "E", # we have Ensembl gene ids, not the default gene symbols ("S") + sam.name = params$library_id, + distance = x, + plot.genes = FALSE, + n.cores = params$threads + ) + + # save prediction to sce + colname <- glue::glue("copykat_{x}") + colData(sce)[[colname]] <- copykat_result$prediction$copykat.pred + + # export full result + output_file <- file.path(output_dir, + glue::glue("{params$library}_copykat_{x}.rds")) + readr::write_rds(copykat_result, output_file) + } + ) + + # finally, export SCE with all copykat predictions + readr::write_rds(sce, copykat_sce) +} ``` + +## copyKat results + +First, we'll look at quick tables for correspondance between submitter cell type annotations and `copyKat` inferences. +For `copyKat`, "diploid" is normal and "aneuploid" is tumor. + +```{r} +table(sce$celltype, sce$copykat_spearman) +``` + +```{r} +table(sce$celltype, sce$copykat_pearson) +``` + +```{r} +table(sce$celltype, sce$copykat_euclidian) +``` + + +Next, we'll look at some classification metrics overall, using a custom function to calculate a confusion matrix. +For this, we will _only consider_ cells which i) the submitter was able to classify, and ii) `copyKat` was able to classify. +In other words, all `NA` values are removed from the data before calculations. + + + +```{r} +# Custom function to calculate some confusion matrix quantities, including +# accuracy, TPR, and FPR +calculate_confusion <- function(df, test_column) { + # assumes the true column is `celltype_class` + df |> + # remove NA values all around + tidyr::drop_na(celltype_class) |> + dplyr::filter({{test_column}} != "not.defined") |> + # confusion matrix + dplyr::mutate(confusion = dplyr::case_when( + celltype_class == "Tumor" & {{test_column}} == "aneuploid" ~ "TP", + celltype_class == "Tumor" & {{test_column}} == "diploid" ~ "FN", + celltype_class == "Normal" & {{test_column}} == "aneuploid" ~ "FP", + celltype_class == "Normal" & {{test_column}} == "diploid" ~ "TN", + TRUE ~ NA_character_, + )) |> + # just in case.. + tidyr::drop_na(confusion) |> + dplyr::count(confusion) |> + tidyr::pivot_wider( + names_from = "confusion", + values_from = "n" + ) |> + # calculate some metrics + dplyr::summarize(accuracy = (TP+TN)/(TP+TN+FP+FN), + tpr = (TP)/(TP+FN), + fpr = (FP)/(FP+TN)) +} +``` + + +```{r} +# Create a data frame for performing calculations on +copykat_df <- colData(sce) |> + as.data.frame() |> + dplyr::select(celltype, contains("copykat")) |> + dplyr::mutate(celltype_class = ifelse( + stringr::str_starts(celltype, "Tumor"), + "Tumor", + "Normal" + )) + +# run confusion matrix function +cat("============== SPEARMAN distance =====================") +calculate_confusion(copykat_df, copykat_spearman) + +cat("============== PEARSON distance =====================") +calculate_confusion(copykat_df, copykat_pearson) + +cat("============== EUCLIDIAN distance =====================") +calculate_confusion(copykat_df, copykat_euclidian) +``` + +## Conclusions + +`copyKat` does not seem to agree with the submitter annotations, but possibly has the best chance of success with one of the correlational distances (spearman or pearson). + +It is also worth noting some drawbacks of the `copyKat` package: + +* It emits _a lot of output_, and we don't have much control over it! + * This includes both `print()` statements and output files, some of which are optional but not all. + * The _lots and lots of output_ is not seen in this notebook because it was rendered after files already existed; the output _cannot_ be suppressed by turning off warnings or messages, because they are all print statements. + * Of this output, we get a `"cell: #"` printed for each cell, and we also see that the majority also have a printed statement: `"WARNING! NOT CONVERGENT!"`. + This latter statement is _printed_ from a [dependency](https://github.com/cran/mixtools/blob/4af1e2789bcea7df3c1775a53cd05b37ec3185d0/R/normalmixEM.R#L129-L131) +* More on the lack of convergence: + * `copyKat` has hardcoded the maximum number of iterations to 500, but the dependency has this default at 1000. + It's possible we'd see convergence with more iterations. + * This convergence information does _not appear to be captured_ in any of the output - it is just a print statement! +* `copyKat` [hardcodes a random seed](https://github.com/navinlabcode/copykat/blob/b795ff793522499f814f6ae282aad1aab790902f/R/copykat.R#L29-L32) so we can't readily assess bias from starting conditions. + +Based on these drawbacks, if we want to use `copyKat` moving forward, we might consider forking the repository and modifying aspects that best suit our needs: + * More control over the seed + * Less printing, where possible + * Fewer output files + * Increasing the number of iterations (or just removing `copyKat`'s 500 limit and defaulting to the dependency's 1000) + * If possible, we'd like to _record and export_ which cells do not converge. + * Easiest way to get this? Run `copyKat` from a script and capture all the `stdout` into a file we can parse, but that parsing will not necessarily be pleasant! + + ## Session Info ```{r} diff --git a/celltype_annotation/analysis/copykat-rms-exploration.nb.html b/celltype_annotation/analysis/copykat-rms-exploration.nb.html new file mode 100644 index 0000000..dd4ff20 --- /dev/null +++ b/celltype_annotation/analysis/copykat-rms-exploration.nb.html @@ -0,0 +1,2341 @@ + + + + + + + + + + + + + +CopyKAT: Rhabdomyosarcoma cell type tumor vs normal inference for SCPCL000488 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+
+
+
+ +
+ + + + + + + + +

This notebook explores performance of the copyKat +package for calling tumor vs. normal cells in pediatric cancer scRNA-seq +data. Specifically, we look at a library from the +SCPCP000005 (RMS) project, where the submitter has +annotated cell types which we can consider “ground truth” for the +purposes of this exploration. We’ll assess agreement between the +submitter tumor/normal classification and what copyKat +infers as tumor/normal.

+
+

Set Up

+ + + +
suppressPackageStartupMessages({
+  library(SingleCellExperiment)
+  library(copykat) # https://github.com/navinlabcode/copykat
+})
+
+# define output path and SCE file
+output_dir <-  here::here("celltype_annotation", 
+                         "analysis", 
+                         "copykat-results")
+fs::dir_create(output_dir)
+
+copykat_sce <- file.path(output_dir,
+                         glue::glue("{params$library}_copykat_sce.rds"))
+ + + +

In the next chunk, we obtain the submitter-annotated SCE +from S3, if it is not already locally present. We’ll wait to read it in +when actually running copyKat, mostly for memory +reasons.

+ + + +
# make local data directory if it doesn't exist
+if (!dir.exists(params$local_data_dir)) {
+  dir.create(params$local_data_dir, recursive = TRUE)
+}
+
+# build path to submitter annotated sce file 
+submitter_annotated_sce_file <- glue::glue("{params$library_id}_processed_celltype.rds")
+
+
+local_submitter_sce_path <- file.path(params$local_data_dir,
+                                      params$sample_id,
+                                      submitter_annotated_sce_file)
+
+# get submitter sce from S3 if not already present locally
+if (!(file.exists(local_submitter_sce_path))) {
+  sync_call <- glue::glue('op run -- aws s3 sync {params$s3_submitter_data_dir} {params$local_data_dir}/{params$sample_id} --exclude "*" --include "{submitter_annotated_sce_file}"')
+  system(sync_call, ignore.stdout = TRUE) 
+}
+ + + +
+
+

Run CopyKAT

+

Now, we are ready to run copyKat on the SCE, using the +raw expression matrix as described in the GitHub +README file.

+

Of note, the README includes this caveat about the +"distance" parameter:

+
+

One struggling observation is that none of one clustering method +could fit all datasets. In this version, I add a distance parameters for +clustering that include “euclidean” distance and correlational distance, +ie. 1-“pearson” and “spearman” similarity. In general, corretional +distances tend to favor noisy data, while euclidean distance tends to +favor data with larger CN segments.

+
+

Therefore we’ll compare "euclidian", +"spearman", and "pearson" values for this +parameter.

+ + + +
# Only run copykat if the final SCE does not exist
+if (file.exists(copykat_sce)) {
+  
+  sce <- readr::read_rds(copykat_sce)
+  
+} else {
+  
+  # Otherwise, read in initial SCE and run copyKat
+  sce <- readr::read_rds(local_submitter_sce_path)
+    
+  purrr::walk(
+    c("spearman", "pearson", "euclidean"), 
+    \(x) {
+      
+      copykat_result <- copykat(
+        rawmat = as.matrix(counts(sce)), 
+        id.type = "E", # we have Ensembl gene ids, not the default gene symbols ("S")
+        sam.name = params$library_id, 
+        distance = x, 
+        plot.genes = FALSE,
+        n.cores = params$threads
+      )
+      
+      # save prediction to sce
+      colname <- glue::glue("copykat_{x}")
+      colData(sce)[[colname]] <- copykat_result$prediction$copykat.pred
+      
+      # export full result
+      output_file <- file.path(output_dir,
+                              glue::glue("{params$library}_copykat_{x}.rds"))
+      readr::write_rds(copykat_result, output_file)
+    }
+  )
+  
+  # finally, export SCE with all copykat predictions
+  readr::write_rds(sce, copykat_sce)
+} 
+ + + +
+
+

copyKat results

+

First, we’ll look at quick tables for correspondance between +submitter cell type annotations and copyKat inferences. For +copyKat, “diploid” is normal and “aneuploid” is tumor.

+ + + +
table(sce$celltype, sce$copykat_spearman)
+ + +
                      
+                       aneuploid diploid not.defined
+  Fibroblast                  54       8           0
+  Lymphocyte                  96      21          10
+  Monocyte                   155      34           6
+  Neuron                      60      29           0
+  Tumor_Mesoderm             183      37           8
+  Tumor_Myoblast-A            54      21           0
+  Tumor_Myoblast-B            24      14           4
+  Tumor_Myoblast-C          1670    1739          66
+  Tumor_Myoblast-D            84      33           8
+  Tumor_Myocyte-A            318     222           5
+  Tumor_Myocyte-B            518     114           6
+  Vascular Endothelium       241      25           3
+ + + + + + +
table(sce$celltype, sce$copykat_pearson)
+ + +
                      
+                       aneuploid diploid not.defined
+  Fibroblast                  55       7           0
+  Lymphocyte                 103      14          10
+  Monocyte                   158      31           6
+  Neuron                      58      31           0
+  Tumor_Mesoderm             199      21           8
+  Tumor_Myoblast-A            60      15           0
+  Tumor_Myoblast-B            26      12           4
+  Tumor_Myoblast-C          1780    1629          66
+  Tumor_Myoblast-D            98      19           8
+  Tumor_Myocyte-A            390     150           5
+  Tumor_Myocyte-B            553      79           6
+  Vascular Endothelium       244      22           3
+ + + + + + +
table(sce$celltype, sce$copykat_euclidian)
+ + +
                      
+                       aneuploid diploid not.defined
+  Fibroblast                  28      34           0
+  Lymphocyte                  29      88          10
+  Monocyte                    87     102           6
+  Neuron                       8      81           0
+  Tumor_Mesoderm              60     160           8
+  Tumor_Myoblast-A             7      68           0
+  Tumor_Myoblast-B             4      34           4
+  Tumor_Myoblast-C           196    3213          66
+  Tumor_Myoblast-D             6     111           8
+  Tumor_Myocyte-A             42     498           5
+  Tumor_Myocyte-B             49     583           6
+  Vascular Endothelium       173      93           3
+ + + +

Next, we’ll look at some classification metrics overall, using a +custom function to calculate a confusion matrix. For this, we will +only consider cells which i) the submitter was able to +classify, and ii) copyKat was able to classify. In other +words, all NA values are removed from the data before +calculations.

+ + + +
# Custom function to calculate some confusion matrix quantities, including
+# accuracy, TPR, and FPR
+calculate_confusion <- function(df, test_column) {
+  # assumes the true column is `celltype_class`
+  df |>
+    # remove NA values all around
+    tidyr::drop_na(celltype_class) |>
+    dplyr::filter({{test_column}} != "not.defined") |>
+    # confusion matrix
+    dplyr::mutate(confusion = dplyr::case_when(
+      celltype_class == "Tumor" & {{test_column}} == "aneuploid"  ~ "TP",
+      celltype_class == "Tumor" & {{test_column}} == "diploid"    ~ "FN",
+      celltype_class == "Normal" & {{test_column}} == "aneuploid" ~ "FP",
+      celltype_class == "Normal" & {{test_column}} == "diploid"   ~ "TN",
+      TRUE ~ NA_character_,
+    )) |>
+    # just in case..
+    tidyr::drop_na(confusion) |>
+    dplyr::count(confusion) |>
+    tidyr::pivot_wider(
+      names_from = "confusion", 
+      values_from = "n"
+    ) |>
+    # calculate some metrics
+    dplyr::summarize(accuracy = (TP+TN)/(TP+TN+FP+FN), 
+                     tpr      = (TP)/(TP+FN),
+                     fpr      = (FP)/(FP+TN)) 
+}
+ + + + + + +
# Create a data frame for performing calculations on
+copykat_df <- colData(sce) |>
+  as.data.frame() |>
+  dplyr::select(celltype, contains("copykat")) |>
+  dplyr::mutate(celltype_class = ifelse(
+    stringr::str_starts(celltype, "Tumor"), 
+    "Tumor", 
+    "Normal"
+  )) 
+
+# run confusion matrix function
+cat("============== SPEARMAN distance =====================")
+ + +
============== SPEARMAN distance =====================
+ + +
calculate_confusion(copykat_df, copykat_spearman)
+ + +
+ +
+ + +

+cat("============== PEARSON distance =====================")
+ + +
============== PEARSON distance =====================
+ + +
calculate_confusion(copykat_df, copykat_pearson)
+ + +
+ +
+ + +

+cat("============== EUCLIDIAN distance =====================")
+ + +
============== EUCLIDIAN distance =====================
+ + +
calculate_confusion(copykat_df, copykat_euclidian)
+ + +
+ +
+ + + +
+
+

Conclusions

+

copyKat does not seem to agree with the submitter +annotations, but possibly has the best chance of success with one of the +correlational distances (spearman or pearson).

+

It is also worth noting some drawbacks of the copyKat +package:

+
    +
  • It emits a lot of output, and we don’t have much control +over it! +
      +
    • This includes both print() statements and output files, +some of which are optional but not all.
    • +
    • The lots and lots of output is not seen in this notebook +because it was rendered after files already existed; the output +cannot be suppressed by turning off warnings or messages, +because they are all print statements.
    • +
    • Of this output, we get a "cell: #" printed for each +cell, and we also see that the majority also have a printed statement: +"WARNING! NOT CONVERGENT!". This latter statement is +printed from a dependency
    • +
  • +
  • More on the lack of convergence: +
      +
    • copyKat has hardcoded the maximum number of iterations +to 500, but the dependency has this default at 1000. It’s possible we’d +see convergence with more iterations.
    • +
    • This convergence information does not appear to be captured +in any of the output - it is just a print statement!
    • +
  • +
  • copyKat hardcodes +a random seed so we can’t readily assess bias from starting +conditions.
  • +
+

Based on these drawbacks, if we want to use copyKat +moving forward, we might consider forking the repository and modifying +aspects that best suit our needs: * More control over the seed * Less +printing, where possible * Fewer output files * Increasing the number of +iterations (or just removing copyKat’s 500 limit and +defaulting to the dependency’s 1000) * If possible, we’d like to +record and export which cells do not converge. * Easiest way to +get this? Run copyKat from a script and capture all the +stdout into a file we can parse, but that parsing will not +necessarily be pleasant!

+
+
+

Session Info

+ + + +
sessionInfo()
+ + +
R version 4.2.3 (2023-03-15)
+Platform: x86_64-pc-linux-gnu (64-bit)
+Running under: Ubuntu 18.04.3 LTS
+
+Matrix products: default
+BLAS:   /usr/lib/x86_64-linux-gnu/openblas/libblas.so.3
+LAPACK: /usr/lib/x86_64-linux-gnu/libopenblasp-r0.2.20.so
+
+locale:
+ [1] LC_CTYPE=C.UTF-8       LC_NUMERIC=C           LC_TIME=C.UTF-8       
+ [4] LC_COLLATE=C.UTF-8     LC_MONETARY=C.UTF-8    LC_MESSAGES=C.UTF-8   
+ [7] LC_PAPER=C.UTF-8       LC_NAME=C              LC_ADDRESS=C          
+[10] LC_TELEPHONE=C         LC_MEASUREMENT=C.UTF-8 LC_IDENTIFICATION=C   
+
+attached base packages:
+[1] stats4    stats     graphics  grDevices datasets  utils     methods   base     
+
+other attached packages:
+ [1] copykat_1.1.0               SingleCellExperiment_1.20.1 SummarizedExperiment_1.28.0
+ [4] Biobase_2.58.0              GenomicRanges_1.50.2        GenomeInfoDb_1.34.9        
+ [7] IRanges_2.32.0              S4Vectors_0.36.2            BiocGenerics_0.44.0        
+[10] MatrixGenerics_1.10.0       matrixStats_0.63.0         
+
+loaded via a namespace (and not attached):
+ [1] modeltools_0.2-23      tidyselect_1.2.0       xfun_0.39             
+ [4] purrr_1.0.1            splines_4.2.3          lattice_0.21-8        
+ [7] colorspace_2.1-0       vctrs_0.6.2            generics_0.1.3        
+[10] yaml_2.3.7             utf8_1.2.3             rlang_1.1.0           
+[13] miQC_1.6.0             pillar_1.9.0           withr_2.5.0           
+[16] glue_1.6.2             GenomeInfoDbData_1.2.9 lifecycle_1.0.3       
+[19] stringr_1.5.0          zlibbioc_1.44.0        munsell_0.5.0         
+[22] gtable_0.3.3           knitr_1.42             tzdb_0.3.0            
+[25] flexmix_2.3-19         fansi_1.0.4            readr_2.1.4           
+[28] renv_0.15.5            scales_1.2.1           BiocManager_1.30.20   
+[31] DelayedArray_0.24.0    XVector_0.38.0         fs_1.6.1              
+[34] ggplot2_3.4.2          hms_1.1.3              stringi_1.7.12        
+[37] dplyr_1.1.2            rprojroot_2.0.3        grid_4.2.3            
+[40] here_1.0.1             cli_3.6.1              tools_4.2.3           
+[43] bitops_1.0-7           magrittr_2.0.3         RCurl_1.98-1.12       
+[46] tibble_3.2.1           tidyr_1.3.0            pkgconfig_2.0.3       
+[49] Matrix_1.6-1           rstudioapi_0.14        R6_2.5.1              
+[52] nnet_7.3-18            compiler_4.2.3        
+ + +
+ +
LS0tCnRpdGxlOiAiQ29weUtBVDogUmhhYmRvbXlvc2FyY29tYSBjZWxsIHR5cGUgdHVtb3IgdnMgbm9ybWFsIGluZmVyZW5jZSBmb3IgYHIge3BhcmFtcyRsaWJyYXJ5X2lkfWAiCm91dHB1dDogCiAgaHRtbF9ub3RlYm9vazoKICB0b2M6IHRydWUKdG9jX2Zsb2F0OiB0cnVlCnBhcmFtczoKICBzM19zaW5nbGVyX2RhdGFfZGlyOiAiczM6Ly9uZXh0Zmxvdy1jY2RsLXJlc3VsdHMvc2NwY2EvcHJvY2Vzc2VkL3Jlc3VsdHMvU0NQQ1AwMDAwMDUiIAogIHMzX3N1Ym1pdHRlcl9kYXRhX2RpcjogInMzOi8vc2MtZGF0YS1pbnRlZ3JhdGlvbi9zY3BjYS9jZWxsdHlwZV9zY2UiCiAgbG9jYWxfZGF0YV9kaXI6ICIuLi9kYXRhIgogIGxpYnJhcnlfaWQ6ICJTQ1BDTDAwMDQ4OCIKICBzYW1wbGVfaWQ6ICJTQ1BDUzAwMDI2MiIKICB0aHJlYWRzOiAxNgotLS0KICAKVGhpcyBub3RlYm9vayBleHBsb3JlcyBwZXJmb3JtYW5jZSBvZiB0aGUgW2Bjb3B5S2F0YF0oaHR0cHM6Ly9naXRodWIuY29tL25hdmlubGFiY29kZS9jb3B5a2F0KSBwYWNrYWdlIGZvciBjYWxsaW5nIHR1bW9yIHZzLiBub3JtYWwgY2VsbHMgaW4gcGVkaWF0cmljIGNhbmNlciBzY1JOQS1zZXEgZGF0YS4KU3BlY2lmaWNhbGx5LCB3ZSBsb29rIGF0IGEgbGlicmFyeSBmcm9tIHRoZSBgU0NQQ1AwMDAwMDVgIChSTVMpIHByb2plY3QsIHdoZXJlIHRoZSBzdWJtaXR0ZXIgaGFzIGFubm90YXRlZCBjZWxsIHR5cGVzIHdoaWNoIHdlIGNhbiBjb25zaWRlciAiZ3JvdW5kIHRydXRoIiBmb3IgdGhlIHB1cnBvc2VzIG9mIHRoaXMgZXhwbG9yYXRpb24uCldlJ2xsIGFzc2VzcyBhZ3JlZW1lbnQgYmV0d2VlbiB0aGUgc3VibWl0dGVyIHR1bW9yL25vcm1hbCBjbGFzc2lmaWNhdGlvbiBhbmQgd2hhdCBgY29weUthdGAgaW5mZXJzIGFzIHR1bW9yL25vcm1hbC4KCgojIyBTZXQgVXAKCmBgYHtyfQpzdXBwcmVzc1BhY2thZ2VTdGFydHVwTWVzc2FnZXMoewogIGxpYnJhcnkoU2luZ2xlQ2VsbEV4cGVyaW1lbnQpCiAgbGlicmFyeShjb3B5a2F0KSAjIGh0dHBzOi8vZ2l0aHViLmNvbS9uYXZpbmxhYmNvZGUvY29weWthdAp9KQoKIyBkZWZpbmUgb3V0cHV0IHBhdGggYW5kIFNDRSBmaWxlCm91dHB1dF9kaXIgPC0gIGhlcmU6OmhlcmUoImNlbGx0eXBlX2Fubm90YXRpb24iLCAKICAgICAgICAgICAgICAgICAgICAgICAgICJhbmFseXNpcyIsIAogICAgICAgICAgICAgICAgICAgICAgICAgImNvcHlrYXQtcmVzdWx0cyIpCmZzOjpkaXJfY3JlYXRlKG91dHB1dF9kaXIpCgpjb3B5a2F0X3NjZSA8LSBmaWxlLnBhdGgob3V0cHV0X2RpciwKICAgICAgICAgICAgICAgICAgICAgICAgIGdsdWU6OmdsdWUoIntwYXJhbXMkbGlicmFyeX1fY29weWthdF9zY2UucmRzIikpCmBgYAoKSW4gdGhlIG5leHQgY2h1bmssIHdlIG9idGFpbiB0aGUgX3N1Ym1pdHRlci1hbm5vdGF0ZWRfIFNDRSBmcm9tIFMzLCBpZiBpdCBpcyBub3QgYWxyZWFkeSBsb2NhbGx5IHByZXNlbnQuCldlJ2xsIHdhaXQgdG8gcmVhZCBpdCBpbiB3aGVuIGFjdHVhbGx5IHJ1bm5pbmcgYGNvcHlLYXRgLCBtb3N0bHkgZm9yIG1lbW9yeSByZWFzb25zLgoKCmBgYHtyfQojIG1ha2UgbG9jYWwgZGF0YSBkaXJlY3RvcnkgaWYgaXQgZG9lc24ndCBleGlzdAppZiAoIWRpci5leGlzdHMocGFyYW1zJGxvY2FsX2RhdGFfZGlyKSkgewogIGRpci5jcmVhdGUocGFyYW1zJGxvY2FsX2RhdGFfZGlyLCByZWN1cnNpdmUgPSBUUlVFKQp9CgojIGJ1aWxkIHBhdGggdG8gc3VibWl0dGVyIGFubm90YXRlZCBzY2UgZmlsZSAKc3VibWl0dGVyX2Fubm90YXRlZF9zY2VfZmlsZSA8LSBnbHVlOjpnbHVlKCJ7cGFyYW1zJGxpYnJhcnlfaWR9X3Byb2Nlc3NlZF9jZWxsdHlwZS5yZHMiKQoKCmxvY2FsX3N1Ym1pdHRlcl9zY2VfcGF0aCA8LSBmaWxlLnBhdGgocGFyYW1zJGxvY2FsX2RhdGFfZGlyLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBhcmFtcyRzYW1wbGVfaWQsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3VibWl0dGVyX2Fubm90YXRlZF9zY2VfZmlsZSkKCiMgZ2V0IHN1Ym1pdHRlciBzY2UgZnJvbSBTMyBpZiBub3QgYWxyZWFkeSBwcmVzZW50IGxvY2FsbHkKaWYgKCEoZmlsZS5leGlzdHMobG9jYWxfc3VibWl0dGVyX3NjZV9wYXRoKSkpIHsKICBzeW5jX2NhbGwgPC0gZ2x1ZTo6Z2x1ZSgnb3AgcnVuIC0tIGF3cyBzMyBzeW5jIHtwYXJhbXMkczNfc3VibWl0dGVyX2RhdGFfZGlyfSB7cGFyYW1zJGxvY2FsX2RhdGFfZGlyfS97cGFyYW1zJHNhbXBsZV9pZH0gLS1leGNsdWRlICIqIiAtLWluY2x1ZGUgIntzdWJtaXR0ZXJfYW5ub3RhdGVkX3NjZV9maWxlfSInKQogIHN5c3RlbShzeW5jX2NhbGwsIGlnbm9yZS5zdGRvdXQgPSBUUlVFKSAKfQpgYGAKCgojIyBSdW4gQ29weUtBVAoKTm93LCB3ZSBhcmUgcmVhZHkgdG8gcnVuIGBjb3B5S2F0YCBvbiB0aGUgU0NFLCB1c2luZyB0aGUgX3JhdyBleHByZXNzaW9uIG1hdHJpeF8gYXMgZGVzY3JpYmVkIGluIHRoZSBbR2l0SHViIGBSRUFETUVgIGZpbGVdKGh0dHBzOi8vZ2l0aHViLmNvbS9uYXZpbmxhYmNvZGUvY29weWthdC9ibG9iL21hc3Rlci9SRUFETUUubWQpLgoKT2Ygbm90ZSwgdGhlIGBSRUFETUVgIGluY2x1ZGVzIHRoaXMgY2F2ZWF0IGFib3V0IHRoZSBgImRpc3RhbmNlImAgcGFyYW1ldGVyOgoKPiBPbmUgc3RydWdnbGluZyBvYnNlcnZhdGlvbiBpcyB0aGF0IG5vbmUgb2Ygb25lIGNsdXN0ZXJpbmcgbWV0aG9kIGNvdWxkIGZpdCBhbGwgZGF0YXNldHMuIEluIHRoaXMgdmVyc2lvbiwgSSBhZGQgYSBkaXN0YW5jZSBwYXJhbWV0ZXJzIGZvciBjbHVzdGVyaW5nIHRoYXQgaW5jbHVkZSAiZXVjbGlkZWFuIiBkaXN0YW5jZSBhbmQgY29ycmVsYXRpb25hbCBkaXN0YW5jZSwgaWUuIDEtInBlYXJzb24iIGFuZCAic3BlYXJtYW4iIHNpbWlsYXJpdHkuIEluIGdlbmVyYWwsIGNvcnJldGlvbmFsIGRpc3RhbmNlcyB0ZW5kIHRvIGZhdm9yIG5vaXN5IGRhdGEsIHdoaWxlIGV1Y2xpZGVhbiBkaXN0YW5jZSB0ZW5kcyB0byBmYXZvciBkYXRhIHdpdGggbGFyZ2VyIENOIHNlZ21lbnRzLgoKVGhlcmVmb3JlIHdlJ2xsIGNvbXBhcmUgYCJldWNsaWRpYW4iYCwgYCJzcGVhcm1hbiJgLCBhbmQgYCJwZWFyc29uImAgdmFsdWVzIGZvciBbdGhpcyBwYXJhbWV0ZXJdKGh0dHBzOi8vZ2l0aHViLmNvbS9uYXZpbmxhYmNvZGUvY29weWthdC9ibG9iL2I3OTVmZjc5MzUyMjQ5OWY4MTRmNmFlMjgyYWFkMWFhYjc5MDkwMmYvUi9jb3B5a2F0LlIjTDI1MS1MMjU1KS4KCgpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQojIE9ubHkgcnVuIGNvcHlrYXQgaWYgdGhlIGZpbmFsIFNDRSBkb2VzIG5vdCBleGlzdAppZiAoZmlsZS5leGlzdHMoY29weWthdF9zY2UpKSB7CiAgCiAgc2NlIDwtIHJlYWRyOjpyZWFkX3Jkcyhjb3B5a2F0X3NjZSkKICAKfSBlbHNlIHsKICAKICAjIE90aGVyd2lzZSwgcmVhZCBpbiBpbml0aWFsIFNDRSBhbmQgcnVuIGNvcHlLYXQKICBzY2UgPC0gcmVhZHI6OnJlYWRfcmRzKGxvY2FsX3N1Ym1pdHRlcl9zY2VfcGF0aCkKICAgIAogIHB1cnJyOjp3YWxrKAogICAgYygic3BlYXJtYW4iLCAicGVhcnNvbiIsICJldWNsaWRlYW4iKSwgCiAgICBcKHgpIHsKICAgICAgCiAgICAgIGNvcHlrYXRfcmVzdWx0IDwtIGNvcHlrYXQoCiAgICAgICAgcmF3bWF0ID0gYXMubWF0cml4KGNvdW50cyhzY2UpKSwgCiAgICAgICAgaWQudHlwZSA9ICJFIiwgIyB3ZSBoYXZlIEVuc2VtYmwgZ2VuZSBpZHMsIG5vdCB0aGUgZGVmYXVsdCBnZW5lIHN5bWJvbHMgKCJTIikKICAgICAgICBzYW0ubmFtZSA9IHBhcmFtcyRsaWJyYXJ5X2lkLCAKICAgICAgICBkaXN0YW5jZSA9IHgsIAogICAgICAgIHBsb3QuZ2VuZXMgPSBGQUxTRSwKICAgICAgICBuLmNvcmVzID0gcGFyYW1zJHRocmVhZHMKICAgICAgKQogICAgICAKICAgICAgIyBzYXZlIHByZWRpY3Rpb24gdG8gc2NlCiAgICAgIGNvbG5hbWUgPC0gZ2x1ZTo6Z2x1ZSgiY29weWthdF97eH0iKQogICAgICBjb2xEYXRhKHNjZSlbW2NvbG5hbWVdXSA8LSBjb3B5a2F0X3Jlc3VsdCRwcmVkaWN0aW9uJGNvcHlrYXQucHJlZAogICAgICAKICAgICAgIyBleHBvcnQgZnVsbCByZXN1bHQKICAgICAgb3V0cHV0X2ZpbGUgPC0gZmlsZS5wYXRoKG91dHB1dF9kaXIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGdsdWU6OmdsdWUoIntwYXJhbXMkbGlicmFyeX1fY29weWthdF97eH0ucmRzIikpCiAgICAgIHJlYWRyOjp3cml0ZV9yZHMoY29weWthdF9yZXN1bHQsIG91dHB1dF9maWxlKQogICAgfQogICkKICAKICAjIGZpbmFsbHksIGV4cG9ydCBTQ0Ugd2l0aCBhbGwgY29weWthdCBwcmVkaWN0aW9ucwogIHJlYWRyOjp3cml0ZV9yZHMoc2NlLCBjb3B5a2F0X3NjZSkKfSAKYGBgCgojIyBjb3B5S2F0IHJlc3VsdHMKCkZpcnN0LCB3ZSdsbCBsb29rIGF0IHF1aWNrIHRhYmxlcyBmb3IgY29ycmVzcG9uZGFuY2UgYmV0d2VlbiBzdWJtaXR0ZXIgY2VsbCB0eXBlIGFubm90YXRpb25zIGFuZCBgY29weUthdGAgaW5mZXJlbmNlcy4KRm9yIGBjb3B5S2F0YCwgImRpcGxvaWQiIGlzIG5vcm1hbCBhbmQgImFuZXVwbG9pZCIgaXMgdHVtb3IuCgpgYGB7cn0KdGFibGUoc2NlJGNlbGx0eXBlLCBzY2UkY29weWthdF9zcGVhcm1hbikKYGBgCgpgYGB7cn0KdGFibGUoc2NlJGNlbGx0eXBlLCBzY2UkY29weWthdF9wZWFyc29uKQpgYGAKCmBgYHtyfQp0YWJsZShzY2UkY2VsbHR5cGUsIHNjZSRjb3B5a2F0X2V1Y2xpZGlhbikKYGBgCgoKTmV4dCwgd2UnbGwgbG9vayBhdCBzb21lIGNsYXNzaWZpY2F0aW9uIG1ldHJpY3Mgb3ZlcmFsbCwgdXNpbmcgYSBjdXN0b20gZnVuY3Rpb24gdG8gY2FsY3VsYXRlIGEgY29uZnVzaW9uIG1hdHJpeC4KRm9yIHRoaXMsIHdlIHdpbGwgX29ubHkgY29uc2lkZXJfIGNlbGxzIHdoaWNoIGkpIHRoZSBzdWJtaXR0ZXIgd2FzIGFibGUgdG8gY2xhc3NpZnksIGFuZCBpaSkgYGNvcHlLYXRgIHdhcyBhYmxlIHRvIGNsYXNzaWZ5LgpJbiBvdGhlciB3b3JkcywgYWxsIGBOQWAgdmFsdWVzIGFyZSByZW1vdmVkIGZyb20gdGhlIGRhdGEgYmVmb3JlIGNhbGN1bGF0aW9ucy4KCgoKYGBge3J9CiMgQ3VzdG9tIGZ1bmN0aW9uIHRvIGNhbGN1bGF0ZSBzb21lIGNvbmZ1c2lvbiBtYXRyaXggcXVhbnRpdGllcywgaW5jbHVkaW5nCiMgYWNjdXJhY3ksIFRQUiwgYW5kIEZQUgpjYWxjdWxhdGVfY29uZnVzaW9uIDwtIGZ1bmN0aW9uKGRmLCB0ZXN0X2NvbHVtbikgewogICMgYXNzdW1lcyB0aGUgdHJ1ZSBjb2x1bW4gaXMgYGNlbGx0eXBlX2NsYXNzYAogIGRmIHw+CiAgICAjIHJlbW92ZSBOQSB2YWx1ZXMgYWxsIGFyb3VuZAogICAgdGlkeXI6OmRyb3BfbmEoY2VsbHR5cGVfY2xhc3MpIHw+CiAgICBkcGx5cjo6ZmlsdGVyKHt7dGVzdF9jb2x1bW59fSAhPSAibm90LmRlZmluZWQiKSB8PgogICAgIyBjb25mdXNpb24gbWF0cml4CiAgICBkcGx5cjo6bXV0YXRlKGNvbmZ1c2lvbiA9IGRwbHlyOjpjYXNlX3doZW4oCiAgICAgIGNlbGx0eXBlX2NsYXNzID09ICJUdW1vciIgJiB7e3Rlc3RfY29sdW1ufX0gPT0gImFuZXVwbG9pZCIgIH4gIlRQIiwKICAgICAgY2VsbHR5cGVfY2xhc3MgPT0gIlR1bW9yIiAmIHt7dGVzdF9jb2x1bW59fSA9PSAiZGlwbG9pZCIgICAgfiAiRk4iLAogICAgICBjZWxsdHlwZV9jbGFzcyA9PSAiTm9ybWFsIiAmIHt7dGVzdF9jb2x1bW59fSA9PSAiYW5ldXBsb2lkIiB+ICJGUCIsCiAgICAgIGNlbGx0eXBlX2NsYXNzID09ICJOb3JtYWwiICYge3t0ZXN0X2NvbHVtbn19ID09ICJkaXBsb2lkIiAgIH4gIlROIiwKICAgICAgVFJVRSB+IE5BX2NoYXJhY3Rlcl8sCiAgICApKSB8PgogICAgIyBqdXN0IGluIGNhc2UuLgogICAgdGlkeXI6OmRyb3BfbmEoY29uZnVzaW9uKSB8PgogICAgZHBseXI6OmNvdW50KGNvbmZ1c2lvbikgfD4KICAgIHRpZHlyOjpwaXZvdF93aWRlcigKICAgICAgbmFtZXNfZnJvbSA9ICJjb25mdXNpb24iLCAKICAgICAgdmFsdWVzX2Zyb20gPSAibiIKICAgICkgfD4KICAgICMgY2FsY3VsYXRlIHNvbWUgbWV0cmljcwogICAgZHBseXI6OnN1bW1hcml6ZShhY2N1cmFjeSA9IChUUCtUTikvKFRQK1ROK0ZQK0ZOKSwgCiAgICAgICAgICAgICAgICAgICAgIHRwciAgICAgID0gKFRQKS8oVFArRk4pLAogICAgICAgICAgICAgICAgICAgICBmcHIgICAgICA9IChGUCkvKEZQK1ROKSkgCn0KYGBgCgoKYGBge3J9CiMgQ3JlYXRlIGEgZGF0YSBmcmFtZSBmb3IgcGVyZm9ybWluZyBjYWxjdWxhdGlvbnMgb24KY29weWthdF9kZiA8LSBjb2xEYXRhKHNjZSkgfD4KICBhcy5kYXRhLmZyYW1lKCkgfD4KICBkcGx5cjo6c2VsZWN0KGNlbGx0eXBlLCBjb250YWlucygiY29weWthdCIpKSB8PgogIGRwbHlyOjptdXRhdGUoY2VsbHR5cGVfY2xhc3MgPSBpZmVsc2UoCiAgICBzdHJpbmdyOjpzdHJfc3RhcnRzKGNlbGx0eXBlLCAiVHVtb3IiKSwgCiAgICAiVHVtb3IiLCAKICAgICJOb3JtYWwiCiAgKSkgCgojIHJ1biBjb25mdXNpb24gbWF0cml4IGZ1bmN0aW9uCmNhdCgiPT09PT09PT09PT09PT0gU1BFQVJNQU4gZGlzdGFuY2UgPT09PT09PT09PT09PT09PT09PT09IikKY2FsY3VsYXRlX2NvbmZ1c2lvbihjb3B5a2F0X2RmLCBjb3B5a2F0X3NwZWFybWFuKQoKY2F0KCI9PT09PT09PT09PT09PSBQRUFSU09OIGRpc3RhbmNlID09PT09PT09PT09PT09PT09PT09PSIpCmNhbGN1bGF0ZV9jb25mdXNpb24oY29weWthdF9kZiwgY29weWthdF9wZWFyc29uKQoKY2F0KCI9PT09PT09PT09PT09PSBFVUNMSURJQU4gZGlzdGFuY2UgPT09PT09PT09PT09PT09PT09PT09IikKY2FsY3VsYXRlX2NvbmZ1c2lvbihjb3B5a2F0X2RmLCBjb3B5a2F0X2V1Y2xpZGlhbikKYGBgCgojIyBDb25jbHVzaW9ucwoKYGNvcHlLYXRgIGRvZXMgbm90IHNlZW0gdG8gYWdyZWUgd2l0aCB0aGUgc3VibWl0dGVyIGFubm90YXRpb25zLCBidXQgcG9zc2libHkgaGFzIHRoZSBiZXN0IGNoYW5jZSBvZiBzdWNjZXNzIHdpdGggb25lIG9mIHRoZSBjb3JyZWxhdGlvbmFsIGRpc3RhbmNlcyAoc3BlYXJtYW4gb3IgcGVhcnNvbikuCgpJdCBpcyBhbHNvIHdvcnRoIG5vdGluZyBzb21lIGRyYXdiYWNrcyBvZiB0aGUgYGNvcHlLYXRgIHBhY2thZ2U6CgoqIEl0IGVtaXRzIF9hIGxvdCBvZiBvdXRwdXRfLCBhbmQgd2UgZG9uJ3QgaGF2ZSBtdWNoIGNvbnRyb2wgb3ZlciBpdCEgCiAgKiBUaGlzIGluY2x1ZGVzIGJvdGggYHByaW50KClgIHN0YXRlbWVudHMgYW5kIG91dHB1dCBmaWxlcywgc29tZSBvZiB3aGljaCBhcmUgb3B0aW9uYWwgYnV0IG5vdCBhbGwuIAogICogVGhlIF9sb3RzIGFuZCBsb3RzIG9mIG91dHB1dF8gaXMgbm90IHNlZW4gaW4gdGhpcyBub3RlYm9vayBiZWNhdXNlIGl0IHdhcyByZW5kZXJlZCBhZnRlciBmaWxlcyBhbHJlYWR5IGV4aXN0ZWQ7IHRoZSBvdXRwdXQgX2Nhbm5vdF8gYmUgc3VwcHJlc3NlZCBieSB0dXJuaW5nIG9mZiB3YXJuaW5ncyBvciBtZXNzYWdlcywgYmVjYXVzZSB0aGV5IGFyZSBhbGwgcHJpbnQgc3RhdGVtZW50cy4gCiAgKiBPZiB0aGlzIG91dHB1dCwgd2UgZ2V0IGEgYCJjZWxsOiAjImAgcHJpbnRlZCBmb3IgZWFjaCBjZWxsLCBhbmQgd2UgYWxzbyBzZWUgdGhhdCB0aGUgbWFqb3JpdHkgYWxzbyBoYXZlIGEgcHJpbnRlZCBzdGF0ZW1lbnQ6IGAiV0FSTklORyEgTk9UIENPTlZFUkdFTlQhImAuCiAgVGhpcyBsYXR0ZXIgc3RhdGVtZW50IGlzIF9wcmludGVkXyBmcm9tIGEgW2RlcGVuZGVuY3ldKGh0dHBzOi8vZ2l0aHViLmNvbS9jcmFuL21peHRvb2xzL2Jsb2IvNGFmMWUyNzg5YmNlYTdkZjNjMTc3NWE1M2NkMDViMzdlYzMxODVkMC9SL25vcm1hbG1peEVNLlIjTDEyOS1MMTMxKQoqIE1vcmUgb24gdGhlIGxhY2sgb2YgY29udmVyZ2VuY2U6CiAgKiBgY29weUthdGAgaGFzIGhhcmRjb2RlZCB0aGUgbWF4aW11bSBudW1iZXIgb2YgaXRlcmF0aW9ucyB0byA1MDAsIGJ1dCB0aGUgZGVwZW5kZW5jeSBoYXMgdGhpcyBkZWZhdWx0IGF0IDEwMDAuIAogIEl0J3MgcG9zc2libGUgd2UnZCBzZWUgY29udmVyZ2VuY2Ugd2l0aCBtb3JlIGl0ZXJhdGlvbnMuCiAgKiBUaGlzIGNvbnZlcmdlbmNlIGluZm9ybWF0aW9uIGRvZXMgX25vdCBhcHBlYXIgdG8gYmUgY2FwdHVyZWRfIGluIGFueSBvZiB0aGUgb3V0cHV0IC0gaXQgaXMganVzdCBhIHByaW50IHN0YXRlbWVudCEKKiBgY29weUthdGAgW2hhcmRjb2RlcyBhIHJhbmRvbSBzZWVkXShodHRwczovL2dpdGh1Yi5jb20vbmF2aW5sYWJjb2RlL2NvcHlrYXQvYmxvYi9iNzk1ZmY3OTM1MjI0OTlmODE0ZjZhZTI4MmFhZDFhYWI3OTA5MDJmL1IvY29weWthdC5SI0wyOS1MMzIpIHNvIHdlIGNhbid0IHJlYWRpbHkgYXNzZXNzIGJpYXMgZnJvbSBzdGFydGluZyBjb25kaXRpb25zLgoKQmFzZWQgb24gdGhlc2UgZHJhd2JhY2tzLCBpZiB3ZSB3YW50IHRvIHVzZSBgY29weUthdGAgbW92aW5nIGZvcndhcmQsIHdlIG1pZ2h0IGNvbnNpZGVyIGZvcmtpbmcgdGhlIHJlcG9zaXRvcnkgYW5kIG1vZGlmeWluZyBhc3BlY3RzIHRoYXQgYmVzdCBzdWl0IG91ciBuZWVkczoKICAqIE1vcmUgY29udHJvbCBvdmVyIHRoZSBzZWVkCiAgKiBMZXNzIHByaW50aW5nLCB3aGVyZSBwb3NzaWJsZQogICogRmV3ZXIgb3V0cHV0IGZpbGVzCiAgKiBJbmNyZWFzaW5nIHRoZSBudW1iZXIgb2YgaXRlcmF0aW9ucyAob3IganVzdCByZW1vdmluZyBgY29weUthdGAncyA1MDAgbGltaXQgYW5kIGRlZmF1bHRpbmcgdG8gdGhlIGRlcGVuZGVuY3kncyAxMDAwKQogICogSWYgcG9zc2libGUsIHdlJ2QgbGlrZSB0byBfcmVjb3JkIGFuZCBleHBvcnRfIHdoaWNoIGNlbGxzIGRvIG5vdCBjb252ZXJnZS4gCiAgICAqIEVhc2llc3Qgd2F5IHRvIGdldCB0aGlzPyBSdW4gYGNvcHlLYXRgIGZyb20gYSBzY3JpcHQgYW5kIGNhcHR1cmUgYWxsIHRoZSBgc3Rkb3V0YCBpbnRvIGEgZmlsZSB3ZSBjYW4gcGFyc2UsIGJ1dCB0aGF0IHBhcnNpbmcgd2lsbCBub3QgbmVjZXNzYXJpbHkgYmUgcGxlYXNhbnQhCgoKIyMgU2Vzc2lvbiBJbmZvCgpgYGB7cn0Kc2Vzc2lvbkluZm8oKQpgYGAK
+ + +
+
+ +
+ + + + + + + + + + + + + + + + +