1 Overview

This notebook evaluates the quality of Harmony batch correction applied to malignant CD4⁺ T cells using the Local Inverse Simpson’s Index (LISI) framework (Korsunsky et al., 2019).

Two complementary LISI metrics are computed:

Metric Covariate Interpretation
cLISI seurat_clusters Cluster purity — values close to 1 indicate well-separated, pure clusters
iLISI (cell line) orig.ident Batch mixing across cell lines — higher values indicate better Harmony correction
iLISI (patient) Patient_origin Batch mixing across patients — higher values indicate better Harmony correction

2 Setup

2.1 Load libraries

# ── Install lisi if not already installed ──────────────────────────────────
# install.packages("remotes")
# remotes::install_github("immunogenomics/lisi")

library(lisi)
library(Seurat)
library(ggplot2)
library(dplyr)
library(tidyr)
library(patchwork)   # for combining plots

2.2 Read Seurat object

seurat_obj
An object of class Seurat 
62900 features across 49305 samples within 6 assays 
Active assay: RNA (36601 features, 0 variable features)
 2 layers present: data, counts
 5 other assays present: ADT, prediction.score.celltype.l1, prediction.score.celltype.l2, prediction.score.celltype.l3, SCT
 5 dimensional reductions calculated: integrated_dr, ref.umap, pca, umap, harmony
table(seurat_obj$Patient_origin)

      P1       P2       P3 CD4T_lab CD4T_10x 
   11760    12434    16501     5106     3504 
table(seurat_obj$orig.ident)

      L1       L2       L3       L4       L5       L6       L7 CD4T_lab CD4T_10x 
    5825     5935     6428     6006     6022     5148     5331     5106     3504 
table(seurat_obj$seurat_clusters)

   0    1    2    3    4    5    6    7    8    9   10   11   12   13 
6789 5275 4663 4661 4086 3634 3536 3409 3338 3273 3212 1675 1063  691 

3 Data Extraction

3.1 Extract Harmony embedding and metadata

# Harmony low-dimensional embedding (cells × components)
harmony_emb <- Embeddings(seurat_obj, reduction = "harmony")

# Metadata: cluster identity + two batch covariates
meta <- seurat_obj@meta.data[, c(
  "seurat_clusters",   # cluster identity  → cLISI
  "orig.ident",         # batch covariate 1 → iLISI
  "Patient_origin"     # batch covariate 2 → iLISI
)]

cat("Cells:", nrow(harmony_emb), "\n")
Cells: 49305 
cat("Harmony dimensions:", ncol(harmony_emb), "\n")
Harmony dimensions: 50 
cat("Clusters:", nlevels(factor(meta$seurat_clusters)), "\n")
Clusters: 14 
cat("Cell lines:", nlevels(factor(meta$orig.ident)), "\n")
Cell lines: 9 
cat("Patients:", nlevels(factor(meta$Patient_origin)), "\n")
Patients: 5 

4 LISI Computation

# compute_lisi returns one score per cell per label column
lisi_scores <- compute_lisi(
  X              = harmony_emb,
  meta_data      = meta,
  label_colnames = c("seurat_clusters", "orig.ident", "Patient_origin")
)

# Rename for clarity
colnames(lisi_scores) <- c("cLISI", "iLISI_orig.ident", "iLISI_Patient")

# Attach cluster label for downstream plotting
lisi_scores$cluster <- factor(seurat_obj@meta.data$seurat_clusters)

head(lisi_scores)

5 Summary Statistics

summary(lisi_scores[, c("cLISI", "iLISI_orig.ident", "iLISI_Patient")])
     cLISI       iLISI_orig.ident iLISI_Patient  
 Min.   :1.000   Min.   :1.000    Min.   :1.000  
 1st Qu.:1.002   1st Qu.:1.013    1st Qu.:1.000  
 Median :1.053   Median :1.140    Median :1.004  
 Mean   :1.284   Mean   :1.372    Mean   :1.166  
 3rd Qu.:1.376   3rd Qu.:1.563    3rd Qu.:1.148  
 Max.   :6.075   Max.   :6.376    Max.   :4.501  
# Per-cluster median cLISI
lisi_scores %>%
  group_by(cluster) %>%
  summarise(
    n          = n(),
    median_cLISI = round(median(cLISI), 3),
    mean_cLISI   = round(mean(cLISI),   3),
    sd_cLISI     = round(sd(cLISI),     3)
  ) %>%
  arrange(cluster)

6 Visualisation

6.1 Cluster Purity (cLISI) per cluster

Interpretation: Each box shows the distribution of cLISI scores for cells within that cluster. Values close to 1 indicate that each cell’s neighbourhood is predominantly composed of cells from the same cluster → high purity. Values approaching n_clusters indicate mixed neighbourhoods → low purity.

p_clisi <- ggplot(lisi_scores, aes(x = cluster, y = cLISI, fill = cluster)) +
  geom_boxplot(outlier.size = 0.4, outlier.alpha = 0.4,
               width = 0.65, show.legend = FALSE) +
  geom_hline(yintercept = 1, linetype = "dashed", colour = "firebrick",
             linewidth = 0.7) +
  annotate("text", x = Inf, y = 1.05, label = "Ideal purity (cLISI = 1)",
           hjust = 1.05, colour = "firebrick", size = 3.2) +
  scale_fill_viridis_d(option = "turbo", begin = 0.1, end = 0.9) +
  labs(
    title    = "Cluster Purity (cLISI) per Malignant CD4⁺ T Cell Cluster",
    subtitle = "Values close to 1 indicate high cluster purity",
    x        = "Seurat Cluster",
    y        = "cLISI Score"
  ) +
  theme_classic(base_size = 12) +
  theme(
    plot.title    = element_text(face = "bold", size = 13),
    plot.subtitle = element_text(colour = "grey45", size = 10),
    axis.text.x   = element_text(angle = 45, hjust = 1)
  )

p_clisi

6.2 Batch Mixing Quality (iLISI)

Interpretation: Higher iLISI values indicate that each cell’s neighbourhood contains cells from many different batches → Harmony has successfully mixed cells from different origins. The theoretical maximum equals the number of unique batch labels.

# Pivot to long format for faceted histogram
ilisi_long <- lisi_scores %>%
  select(iLISI_orig.ident, iLISI_Patient) %>%
  pivot_longer(
    cols      = everything(),
    names_to  = "covariate",
    values_to = "iLISI"
  ) %>%
  mutate(covariate = recode(covariate,
    "iLISI_orig.ident" = "Cell Line",
    "iLISI_Patient"   = "Patient Origin"
  ))

# Max possible iLISI per covariate (= number of unique labels)
n_orig.ident <- nlevels(factor(meta$orig.ident))
n_patient   <- nlevels(factor(meta$Patient_origin))

ref_df <- data.frame(
  covariate = c("Cell Line",   "Patient Origin"),
  max_lisi  = c(n_orig.ident,   n_patient)
)

p_ilisi <- ggplot(ilisi_long, aes(x = iLISI, fill = covariate)) +
  geom_histogram(bins = 30, colour = "white", show.legend = FALSE) +
  geom_vline(data = ref_df,
             aes(xintercept = max_lisi),
             linetype = "dashed", colour = "firebrick", linewidth = 0.7) +
  facet_wrap(~ covariate, scales = "free") +
  scale_fill_manual(values = c("Cell Line" = "steelblue",
                               "Patient Origin" = "darkorange")) +
  labs(
    title    = "Batch Mixing Quality (iLISI) after Harmony Correction",
    subtitle = "Dashed red line = theoretical maximum (= number of unique batch labels)",
    x        = "iLISI Score",
    y        = "Cell Count"
  ) +
  theme_classic(base_size = 12) +
  theme(
    plot.title    = element_text(face = "bold", size = 13),
    plot.subtitle = element_text(colour = "grey45", size = 10),
    strip.text    = element_text(face = "bold")
  )

p_ilisi

6.3 Combined QC Panel

p_clisi / p_ilisi +
  plot_annotation(
    title   = "Harmony Integration Quality — Malignant CD4⁺ T Cells",
    caption = "LISI computed on Harmony embedding | cLISI = cluster purity | iLISI = batch mixing",
    theme   = theme(
      plot.title   = element_text(face = "bold", size = 15),
      plot.caption = element_text(colour = "grey50", size = 9)
    )
  )


7 Interpretation Guide

trying URL 'https://cloud.r-project.org/src/contrib/kableExtra_1.4.0.tar.gz'
Content type 'application/x-gzip' length 1824636 bytes (1.7 MB)
==================================================
downloaded 1.7 MB


The downloaded source packages are in
    ‘/tmp/RtmpbsswCp/downloaded_packages’
Metric Ideal value Poor value Meaning
cLISI Close to 1 >> 1 (mixed clusters) Harmony preserves biologically distinct clusters
iLISI (cell line) Close to n_orig.idents Close to 1 (no mixing) Harmony successfully removes cell-line batch effects
iLISI (patient) Close to n_patients Close to 1 (no mixing) Harmony successfully removes patient-of-origin batch effects
trying URL 'https://cloud.r-project.org/src/contrib/DT_0.34.0.tar.gz'
Content type 'application/x-gzip' length 1664306 bytes (1.6 MB)
==================================================
downloaded 1.6 MB


The downloaded source packages are in
    ‘/tmp/RtmpbsswCp/downloaded_packages’
trying URL 'https://cloud.r-project.org/src/contrib/gt_1.3.0.tar.gz'
Content type 'application/x-gzip' length 3449540 bytes (3.3 MB)
==================================================
downloaded 3.3 MB


The downloaded source packages are in
    ‘/tmp/RtmpbsswCp/downloaded_packages’
Metric Ideal value Poor value Meaning
cLISI Close to 1 >> 1 (mixed clusters) Harmony preserves biologically distinct clusters
iLISI (cell line) Close to n_orig.idents Close to 1 (no mixing) Harmony successfully removes cell-line batch effects
iLISI (patient) Close to n_patients Close to 1 (no mixing) Harmony successfully removes patient-of-origin batch effects

8 To know if clusters 3 and 10 specifically are affected

# Stacked bar: composition of each cluster by source
ggplot(seurat_obj@meta.data,
       aes(x = factor(seurat_clusters), fill = orig.ident)) +
  geom_bar(position = "fill") +
  scale_y_continuous(labels = scales::percent) +
  labs(x = "Cluster", y = "Proportion", fill = "Source",
       title = "Source composition per cluster") +
  theme_classic()


9 Session Information

sessionInfo()
R version 4.5.2 (2025-10-31)
Platform: x86_64-pc-linux-gnu
Running under: Ubuntu 24.04.3 LTS

Matrix products: default
BLAS:   /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.12.0 
LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.12.0  LAPACK version 3.12.0

locale:
 [1] LC_CTYPE=en_GB.UTF-8       LC_NUMERIC=C               LC_TIME=fr_FR.UTF-8        LC_COLLATE=en_GB.UTF-8    
 [5] LC_MONETARY=fr_FR.UTF-8    LC_MESSAGES=en_GB.UTF-8    LC_PAPER=fr_FR.UTF-8       LC_NAME=C                 
 [9] LC_ADDRESS=C               LC_TELEPHONE=C             LC_MEASUREMENT=fr_FR.UTF-8 LC_IDENTIFICATION=C       

time zone: Europe/Paris
tzcode source: system (glibc)

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] patchwork_1.3.2    tidyr_1.3.2        dplyr_1.2.0        ggplot2_4.0.2      Seurat_5.4.0       SeuratObject_5.3.0
[7] sp_2.2-1           lisi_1.0          

loaded via a namespace (and not attached):
  [1] RColorBrewer_1.1-3     rstudioapi_0.18.0      jsonlite_2.0.0         magrittr_2.0.4         spatstat.utils_3.2-1  
  [6] farver_2.1.2           rmarkdown_2.30         fs_2.0.1               vctrs_0.7.1            ROCR_1.0-12           
 [11] memoise_2.0.1          spatstat.explore_3.7-0 htmltools_0.5.9        usethis_3.2.1          curl_7.0.0            
 [16] sass_0.4.10            sctransform_0.4.3      parallelly_1.46.1      KernSmooth_2.23-26     bslib_0.10.0          
 [21] htmlwidgets_1.6.4      desc_1.4.3             ica_1.0-3              plyr_1.8.9             plotly_4.12.0         
 [26] zoo_1.8-15             cachem_1.1.0           igraph_2.2.2           mime_0.13              lifecycle_1.0.5       
 [31] pkgconfig_2.0.3        Matrix_1.7-4           R6_2.6.1               fastmap_1.2.0          fitdistrplus_1.2-6    
 [36] future_1.69.0          shiny_1.13.0           digest_0.6.39          ps_1.9.1               tensor_1.5.1          
 [41] RSpectra_0.16-2        irlba_2.3.7            pkgload_1.5.0          labeling_0.4.3         progressr_0.18.0      
 [46] spatstat.sparse_3.1-0  httr_1.4.8             polyclip_1.10-7        abind_1.4-8            compiler_4.5.2        
 [51] remotes_2.5.0          withr_3.0.2            S7_0.2.1               fastDummies_1.7.5      pkgbuild_1.4.8        
 [56] MASS_7.3-65            sessioninfo_1.2.3      tools_4.5.2            lmtest_0.9-40          otel_0.2.0            
 [61] httpuv_1.6.16          future.apply_1.20.2    goftest_1.2-3          glue_1.8.0             callr_3.7.6           
 [66] nlme_3.1-168           promises_1.5.0         grid_4.5.2             rsconnect_1.7.0        Rtsne_0.17            
 [71] cluster_2.1.8.2        reshape2_1.4.5         generics_0.1.4         gtable_0.3.6           spatstat.data_3.1-9   
 [76] data.table_1.18.2.1    spatstat.geom_3.7-0    RcppAnnoy_0.0.23       ggrepel_0.9.7          RANN_2.6.2            
 [81] pillar_1.11.1          stringr_1.6.0          spam_2.11-3            RcppHNSW_0.6.0         later_1.4.7           
 [86] splines_4.5.2          lattice_0.22-9         survival_3.8-3         deldir_2.0-4           tidyselect_1.2.1      
 [91] miniUI_0.1.2           pbapply_1.7-4          knitr_1.51             gridExtra_2.3          scattermore_1.2       
 [96] xfun_0.56              devtools_2.5.0         matrixStats_1.5.0      stringi_1.8.7          lazyeval_0.2.2        
[101] yaml_2.3.12            evaluate_1.0.5         codetools_0.2-20       tibble_3.3.1           cli_3.6.5             
[106] uwot_0.2.4             xtable_1.8-8           reticulate_1.45.0      processx_3.8.6         jquerylib_0.1.4       
[111] dichromat_2.0-0.1      Rcpp_1.1.1             globals_0.19.0         spatstat.random_3.4-4  png_0.1-8             
[116] spatstat.univar_3.1-6  parallel_4.5.2         ellipsis_0.3.2         dotCall64_1.2          listenv_0.10.0        
[121] viridisLite_0.4.3      scales_1.4.0           ggridges_0.5.7         purrr_1.2.1            rlang_1.1.7           
[126] cowplot_1.2.0         
LS0tCnRpdGxlOiAiSGFybW9ueSBJbnRlZ3JhdGlvbiBRdWFsaXR5IEFzc2Vzc21lbnQgdmlhIExJU0kgaW4gTWFsaWduYW50IENENOKBuiBUIENlbGxzIgphdXRob3I6ICJOYXNpciBNYWhtb29kIEFiYmFzaSIKZGF0ZTogImByIGZvcm1hdChTeXMudGltZSgpLCAnJUIgJWQsICVZJylgIgpvdXRwdXQ6CiAgaHRtbF9ub3RlYm9vazoKICAgIG51bWJlcl9zZWN0aW9uczogdHJ1ZQogICAgdG9jOiB0cnVlCiAgICB0b2NfZmxvYXQ6CiAgICAgIGNvbGxhcHNlZDogdHJ1ZQogICAgdGhlbWU6IGpvdXJuYWwKLS0tCgpgYGB7ciBzZXR1cCwgaW5jbHVkZT1GQUxTRX0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KAogIGVjaG8gICAgPSBUUlVFLAogIG1lc3NhZ2UgPSBGQUxTRSwKICB3YXJuaW5nID0gRkFMU0UsCiAgZmlnLndpZHRoICA9IDgsCiAgZmlnLmhlaWdodCA9IDUsCiAgZHBpICAgICA9IDE1MAopCmBgYAoKIyBPdmVydmlldwoKVGhpcyBub3RlYm9vayBldmFsdWF0ZXMgdGhlIHF1YWxpdHkgb2YgKipIYXJtb255IGJhdGNoIGNvcnJlY3Rpb24qKiBhcHBsaWVkIHRvCm1hbGlnbmFudCBDRDTigbogVCBjZWxscyB1c2luZyB0aGUgKipMb2NhbCBJbnZlcnNlIFNpbXBzb24ncyBJbmRleCAoTElTSSkqKgpmcmFtZXdvcmsgWyhLb3JzdW5za3kgKmV0IGFsLiosIDIwMTkpXShodHRwczovL3d3dy5uYXR1cmUuY29tL2FydGljbGVzL3M0MTU5Mi0wMTktMDYxOS0wKS4KClR3byBjb21wbGVtZW50YXJ5IExJU0kgbWV0cmljcyBhcmUgY29tcHV0ZWQ6Cgp8IE1ldHJpYyB8IENvdmFyaWF0ZSB8IEludGVycHJldGF0aW9uIHwKfC0tLXwtLS18LS0tfAp8ICoqY0xJU0kqKiB8IGBzZXVyYXRfY2x1c3RlcnNgIHwgQ2x1c3RlciBwdXJpdHkg4oCUIHZhbHVlcyBjbG9zZSB0byAqKjEqKiBpbmRpY2F0ZSB3ZWxsLXNlcGFyYXRlZCwgcHVyZSBjbHVzdGVycyB8CnwgKippTElTSSAoY2VsbCBsaW5lKSoqIHwgYG9yaWcuaWRlbnRgIHwgQmF0Y2ggbWl4aW5nIGFjcm9zcyBjZWxsIGxpbmVzIOKAlCBoaWdoZXIgdmFsdWVzIGluZGljYXRlIGJldHRlciBIYXJtb255IGNvcnJlY3Rpb24gfAp8ICoqaUxJU0kgKHBhdGllbnQpKiogfCBgUGF0aWVudF9vcmlnaW5gIHwgQmF0Y2ggbWl4aW5nIGFjcm9zcyBwYXRpZW50cyDigJQgaGlnaGVyIHZhbHVlcyBpbmRpY2F0ZSBiZXR0ZXIgSGFybW9ueSBjb3JyZWN0aW9uIHwKCi0tLQoKIyBTZXR1cAoKIyMgTG9hZCBsaWJyYXJpZXMKCmBgYHtyIGxpYnJhcmllc30KIyDilIDilIAgSW5zdGFsbCBsaXNpIGlmIG5vdCBhbHJlYWR5IGluc3RhbGxlZCDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKIyBpbnN0YWxsLnBhY2thZ2VzKCJyZW1vdGVzIikKIyByZW1vdGVzOjppbnN0YWxsX2dpdGh1YigiaW1tdW5vZ2Vub21pY3MvbGlzaSIpCgpsaWJyYXJ5KGxpc2kpCmxpYnJhcnkoU2V1cmF0KQpsaWJyYXJ5KGdncGxvdDIpCmxpYnJhcnkoZHBseXIpCmxpYnJhcnkodGlkeXIpCmxpYnJhcnkocGF0Y2h3b3JrKSAgICMgZm9yIGNvbWJpbmluZyBwbG90cwpgYGAKCiMjIFJlYWQgU2V1cmF0IG9iamVjdCAKYGBge3IgbG9hZFNldXJhdH0KCkFsbF9zYW1wbGVzX01lcmdlZCA8LSByZWFkUkRTKCIuLi8wLVNldXJhdF9SRFNfRmluYWxfT0JKRUNUL0FsbF9zYW1wbGVzX01lcmdlZF93aXRoX1JlbmFtZWRfQ2x1c3RlcnNfQ2VsbF9zdGF0ZS0wMy0xMi0yMDI1LnJkcyIpCgpzZXVyYXRfb2JqIDwtIEFsbF9zYW1wbGVzX01lcmdlZApybShBbGxfc2FtcGxlc19NZXJnZWQpICAjIGZyZWUgbWVtb3J5CnNldXJhdF9vYmoKCnRhYmxlKHNldXJhdF9vYmokUGF0aWVudF9vcmlnaW4pCnRhYmxlKHNldXJhdF9vYmokb3JpZy5pZGVudCkKdGFibGUoc2V1cmF0X29iaiRzZXVyYXRfY2x1c3RlcnMpCgpgYGAKCi0tLQoKIyBEYXRhIEV4dHJhY3Rpb24KCiMjIEV4dHJhY3QgSGFybW9ueSBlbWJlZGRpbmcgYW5kIG1ldGFkYXRhCgpgYGB7ciBleHRyYWN0LWRhdGF9CiMgSGFybW9ueSBsb3ctZGltZW5zaW9uYWwgZW1iZWRkaW5nIChjZWxscyDDlyBjb21wb25lbnRzKQpoYXJtb255X2VtYiA8LSBFbWJlZGRpbmdzKHNldXJhdF9vYmosIHJlZHVjdGlvbiA9ICJoYXJtb255IikKCiMgTWV0YWRhdGE6IGNsdXN0ZXIgaWRlbnRpdHkgKyB0d28gYmF0Y2ggY292YXJpYXRlcwptZXRhIDwtIHNldXJhdF9vYmpAbWV0YS5kYXRhWywgYygKICAic2V1cmF0X2NsdXN0ZXJzIiwgICAjIGNsdXN0ZXIgaWRlbnRpdHkgIOKGkiBjTElTSQogICJvcmlnLmlkZW50IiwgICAgICAgICAjIGJhdGNoIGNvdmFyaWF0ZSAxIOKGkiBpTElTSQogICJQYXRpZW50X29yaWdpbiIgICAgICMgYmF0Y2ggY292YXJpYXRlIDIg4oaSIGlMSVNJCildCgpjYXQoIkNlbGxzOiIsIG5yb3coaGFybW9ueV9lbWIpLCAiXG4iKQpjYXQoIkhhcm1vbnkgZGltZW5zaW9uczoiLCBuY29sKGhhcm1vbnlfZW1iKSwgIlxuIikKY2F0KCJDbHVzdGVyczoiLCBubGV2ZWxzKGZhY3RvcihtZXRhJHNldXJhdF9jbHVzdGVycykpLCAiXG4iKQpjYXQoIkNlbGwgbGluZXM6IiwgbmxldmVscyhmYWN0b3IobWV0YSRvcmlnLmlkZW50KSksICJcbiIpCmNhdCgiUGF0aWVudHM6IiwgbmxldmVscyhmYWN0b3IobWV0YSRQYXRpZW50X29yaWdpbikpLCAiXG4iKQpgYGAKCi0tLQoKIyBMSVNJIENvbXB1dGF0aW9uCgpgYGB7ciBjb21wdXRlLWxpc2l9CiMgY29tcHV0ZV9saXNpIHJldHVybnMgb25lIHNjb3JlIHBlciBjZWxsIHBlciBsYWJlbCBjb2x1bW4KbGlzaV9zY29yZXMgPC0gY29tcHV0ZV9saXNpKAogIFggICAgICAgICAgICAgID0gaGFybW9ueV9lbWIsCiAgbWV0YV9kYXRhICAgICAgPSBtZXRhLAogIGxhYmVsX2NvbG5hbWVzID0gYygic2V1cmF0X2NsdXN0ZXJzIiwgIm9yaWcuaWRlbnQiLCAiUGF0aWVudF9vcmlnaW4iKQopCgojIFJlbmFtZSBmb3IgY2xhcml0eQpjb2xuYW1lcyhsaXNpX3Njb3JlcykgPC0gYygiY0xJU0kiLCAiaUxJU0lfb3JpZy5pZGVudCIsICJpTElTSV9QYXRpZW50IikKCiMgQXR0YWNoIGNsdXN0ZXIgbGFiZWwgZm9yIGRvd25zdHJlYW0gcGxvdHRpbmcKbGlzaV9zY29yZXMkY2x1c3RlciA8LSBmYWN0b3Ioc2V1cmF0X29iakBtZXRhLmRhdGEkc2V1cmF0X2NsdXN0ZXJzKQoKaGVhZChsaXNpX3Njb3JlcykKYGBgCgotLS0KCiMgU3VtbWFyeSBTdGF0aXN0aWNzCgpgYGB7ciBzdW1tYXJ5LXN0YXRzfQpzdW1tYXJ5KGxpc2lfc2NvcmVzWywgYygiY0xJU0kiLCAiaUxJU0lfb3JpZy5pZGVudCIsICJpTElTSV9QYXRpZW50IildKQpgYGAKCmBgYHtyIHN1bW1hcnktdGFibGV9CiMgUGVyLWNsdXN0ZXIgbWVkaWFuIGNMSVNJCmxpc2lfc2NvcmVzICU+JQogIGdyb3VwX2J5KGNsdXN0ZXIpICU+JQogIHN1bW1hcmlzZSgKICAgIG4gICAgICAgICAgPSBuKCksCiAgICBtZWRpYW5fY0xJU0kgPSByb3VuZChtZWRpYW4oY0xJU0kpLCAzKSwKICAgIG1lYW5fY0xJU0kgICA9IHJvdW5kKG1lYW4oY0xJU0kpLCAgIDMpLAogICAgc2RfY0xJU0kgICAgID0gcm91bmQoc2QoY0xJU0kpLCAgICAgMykKICApICU+JQogIGFycmFuZ2UoY2x1c3RlcikKYGBgCgotLS0KCiMgVmlzdWFsaXNhdGlvbgoKIyMgQ2x1c3RlciBQdXJpdHkgKGNMSVNJKSBwZXIgY2x1c3RlcgoKPiAqKkludGVycHJldGF0aW9uOioqIEVhY2ggYm94IHNob3dzIHRoZSBkaXN0cmlidXRpb24gb2YgY0xJU0kgc2NvcmVzIGZvciBjZWxscwo+IHdpdGhpbiB0aGF0IGNsdXN0ZXIuIFZhbHVlcyBjbG9zZSB0byAxIGluZGljYXRlIHRoYXQgZWFjaCBjZWxsJ3MgbmVpZ2hib3VyaG9vZAo+IGlzIHByZWRvbWluYW50bHkgY29tcG9zZWQgb2YgY2VsbHMgZnJvbSB0aGUgKnNhbWUqIGNsdXN0ZXIg4oaSIGhpZ2ggcHVyaXR5Lgo+IFZhbHVlcyBhcHByb2FjaGluZyAqbipfY2x1c3RlcnMgaW5kaWNhdGUgbWl4ZWQgbmVpZ2hib3VyaG9vZHMg4oaSIGxvdyBwdXJpdHkuCgpgYGB7ciBwbG90LWNMSVNJLCBmaWcud2lkdGg9OSwgZmlnLmhlaWdodD01fQpwX2NsaXNpIDwtIGdncGxvdChsaXNpX3Njb3JlcywgYWVzKHggPSBjbHVzdGVyLCB5ID0gY0xJU0ksIGZpbGwgPSBjbHVzdGVyKSkgKwogIGdlb21fYm94cGxvdChvdXRsaWVyLnNpemUgPSAwLjQsIG91dGxpZXIuYWxwaGEgPSAwLjQsCiAgICAgICAgICAgICAgIHdpZHRoID0gMC42NSwgc2hvdy5sZWdlbmQgPSBGQUxTRSkgKwogIGdlb21faGxpbmUoeWludGVyY2VwdCA9IDEsIGxpbmV0eXBlID0gImRhc2hlZCIsIGNvbG91ciA9ICJmaXJlYnJpY2siLAogICAgICAgICAgICAgbGluZXdpZHRoID0gMC43KSArCiAgYW5ub3RhdGUoInRleHQiLCB4ID0gSW5mLCB5ID0gMS4wNSwgbGFiZWwgPSAiSWRlYWwgcHVyaXR5IChjTElTSSA9IDEpIiwKICAgICAgICAgICBoanVzdCA9IDEuMDUsIGNvbG91ciA9ICJmaXJlYnJpY2siLCBzaXplID0gMy4yKSArCiAgc2NhbGVfZmlsbF92aXJpZGlzX2Qob3B0aW9uID0gInR1cmJvIiwgYmVnaW4gPSAwLjEsIGVuZCA9IDAuOSkgKwogIGxhYnMoCiAgICB0aXRsZSAgICA9ICJDbHVzdGVyIFB1cml0eSAoY0xJU0kpIHBlciBNYWxpZ25hbnQgQ0Q04oG6IFQgQ2VsbCBDbHVzdGVyIiwKICAgIHN1YnRpdGxlID0gIlZhbHVlcyBjbG9zZSB0byAxIGluZGljYXRlIGhpZ2ggY2x1c3RlciBwdXJpdHkiLAogICAgeCAgICAgICAgPSAiU2V1cmF0IENsdXN0ZXIiLAogICAgeSAgICAgICAgPSAiY0xJU0kgU2NvcmUiCiAgKSArCiAgdGhlbWVfY2xhc3NpYyhiYXNlX3NpemUgPSAxMikgKwogIHRoZW1lKAogICAgcGxvdC50aXRsZSAgICA9IGVsZW1lbnRfdGV4dChmYWNlID0gImJvbGQiLCBzaXplID0gMTMpLAogICAgcGxvdC5zdWJ0aXRsZSA9IGVsZW1lbnRfdGV4dChjb2xvdXIgPSAiZ3JleTQ1Iiwgc2l6ZSA9IDEwKSwKICAgIGF4aXMudGV4dC54ICAgPSBlbGVtZW50X3RleHQoYW5nbGUgPSA0NSwgaGp1c3QgPSAxKQogICkKCnBfY2xpc2kKYGBgCgojIyBCYXRjaCBNaXhpbmcgUXVhbGl0eSAoaUxJU0kpCgo+ICoqSW50ZXJwcmV0YXRpb246KiogSGlnaGVyIGlMSVNJIHZhbHVlcyBpbmRpY2F0ZSB0aGF0IGVhY2ggY2VsbCdzIG5laWdoYm91cmhvb2QKPiBjb250YWlucyBjZWxscyBmcm9tIG1hbnkgZGlmZmVyZW50IGJhdGNoZXMg4oaSIEhhcm1vbnkgaGFzIHN1Y2Nlc3NmdWxseSBtaXhlZAo+IGNlbGxzIGZyb20gZGlmZmVyZW50IG9yaWdpbnMuIFRoZSB0aGVvcmV0aWNhbCBtYXhpbXVtIGVxdWFscyB0aGUgbnVtYmVyIG9mCj4gdW5pcXVlIGJhdGNoIGxhYmVscy4KCmBgYHtyIHBsb3QtaUxJU0ktY29tYmluZWQsIGZpZy53aWR0aD0xMCwgZmlnLmhlaWdodD00LjV9CiMgUGl2b3QgdG8gbG9uZyBmb3JtYXQgZm9yIGZhY2V0ZWQgaGlzdG9ncmFtCmlsaXNpX2xvbmcgPC0gbGlzaV9zY29yZXMgJT4lCiAgc2VsZWN0KGlMSVNJX29yaWcuaWRlbnQsIGlMSVNJX1BhdGllbnQpICU+JQogIHBpdm90X2xvbmdlcigKICAgIGNvbHMgICAgICA9IGV2ZXJ5dGhpbmcoKSwKICAgIG5hbWVzX3RvICA9ICJjb3ZhcmlhdGUiLAogICAgdmFsdWVzX3RvID0gImlMSVNJIgogICkgJT4lCiAgbXV0YXRlKGNvdmFyaWF0ZSA9IHJlY29kZShjb3ZhcmlhdGUsCiAgICAiaUxJU0lfb3JpZy5pZGVudCIgPSAiQ2VsbCBMaW5lIiwKICAgICJpTElTSV9QYXRpZW50IiAgID0gIlBhdGllbnQgT3JpZ2luIgogICkpCgojIE1heCBwb3NzaWJsZSBpTElTSSBwZXIgY292YXJpYXRlICg9IG51bWJlciBvZiB1bmlxdWUgbGFiZWxzKQpuX29yaWcuaWRlbnQgPC0gbmxldmVscyhmYWN0b3IobWV0YSRvcmlnLmlkZW50KSkKbl9wYXRpZW50ICAgPC0gbmxldmVscyhmYWN0b3IobWV0YSRQYXRpZW50X29yaWdpbikpCgpyZWZfZGYgPC0gZGF0YS5mcmFtZSgKICBjb3ZhcmlhdGUgPSBjKCJDZWxsIExpbmUiLCAgICJQYXRpZW50IE9yaWdpbiIpLAogIG1heF9saXNpICA9IGMobl9vcmlnLmlkZW50LCAgIG5fcGF0aWVudCkKKQoKcF9pbGlzaSA8LSBnZ3Bsb3QoaWxpc2lfbG9uZywgYWVzKHggPSBpTElTSSwgZmlsbCA9IGNvdmFyaWF0ZSkpICsKICBnZW9tX2hpc3RvZ3JhbShiaW5zID0gMzAsIGNvbG91ciA9ICJ3aGl0ZSIsIHNob3cubGVnZW5kID0gRkFMU0UpICsKICBnZW9tX3ZsaW5lKGRhdGEgPSByZWZfZGYsCiAgICAgICAgICAgICBhZXMoeGludGVyY2VwdCA9IG1heF9saXNpKSwKICAgICAgICAgICAgIGxpbmV0eXBlID0gImRhc2hlZCIsIGNvbG91ciA9ICJmaXJlYnJpY2siLCBsaW5ld2lkdGggPSAwLjcpICsKICBmYWNldF93cmFwKH4gY292YXJpYXRlLCBzY2FsZXMgPSAiZnJlZSIpICsKICBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXMgPSBjKCJDZWxsIExpbmUiID0gInN0ZWVsYmx1ZSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiUGF0aWVudCBPcmlnaW4iID0gImRhcmtvcmFuZ2UiKSkgKwogIGxhYnMoCiAgICB0aXRsZSAgICA9ICJCYXRjaCBNaXhpbmcgUXVhbGl0eSAoaUxJU0kpIGFmdGVyIEhhcm1vbnkgQ29ycmVjdGlvbiIsCiAgICBzdWJ0aXRsZSA9ICJEYXNoZWQgcmVkIGxpbmUgPSB0aGVvcmV0aWNhbCBtYXhpbXVtICg9IG51bWJlciBvZiB1bmlxdWUgYmF0Y2ggbGFiZWxzKSIsCiAgICB4ICAgICAgICA9ICJpTElTSSBTY29yZSIsCiAgICB5ICAgICAgICA9ICJDZWxsIENvdW50IgogICkgKwogIHRoZW1lX2NsYXNzaWMoYmFzZV9zaXplID0gMTIpICsKICB0aGVtZSgKICAgIHBsb3QudGl0bGUgICAgPSBlbGVtZW50X3RleHQoZmFjZSA9ICJib2xkIiwgc2l6ZSA9IDEzKSwKICAgIHBsb3Quc3VidGl0bGUgPSBlbGVtZW50X3RleHQoY29sb3VyID0gImdyZXk0NSIsIHNpemUgPSAxMCksCiAgICBzdHJpcC50ZXh0ICAgID0gZWxlbWVudF90ZXh0KGZhY2UgPSAiYm9sZCIpCiAgKQoKcF9pbGlzaQpgYGAKCiMjIENvbWJpbmVkIFFDIFBhbmVsCgpgYGB7ciBjb21iaW5lZC1wYW5lbCwgZmlnLndpZHRoPTExLCBmaWcuaGVpZ2h0PTEwfQpwX2NsaXNpIC8gcF9pbGlzaSArCiAgcGxvdF9hbm5vdGF0aW9uKAogICAgdGl0bGUgICA9ICJIYXJtb255IEludGVncmF0aW9uIFF1YWxpdHkg4oCUIE1hbGlnbmFudCBDRDTigbogVCBDZWxscyIsCiAgICBjYXB0aW9uID0gIkxJU0kgY29tcHV0ZWQgb24gSGFybW9ueSBlbWJlZGRpbmcgfCBjTElTSSA9IGNsdXN0ZXIgcHVyaXR5IHwgaUxJU0kgPSBiYXRjaCBtaXhpbmciLAogICAgdGhlbWUgICA9IHRoZW1lKAogICAgICBwbG90LnRpdGxlICAgPSBlbGVtZW50X3RleHQoZmFjZSA9ICJib2xkIiwgc2l6ZSA9IDE1KSwKICAgICAgcGxvdC5jYXB0aW9uID0gZWxlbWVudF90ZXh0KGNvbG91ciA9ICJncmV5NTAiLCBzaXplID0gOSkKICAgICkKICApCmBgYAoKLS0tCgojIEludGVycHJldGF0aW9uIEd1aWRlCgpgYGB7ciBpbnRlcnAtdGFibGUsIGVjaG89RkFMU0V9CiMgT3B0aW9uIDEg4oCUIGtuaXRyOjprYWJsZSAoYWxyZWFkeSBpbiB5b3VyIGNodW5rLCB6ZXJvIGV4dHJhIGluc3RhbGxzKQojIEp1c3Qga25pdCB0aGUgZG9jdW1lbnQuIGthYmxlIHJlbmRlcnMgYXV0b21hdGljYWxseSBpbiBIVE1MIG5vdGVib29rcy4KCiMgT3B0aW9uIDIg4oCUIGthYmxlRXh0cmEgKG1vc3QgcG9wdWxhciwgYmVzdCBzdHlsaW5nIGNvbnRyb2wpCmluc3RhbGwucGFja2FnZXMoImthYmxlRXh0cmEiKQpsaWJyYXJ5KGthYmxlRXh0cmEpCgprbml0cjo6a2FibGUoaW50ZXJwLCBhbGlnbiA9ICJsbGxsIikgJT4lCiAga2FibGVfc3R5bGluZygKICAgIGJvb3RzdHJhcF9vcHRpb25zID0gYygic3RyaXBlZCIsICJob3ZlciIsICJjb25kZW5zZWQiKSwKICAgIGZ1bGxfd2lkdGggICAgICAgID0gRkFMU0UsCiAgICBmb250X3NpemUgICAgICAgICA9IDEzCiAgKSAlPiUKICBjb2x1bW5fc3BlYygxLCBib2xkID0gVFJVRSkgJT4lCiAgY29sdW1uX3NwZWMoNCwgd2lkdGggPSAiMjVlbSIpCgojIE9wdGlvbiAzIOKAlCBEVCAoaW50ZXJhY3RpdmUsIHNlYXJjaGFibGUsIHNvcnRhYmxlKQppbnN0YWxsLnBhY2thZ2VzKCJEVCIpCmxpYnJhcnkoRFQpCgpkYXRhdGFibGUoaW50ZXJwLCByb3duYW1lcyA9IEZBTFNFLCBvcHRpb25zID0gbGlzdChkb20gPSAidCIsIHBhZ2VMZW5ndGggPSAxMCkpCgojIE9wdGlvbiA0IOKAlCBndCAocHVibGljYXRpb24tcXVhbGl0eSwgdGlkeXZlcnNlLXN0eWxlKQppbnN0YWxsLnBhY2thZ2VzKCJndCIpCmxpYnJhcnkoZ3QpCgpndChpbnRlcnApICU+JQogIHRhYl9zdHlsZSgKICAgIHN0eWxlID0gY2VsbF90ZXh0KHdlaWdodCA9ICJib2xkIiksCiAgICBsb2NhdGlvbnMgPSBjZWxsc19jb2x1bW5fbGFiZWxzKCkKICApCmBgYAoKCiMgVG8ga25vdyBpZiBjbHVzdGVycyAzIGFuZCAxMCBzcGVjaWZpY2FsbHkgYXJlIGFmZmVjdGVkCgpgYGB7ciAsIGVjaG89RkFMU0V9CmxpYnJhcnkoZHBseXIpCgojIFdoYXQgZnJhY3Rpb24gb2YgZWFjaCBjbHVzdGVyIGNvbWVzIGZyb20gZWFjaCBzb3VyY2U/CnNldXJhdF9vYmpAbWV0YS5kYXRhICU+JQogIGdyb3VwX2J5KHNldXJhdF9jbHVzdGVycywgb3JpZy5pZGVudCkgJT4lCiAgc3VtbWFyaXNlKG4gPSBuKCksIC5ncm91cHMgPSAiZHJvcCIpICU+JQogIGdyb3VwX2J5KHNldXJhdF9jbHVzdGVycykgJT4lCiAgbXV0YXRlKHBjdCA9IHJvdW5kKDEwMCAqIG4gLyBzdW0obiksIDEpKSAlPiUKICBhcnJhbmdlKHNldXJhdF9jbHVzdGVycywgZGVzYyhwY3QpKQpgYGAKCmBgYHtyICwgZmlnLndpZHRoPTgsIGZpZy5oZWlnaHQ9NH0KIyBTdGFja2VkIGJhcjogY29tcG9zaXRpb24gb2YgZWFjaCBjbHVzdGVyIGJ5IHNvdXJjZQpnZ3Bsb3Qoc2V1cmF0X29iakBtZXRhLmRhdGEsCiAgICAgICBhZXMoeCA9IGZhY3RvcihzZXVyYXRfY2x1c3RlcnMpLCBmaWxsID0gb3JpZy5pZGVudCkpICsKICBnZW9tX2Jhcihwb3NpdGlvbiA9ICJmaWxsIikgKwogIHNjYWxlX3lfY29udGludW91cyhsYWJlbHMgPSBzY2FsZXM6OnBlcmNlbnQpICsKICBsYWJzKHggPSAiQ2x1c3RlciIsIHkgPSAiUHJvcG9ydGlvbiIsIGZpbGwgPSAiU291cmNlIiwKICAgICAgIHRpdGxlID0gIlNvdXJjZSBjb21wb3NpdGlvbiBwZXIgY2x1c3RlciIpICsKICB0aGVtZV9jbGFzc2ljKCkKYGBgCi0tLQoKIyBTZXNzaW9uIEluZm9ybWF0aW9uCgpgYGB7ciBzZXNzaW9uLWluZm99CnNlc3Npb25JbmZvKCkKYGBg