In this R Notebook we preprocess spatial and corresponding reference scRNA-seq data of human Melanoma for cell type deconvolution.

  1. Spatial data preprocessing:

    1.1 Input original data files

    1.2 Output data files for cell type deconvolution

  2. Reference scRNA-seq data preprocessing:

    2.1 Input original data files

    scRNA-seq data are downloaded from GSE115978.

    2.2 Output data files for cell type deconvolution

    We select 2,495 cells from 8 samples, and re-annotate cells with unknown cell type “?”. NO filtering on genes, i.e. all genes are included for analysis.

1 Version

version[['version.string']]
[1] "R version 4.2.2 Patched (2022-11-10 r83330)"

2 Preprocess Melanoma spatial dataset

2.1 Read original data file ST_mel1_rep2_counts.tsv

  • WARNING: gene with same Gene Symbol but mapping to 2 different Ensembl IDs are found! Total 19 genes are renamed!
file_name = file.path(home.dir, 'ST_mel1_rep2_counts.tsv')
org_data = read.csv(file_name, sep = '\t', check.names = F, header = T)
print(sprintf('load data from %s', file_name))
[1] "load data from /home/hill103/Documents/SharedFolder/ToHost/CVAE-GLRM_Analysis/RealData/Melanoma/ST_mel1_rep2_counts.tsv"
spot_names = colnames(org_data)[2:ncol(org_data)]

# extract first column as row name
org_data = org_data %>%
  tidyr::separate_wider_delim(gene, ' ', names = c('gene_name', NA), cols_remove = F)

org_data$duplicated = duplicated(org_data$gene_name)
duplicated_genes = org_data[org_data$duplicated, 'gene_name', drop=T]
org_data$gene_unique = make.unique(org_data$gene_name)
org_data$row = seq_len(nrow(org_data))

for_show = org_data[org_data$gene_name %in% duplicated_genes, c('row', 'gene', 'gene_unique')]
for_show[order(for_show$gene_unique), ]
gene_order = org_data$gene_unique

org_data = org_data[, spot_names]
org_data = as.data.frame(data.table::transpose(org_data))
Registered S3 method overwritten by 'data.table':
  method           from
  print.data.table     
row.names(org_data) = spot_names
colnames(org_data) = gene_order

print(sprintf('spots: %d; genes: %d', nrow(org_data), ncol(org_data)))
[1] "spots: 293; genes: 16148"
org_data[1:5, 1:5]

2.2 Save files for deconvolution

2.2.1 Spatial spot nUMI

No filtering on spots or genes, directly save all spots and genes into file Melanoma_spatial_spot_nUMI.csv. Rows as spatial spots and columns as genes.

write.csv(org_data, 'Melanoma_spatial_spot_nUMI.csv')
print(sprintf('save %d gene nUMIs of %d spatial spots into file %s', ncol(org_data), nrow(org_data), 'Melanoma_spatial_spot_nUMI.csv'))
[1] "save 16148 gene nUMIs of 293 spatial spots into file Melanoma_spatial_spot_nUMI.csv"

2.2.2 Physical Locations of spatial spots

Directly extract the spatial x and y coordinates from spot names, then save it into file Melanoma_spatial_spot_loc.csv.

local_df = data.frame(names = row.names(org_data), row.names = row.names(org_data))
local_df = local_df %>%
  tidyr::separate_wider_delim(names, 'x', names = c('y', 'x'))
local_df = as.data.frame(local_df)
row.names(local_df) = row.names(org_data)

local_df['x'] = as.numeric(local_df$x)
local_df['y'] = as.numeric(local_df$y)

local_df[1:5, ]

write.csv(local_df, 'Melanoma_spatial_spot_loc.csv')
print(sprintf('save Physical Locations of spatial spots into file %s', 'Melanoma_spatial_spot_loc.csv'))
[1] "save Physical Locations of spatial spots into file Melanoma_spatial_spot_loc.csv"

2.2.3 Adjacency Matrix of spatial spots

We define the neighborhood of a spatial spot contains the adjacent left, right, top and bottom spot, that is, one spot has at most 4 neighbors.

The generated Adjacency Matrix A only contains 1 and 0, where 1 represents corresponding two spots are adjacent spots according to the definition of neighborhood, while value 0 for non-adjacent spots. Note all diagonal entries are 0s.

Adjacency Matrix are saved into file Melanoma_spatial_spot_adjacency_matrix.csv.

getNeighbour = function(array_row, array_col) {
  # based on the (row, col) of one spot, return the (row, col) of all 4 neighbours
  return(list(c(array_row-1, array_col),
              c(array_row+1, array_col),
              c(array_row+0, array_col-1),
              c(array_row+0, array_col+1)))
}

# adjacency matrix
A = matrix(0, nrow = nrow(local_df), ncol = nrow(local_df))
row.names(A) = rownames(local_df)
colnames(A) = rownames(local_df)
for (i in 1:nrow(local_df)) {
  barcode = rownames(local_df)[i]
  array_row = local_df[i, 'y']
  array_col = local_df[i, 'x']
  
  # get neighbors
  neighbours = getNeighbour(array_row, array_col)
  
  # fill the adjacency matrix
  for (this.vec in neighbours) {
    tmp.p = rownames(local_df[local_df$y==this.vec[1] & local_df$x==this.vec[2], ])
    
    if (length(tmp.p) >= 1) {
      # target spots have neighbors in selected spots
      for (neigh.barcode in tmp.p) {
        A[barcode, neigh.barcode] = 1
      }
    }
  }
}

A[1:5, 1:5]
     7x15 7x16 7x17 7x18 8x13
7x15    0    1    0    0    0
7x16    1    0    1    0    0
7x17    0    1    0    1    0
7x18    0    0    1    0    0
8x13    0    0    0    0    0
write.csv(A, 'Melanoma_spatial_spot_adjacency_matrix.csv')
print(sprintf('save Adjacency Matrix of spatial spots into file %s', 'Melanoma_spatial_spot_adjacency_matrix.csv'))
[1] "save Adjacency Matrix of spatial spots into file Melanoma_spatial_spot_adjacency_matrix.csv"

Plot Adjacency Matrix. Each node is spot, spots within neighborhood are connected with edges.

g = graph_from_adjacency_matrix(A, 'undirected', add.colnames = NA, add.rownames = NA)
# manually set nodes x and y coordinates
vertex_attr(g, name = 'x') = local_df$x
vertex_attr(g, name = 'y') = local_df$y
plot(g, vertex.size=5, edge.width=4, margin=-0.05)

3 Proprocess reference scRNA-seq data

3.1 Read and preprocess scRNA-seq meta data

Original meta data file is GSE115978_cell.annotations.csv.gz downloaded from GSE115978.

It contains meta data of 7,186 cells from human melanoma tumors. Based on Table S1A. Clinical characteristics of the patients and samples in the scRNA-seq cohort, We select samples for cell type deconvolution by following criteria:

  1. Treatment: None;
  2. Lesion type: metastasis;
  3. Site: all kinds of lymph node.

8 samples (Mel79, Mel80, Mel81, Mel82, Mel89, Mel103, Mel116, Mel128) with 2,495 cells are selected.

The cell type annotation is stored in column cell.types, which includes total 10 distinct annotations.

We selected 7 cell types as below:

  1. malignant cells: “Mal”,
  2. T cells: “T.cell” + “T.CD4” + “T.CD8”
  3. B cells: “B.cell”
  4. natural killer (NK) cells: “NK”
  5. macrophages: “Macrophage”
  6. cancer-associated fibroblasts (CAFs): “CAF”
  7. endothelial cells: “Endo.”

We re-analysis the gene expression of selected 2,495 cells, cluster all cells into 10 clusters using TOP 5 PCs, and re-label the cells with unclear cell type “?” in each cluster as the dominate cell type of the cluster.

The refined cell type annotation of selected 2,495 cells is provided in Melanoma_ref_scRNA_cell_celltype.csv.

file_name = file.path(home.dir, 'Melanoma_ref_scRNA_cell_celltype.csv')
ref_meta = read.csv(file_name, sep=',', check.names = F, header = T, row.names = 1)
print(sprintf('load data from %s', file_name))
[1] "load data from /home/hill103/Documents/SharedFolder/ToHost/CVAE-GLRM_Analysis/RealData/Melanoma/Melanoma_ref_scRNA_cell_celltype.csv"
print(sprintf('total %d cells with distinct %d cell type annotations', nrow(ref_meta), length(unique(ref_meta$new_celltype))))
[1] "total 2495 cells with distinct 7 cell type annotations"
table(ref_meta$new_celltype)

    B.cell        CAF      Endo. Macrophage        Mal         NK     T.cell 
       364         48         34         62       1030         15        942 
ref_meta[1:5, 'new_celltype', drop=F]

3.2 Read and preprocess scRNA-seq nUMI data

Original gene nUMI count data file is GSE115978_counts.csv.gz downloaded from GSE115978. It contains total 7,186 cells and 23,686 genes.

We just selected 2,495 cells of the selected 7 cell types by barcodes, and discard other cells. NO filtering on genes, i.e. all 23,686 genes will be used for cell type deconvolution.

file_name = file.path(home.dir, 'GSE115978_counts.csv.gz')
ref_data = data.table::fread(file_name, sep = ",", check.names = FALSE)
gene_names = ref_data$V1
cell_names = colnames(ref_data)[2:ncol(ref_data)]

# transpose it
ref_data = as.data.frame(data.table::transpose(ref_data %>%
  select(cell_names)))

row.names(ref_data) = cell_names
colnames(ref_data) = gene_names

print(sprintf('load data from %s', file_name))
[1] "load data from /home/hill103/Documents/SharedFolder/ToHost/CVAE-GLRM_Analysis/RealData/Melanoma/GSE115978_counts.csv.gz"
print(sprintf('total cells: %d; genes: %d', nrow(ref_data), ncol(ref_data)))
[1] "total cells: 7186; genes: 23686"

Select cells and save scRNA-seq nUMI matrix to file Melanoma_ref_scRNA_cell_nUMI.csv.gz

ref_data = ref_data[row.names(ref_meta), ]

ref_data[1:5, 1:5]

data.table::fwrite(ref_data, 'Melanoma_ref_scRNA_cell_nUMI.csv.gz', row.names = T)
print(sprintf('save nUMI matrix of reference scRNA-seq cells into gzip compressed file %s', 'Melanoma_ref_scRNA_cell_nUMI.csv.gz'))
[1] "save nUMI matrix of reference scRNA-seq cells into gzip compressed file Melanoma_ref_scRNA_cell_nUMI.csv.gz"
LS0tCnRpdGxlOiAiUHJlcHJvY2VzcyBNZWxhbm9tYSBkYXRhIGZvciBjZWxsIHR5cGUgZGVjb252b2x1dGlvbiIKYXV0aG9yOiAiTmluZ3NoYW4gTGkgJiBOYXRpbmcgV2FuZyAmIFl1bnFpbmcgTGl1IgpkYXRlOiAiMjAyMy8wMy8yMSIKb3V0cHV0OiAKICBodG1sX25vdGVib29rOgogICAgY29kZV9mb2xkaW5nOiBoaWRlCiAgICBoaWdobGlnaHQ6IHRhbmdvCiAgICBudW1iZXJfc2VjdGlvbnM6IHllcwogICAgdGhlbWU6IHVuaXRlZAogICAgdG9jOiB5ZXMKICAgIHRvY19kZXB0aDogNgogICAgdG9jX2Zsb2F0OiB5ZXMKLS0tCgpgYGB7ciBzZXR1cCwgaW5jbHVkZT1GQUxTRX0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFLCBldmFsID0gVFJVRSwgd2FybmluZyA9IEZBTFNFLCBtZXNzYWdlID0gRkFMU0UsIHJlc3VsdHM9J2hvbGQnLCBmaWcud2lkdGggPSA3LCBmaWcuaGVpZ2h0ID0gNSwgZHBpID0gMzAwKQoKCmxpYnJhcnkoZHBseXIpCmxpYnJhcnkoZ2dwbG90MikKbGlicmFyeShpZ3JhcGgpCgpgJW5vdGluJWAgPSBOZWdhdGUoYCVpbiVgKQoKc2V0LnNlZWQoMSkKCmhvbWUuZGlyID0gJy9ob21lL2hpbGwxMDMvRG9jdW1lbnRzL1NoYXJlZEZvbGRlci9Ub0hvc3QvQ1ZBRS1HTFJNX0FuYWx5c2lzL1JlYWxEYXRhL01lbGFub21hJwoKCm15LmRpc3RpbmN0LmNvbG9yczIwID0gYygiI2U2MTk0YiIsICIjM2NiNDRiIiwgIiNmZmUxMTkiLCAiIzQzNjNkOCIsICIjZjU4MjMxIiwgIiM5MTFlYjQiLCAiIzQ2ZjBmMCIsICIjZjAzMmU2IiwgIiNiY2Y2MGMiLCAiI2ZhYmViZSIsICIjMDA4MDgwIiwgIiM5YTYzMjQiLCAiIzgwMDAwMCIsICIjYWFmZmMzIiwgIiM4MDgwMDAiLCAiIzAwMDA3NSIsICIjODA4MDgwIiwgIiNlNmJlZmYiLCAiI2ZmZDhiMSIsICIjMDAwMDAwIikKCm15LmRpc3RpbmN0LmNvbG9yczQwID0gYygiIzAwZmYwMCIsIiNmZjQ1MDAiLCIjMDBjZWQxIiwiIzU1NmIyZiIsIiNhMDUyMmQiLCIjOGIwMDAwIiwiIzgwODAwMCIsIiM0ODNkOGIiLCIjMDA4MDAwIiwiIzAwODA4MCIsIiM0NjgyYjQiLCIjMDAwMDgwIiwiIzlhY2QzMiIsIiNkYWE1MjAiLCIjN2YwMDdmIiwiIzhmYmM4ZiIsIiNiMDMwNjAiLCIjZDJiNDhjIiwiIzY5Njk2OSIsIiNmZjhjMDAiLCIjMDBmZjdmIiwiI2RjMTQzYyIsIiNmNGE0NjAiLCIjMDAwMGZmIiwiI2EwMjBmMCIsIiNhZGZmMmYiLCIjZmYwMGZmIiwiIzFlOTBmZiIsIiNmMGU2OGMiLCIjZmE4MDcyIiwiI2ZmZmY1NCIsIiNkZGEwZGQiLCIjODdjZWViIiwiIzdiNjhlZSIsIiNlZTgyZWUiLCIjOThmYjk4IiwiIzdmZmZkNCIsIiNmZmI2YzEiLCIjZGNkY2RjIiwiIzAwMDAwMCIpCmBgYAoKCkluIHRoaXMgUiBOb3RlYm9vayB3ZSBwcmVwcm9jZXNzIHNwYXRpYWwgYW5kIGNvcnJlc3BvbmRpbmcgcmVmZXJlbmNlIHNjUk5BLXNlcSBkYXRhIG9mIGh1bWFuICoqTWVsYW5vbWEqKiBmb3IgY2VsbCB0eXBlIGRlY29udm9sdXRpb24uCgoxLiAqKlNwYXRpYWwgZGF0YSBwcmVwcm9jZXNzaW5nKio6CgogICAgMS4xIElucHV0IG9yaWdpbmFsIGRhdGEgZmlsZXMKICAgIAogICAgKiBSYXcgblVNSSBvZiBzcGF0aWFsIHNwb3RzOiBgU1RfbWVsMV9yZXAyX2NvdW50cy50c3ZgIGlzIGZyb20gemlwcGVkIGZpbGUgW1NULU1lbGFub21hLURhdGFzZXRzXzEuemlwXShodHRwczovLzliMGNlMi5wM2NkbjEuc2VjdXJlc2VydmVyLm5ldC93cC1jb250ZW50L3VwbG9hZHMvMjAxOS8wMy9TVC1NZWxhbm9tYS1EYXRhc2V0c18xLnppcCkgZG93bmxvYWRlZCBmcm9tIFtTcGF0aWFsIFJlc2VhcmNoIHdlYnNpdGVdKGh0dHBzOi8vd3d3LnNwYXRpYWxyZXNlYXJjaC5vcmcvcmVzb3VyY2VzLXB1Ymxpc2hlZC1kYXRhc2V0cy9kb2ktMTAtMTE1OC0wMDA4LTU0NzItY2FuLTE4LTA3NDcvKSwgYW5kICoqc2Vjb25kIHJlcGxpY2F0ZSBmcm9tIGJpb3BzeSAxKiogaXMgc2VsZWN0ZWQgZm9yIGFuYWx5c2lzLgogICAgCiAgICAxLjIgT3V0cHV0IGRhdGEgZmlsZXMgZm9yIGNlbGwgdHlwZSBkZWNvbnZvbHV0aW9uCiAgICAKICAgICogUmF3IG5VTUkgb2Ygc3BhdGlhbCBzcG90czogW01lbGFub21hX3NwYXRpYWxfc3BvdF9uVU1JLmNzdl0oaHR0cHM6Ly9naXRodWIuY29tL2F6N2poMi9TRGVQRVJfQW5hbHlzaXMvYmxvYi9tYWluL1JlYWxEYXRhL01lbGFub21hL01lbGFub21hX3NwYXRpYWxfc3BvdF9uVU1JLmNzdikuICoqTm8gZmlsdGVyaW5nIG9uIHNwb3RzIG9yIGdlbmVzKiosIGkuZS4gYWxsIHNwb3RzIGFuZCBnZW5lcyBhcmUgcHJlc2VydmVkLgogICAgKiBQaHlzaWNhbCBsb2NhdGlvbiBvZiBzcGF0aWFsIHNwb3RzOiBbTWVsYW5vbWFfc3BhdGlhbF9zcG90X2xvYy5jc3ZdKGh0dHBzOi8vZ2l0aHViLmNvbS9hejdqaDIvU0RlUEVSX0FuYWx5c2lzL2Jsb2IvbWFpbi9SZWFsRGF0YS9NZWxhbm9tYS9NZWxhbm9tYV9zcGF0aWFsX3Nwb3RfbG9jLmNzdikuIFRoZSBzcGF0aWFsIGB4YCBhbmQgYHlgIGNvb3JkaW5hdGVzIGFyZSBkaXJlY3RseSBleHRyYWN0ZWQgZnJvbSBzcG90IG5hbWVzLgogICAgKiBBZGphY2VuY3kgTWF0cml4OiBbTWVsYW5vbWFfc3BhdGlhbF9zcG90X2FkamFjZW5jeV9tYXRyaXguY3N2XShodHRwczovL2dpdGh1Yi5jb20vYXo3amgyL1NEZVBFUl9BbmFseXNpcy9ibG9iL21haW4vUmVhbERhdGEvTWVsYW5vbWEvTWVsYW5vbWFfc3BhdGlhbF9zcG90X2FkamFjZW5jeV9tYXRyaXguY3N2KS4gU3BvdHMgd2l0aGluIG5laWdoYm9yaG9vZCBhcmUgYWRqYWNlbnQgKipsZWZ0KiosICoqcmlnaHQqKiwgKip0b3AqKiBhbmQgKipib3R0b20qKiBzcG90cy4KCgoyLiAqKlJlZmVyZW5jZSBzY1JOQS1zZXEgZGF0YSBwcmVwcm9jZXNzaW5nKio6CgogICAgMi4xIElucHV0IG9yaWdpbmFsIGRhdGEgZmlsZXMKICAgIAogICAgc2NSTkEtc2VxIGRhdGEgYXJlIGRvd25sb2FkZWQgZnJvbSBbR1NFMTE1OTc4XShodHRwczovL3d3dy5uY2JpLm5sbS5uaWguZ292L2dlby9xdWVyeS9hY2MuY2dpP2FjYz1HU0UxMTU5NzgpLgogICAgCiAgICAqIFJhdyBuVU1JIG9mIGFsbCA3LDE4NiBzaW5nbGUgY2VsbHM6ICBbR1NFMTE1OTc4X2NvdW50cy5jc3YuZ3pdKGh0dHBzOi8vd3d3Lm5jYmkubmxtLm5paC5nb3YvZ2VvL2Rvd25sb2FkLz9hY2M9R1NFMTE1OTc4JmZvcm1hdD1maWxlJmZpbGU9R1NFMTE1OTc4JTVGY291bnRzJTJFY3N2JTJFZ3opIAogICAgCiAgICAqIE1ldGEgZGF0YSBvZiBhbGwgNywxODYgY2VsbHM6IFtHU0UxMTU5NzhfY2VsbC5hbm5vdGF0aW9ucy5jc3YuZ3pdKGh0dHBzOi8vd3d3Lm5jYmkubmxtLm5paC5nb3YvZ2VvL2Rvd25sb2FkLz9hY2M9R1NFMTE1OTc4JmZvcm1hdD1maWxlJmZpbGU9R1NFMTE1OTc4JTVGY2VsbCUyRWFubm90YXRpb25zJTJFY3N2JTJFZ3opCiAgICAKICAgIDIuMiBPdXRwdXQgZGF0YSBmaWxlcyBmb3IgY2VsbCB0eXBlIGRlY29udm9sdXRpb24KICAgIAogICAgV2Ugc2VsZWN0ICoqMiw0OTUqKiBjZWxscyBmcm9tIDggc2FtcGxlcywgYW5kIHJlLWFubm90YXRlIGNlbGxzIHdpdGggdW5rbm93biBjZWxsIHR5cGUgIj8iLiAqKk5PIGZpbHRlcmluZyBvbiBnZW5lcyoqLCBpLmUuIGFsbCBnZW5lcyBhcmUgaW5jbHVkZWQgZm9yIGFuYWx5c2lzLgogICAgCiAgICAqIFJhdyBuVU1JIG9mIDIsNDk1IGNlbGxzIHdpdGggNyBjZWxsIHR5cGVzIGFuZCAyMyw2ODYgZ2VuZXM6IFtNZWxhbm9tYV9yZWZfc2NSTkFfY2VsbF9uVU1JLmNzdi5nel0oaHR0cHM6Ly9naXRodWIuY29tL2F6N2poMi9TRGVQRVJfQW5hbHlzaXMvYmxvYi9tYWluL1JlYWxEYXRhL01lbGFub21hL01lbGFub21hX3JlZl9zY1JOQV9jZWxsX25VTUkuY3N2Lmd6KS4KICAgIAogICAgKiBDZWxsIHR5cGUgYW5ub3RhdGlvbiBmb3IgdGhvc2UgMiw0OTUgY2VsbHM6IFtNZWxhbm9tYV9yZWZfc2NSTkFfY2VsbF9jZWxsdHlwZS5jc3ZdKGh0dHBzOi8vZ2l0aHViLmNvbS9hejdqaDIvU0RlUEVSX0FuYWx5c2lzL2Jsb2IvbWFpbi9SZWFsRGF0YS9NZWxhbm9tYS9NZWxhbm9tYV9yZWZfc2NSTkFfY2VsbF9jZWxsdHlwZS5jc3YpLgoKCgojIFZlcnNpb24KCmBgYHtyfQp2ZXJzaW9uW1sndmVyc2lvbi5zdHJpbmcnXV0KYGBgCgoKIyBQcmVwcm9jZXNzIE1lbGFub21hIHNwYXRpYWwgZGF0YXNldAoKIyMgUmVhZCBvcmlnaW5hbCBkYXRhIGZpbGUgYFNUX21lbDFfcmVwMl9jb3VudHMudHN2YAoKKiBXQVJOSU5HOiBnZW5lIHdpdGggc2FtZSBHZW5lIFN5bWJvbCBidXQgbWFwcGluZyB0byAyIGRpZmZlcmVudCBFbnNlbWJsIElEcyBhcmUgZm91bmQhIFRvdGFsIDE5IGdlbmVzIGFyZSByZW5hbWVkIQoKYGBge3J9CmZpbGVfbmFtZSA9IGZpbGUucGF0aChob21lLmRpciwgJ1NUX21lbDFfcmVwMl9jb3VudHMudHN2JykKb3JnX2RhdGEgPSByZWFkLmNzdihmaWxlX25hbWUsIHNlcCA9ICdcdCcsIGNoZWNrLm5hbWVzID0gRiwgaGVhZGVyID0gVCkKcHJpbnQoc3ByaW50ZignbG9hZCBkYXRhIGZyb20gJXMnLCBmaWxlX25hbWUpKQoKc3BvdF9uYW1lcyA9IGNvbG5hbWVzKG9yZ19kYXRhKVsyOm5jb2wob3JnX2RhdGEpXQoKIyBleHRyYWN0IGZpcnN0IGNvbHVtbiBhcyByb3cgbmFtZQpvcmdfZGF0YSA9IG9yZ19kYXRhICU+JQogIHRpZHlyOjpzZXBhcmF0ZV93aWRlcl9kZWxpbShnZW5lLCAnICcsIG5hbWVzID0gYygnZ2VuZV9uYW1lJywgTkEpLCBjb2xzX3JlbW92ZSA9IEYpCgpvcmdfZGF0YSRkdXBsaWNhdGVkID0gZHVwbGljYXRlZChvcmdfZGF0YSRnZW5lX25hbWUpCmR1cGxpY2F0ZWRfZ2VuZXMgPSBvcmdfZGF0YVtvcmdfZGF0YSRkdXBsaWNhdGVkLCAnZ2VuZV9uYW1lJywgZHJvcD1UXQpvcmdfZGF0YSRnZW5lX3VuaXF1ZSA9IG1ha2UudW5pcXVlKG9yZ19kYXRhJGdlbmVfbmFtZSkKb3JnX2RhdGEkcm93ID0gc2VxX2xlbihucm93KG9yZ19kYXRhKSkKCmZvcl9zaG93ID0gb3JnX2RhdGFbb3JnX2RhdGEkZ2VuZV9uYW1lICVpbiUgZHVwbGljYXRlZF9nZW5lcywgYygncm93JywgJ2dlbmUnLCAnZ2VuZV91bmlxdWUnKV0KZm9yX3Nob3dbb3JkZXIoZm9yX3Nob3ckZ2VuZV91bmlxdWUpLCBdCmBgYAoKCmBgYHtyfQpnZW5lX29yZGVyID0gb3JnX2RhdGEkZ2VuZV91bmlxdWUKCm9yZ19kYXRhID0gb3JnX2RhdGFbLCBzcG90X25hbWVzXQpvcmdfZGF0YSA9IGFzLmRhdGEuZnJhbWUoZGF0YS50YWJsZTo6dHJhbnNwb3NlKG9yZ19kYXRhKSkKCnJvdy5uYW1lcyhvcmdfZGF0YSkgPSBzcG90X25hbWVzCmNvbG5hbWVzKG9yZ19kYXRhKSA9IGdlbmVfb3JkZXIKCnByaW50KHNwcmludGYoJ3Nwb3RzOiAlZDsgZ2VuZXM6ICVkJywgbnJvdyhvcmdfZGF0YSksIG5jb2wob3JnX2RhdGEpKSkKb3JnX2RhdGFbMTo1LCAxOjVdCmBgYAoKCiMjIFNhdmUgZmlsZXMgZm9yIGRlY29udm9sdXRpb24KCiMjIyBTcGF0aWFsIHNwb3QgblVNSQoKKipObyBmaWx0ZXJpbmcgb24gc3BvdHMgb3IgZ2VuZXMqKiwgZGlyZWN0bHkgc2F2ZSBhbGwgc3BvdHMgYW5kIGdlbmVzIGludG8gZmlsZSBbTWVsYW5vbWFfc3BhdGlhbF9zcG90X25VTUkuY3N2XShodHRwczovL2dpdGh1Yi5jb20vYXo3amgyL1NEZVBFUl9BbmFseXNpcy9ibG9iL21haW4vUmVhbERhdGEvTWVsYW5vbWEvTWVsYW5vbWFfc3BhdGlhbF9zcG90X25VTUkuY3N2KS4gKipSb3dzIGFzIHNwYXRpYWwgc3BvdHMgYW5kIGNvbHVtbnMgYXMgZ2VuZXMqKi4KCmBgYHtyfQp3cml0ZS5jc3Yob3JnX2RhdGEsICdNZWxhbm9tYV9zcGF0aWFsX3Nwb3RfblVNSS5jc3YnKQpwcmludChzcHJpbnRmKCdzYXZlICVkIGdlbmUgblVNSXMgb2YgJWQgc3BhdGlhbCBzcG90cyBpbnRvIGZpbGUgJXMnLCBuY29sKG9yZ19kYXRhKSwgbnJvdyhvcmdfZGF0YSksICdNZWxhbm9tYV9zcGF0aWFsX3Nwb3RfblVNSS5jc3YnKSkKYGBgCgoKIyMjIFBoeXNpY2FsIExvY2F0aW9ucyBvZiBzcGF0aWFsIHNwb3RzCgpEaXJlY3RseSBleHRyYWN0IHRoZSBzcGF0aWFsIGB4YCBhbmQgYHlgIGNvb3JkaW5hdGVzIGZyb20gc3BvdCBuYW1lcywgdGhlbiBzYXZlIGl0IGludG8gZmlsZSBbTWVsYW5vbWFfc3BhdGlhbF9zcG90X2xvYy5jc3ZdKGh0dHBzOi8vZ2l0aHViLmNvbS9hejdqaDIvU0RlUEVSX0FuYWx5c2lzL2Jsb2IvbWFpbi9SZWFsRGF0YS9NZWxhbm9tYS9NZWxhbm9tYV9zcGF0aWFsX3Nwb3RfbG9jLmNzdikuCgoKYGBge3J9CmxvY2FsX2RmID0gZGF0YS5mcmFtZShuYW1lcyA9IHJvdy5uYW1lcyhvcmdfZGF0YSksIHJvdy5uYW1lcyA9IHJvdy5uYW1lcyhvcmdfZGF0YSkpCmxvY2FsX2RmID0gbG9jYWxfZGYgJT4lCiAgdGlkeXI6OnNlcGFyYXRlX3dpZGVyX2RlbGltKG5hbWVzLCAneCcsIG5hbWVzID0gYygneScsICd4JykpCmxvY2FsX2RmID0gYXMuZGF0YS5mcmFtZShsb2NhbF9kZikKcm93Lm5hbWVzKGxvY2FsX2RmKSA9IHJvdy5uYW1lcyhvcmdfZGF0YSkKCmxvY2FsX2RmWyd4J10gPSBhcy5udW1lcmljKGxvY2FsX2RmJHgpCmxvY2FsX2RmWyd5J10gPSBhcy5udW1lcmljKGxvY2FsX2RmJHkpCgpsb2NhbF9kZlsxOjUsIF0KCndyaXRlLmNzdihsb2NhbF9kZiwgJ01lbGFub21hX3NwYXRpYWxfc3BvdF9sb2MuY3N2JykKcHJpbnQoc3ByaW50Zignc2F2ZSBQaHlzaWNhbCBMb2NhdGlvbnMgb2Ygc3BhdGlhbCBzcG90cyBpbnRvIGZpbGUgJXMnLCAnTWVsYW5vbWFfc3BhdGlhbF9zcG90X2xvYy5jc3YnKSkKYGBgCgoKIyMjIEFkamFjZW5jeSBNYXRyaXggb2Ygc3BhdGlhbCBzcG90cwoKV2UgZGVmaW5lIHRoZSBuZWlnaGJvcmhvb2Qgb2YgYSBzcGF0aWFsIHNwb3QgY29udGFpbnMgdGhlIGFkamFjZW50ICoqbGVmdCoqLCAqKnJpZ2h0KiosICoqdG9wKiogYW5kICoqYm90dG9tKiogc3BvdCwgdGhhdCBpcywgb25lIHNwb3QgaGFzIGF0IG1vc3QgNCBuZWlnaGJvcnMuCgpUaGUgZ2VuZXJhdGVkIEFkamFjZW5jeSBNYXRyaXggYEFgIG9ubHkgY29udGFpbnMgKioxKiogYW5kICoqMCoqLCB3aGVyZSAxIHJlcHJlc2VudHMgY29ycmVzcG9uZGluZyB0d28gc3BvdHMgYXJlIGFkamFjZW50IHNwb3RzIGFjY29yZGluZyB0byB0aGUgZGVmaW5pdGlvbiBvZiBuZWlnaGJvcmhvb2QsIHdoaWxlIHZhbHVlIDAgZm9yIG5vbi1hZGphY2VudCBzcG90cy4gTm90ZSAqKmFsbCBkaWFnb25hbCBlbnRyaWVzIGFyZSAwcyoqLgoKQWRqYWNlbmN5IE1hdHJpeCBhcmUgc2F2ZWQgaW50byBmaWxlIFtNZWxhbm9tYV9zcGF0aWFsX3Nwb3RfYWRqYWNlbmN5X21hdHJpeC5jc3ZdKGh0dHBzOi8vZ2l0aHViLmNvbS9hejdqaDIvU0RlUEVSX0FuYWx5c2lzL2Jsb2IvbWFpbi9SZWFsRGF0YS9NZWxhbm9tYS9NZWxhbm9tYV9zcGF0aWFsX3Nwb3RfYWRqYWNlbmN5X21hdHJpeC5jc3YpLgoKYGBge3J9CmdldE5laWdoYm91ciA9IGZ1bmN0aW9uKGFycmF5X3JvdywgYXJyYXlfY29sKSB7CiAgIyBiYXNlZCBvbiB0aGUgKHJvdywgY29sKSBvZiBvbmUgc3BvdCwgcmV0dXJuIHRoZSAocm93LCBjb2wpIG9mIGFsbCA0IG5laWdoYm91cnMKICByZXR1cm4obGlzdChjKGFycmF5X3Jvdy0xLCBhcnJheV9jb2wpLAogICAgICAgICAgICAgIGMoYXJyYXlfcm93KzEsIGFycmF5X2NvbCksCiAgICAgICAgICAgICAgYyhhcnJheV9yb3crMCwgYXJyYXlfY29sLTEpLAogICAgICAgICAgICAgIGMoYXJyYXlfcm93KzAsIGFycmF5X2NvbCsxKSkpCn0KCiMgYWRqYWNlbmN5IG1hdHJpeApBID0gbWF0cml4KDAsIG5yb3cgPSBucm93KGxvY2FsX2RmKSwgbmNvbCA9IG5yb3cobG9jYWxfZGYpKQpyb3cubmFtZXMoQSkgPSByb3duYW1lcyhsb2NhbF9kZikKY29sbmFtZXMoQSkgPSByb3duYW1lcyhsb2NhbF9kZikKZm9yIChpIGluIDE6bnJvdyhsb2NhbF9kZikpIHsKICBiYXJjb2RlID0gcm93bmFtZXMobG9jYWxfZGYpW2ldCiAgYXJyYXlfcm93ID0gbG9jYWxfZGZbaSwgJ3knXQogIGFycmF5X2NvbCA9IGxvY2FsX2RmW2ksICd4J10KICAKICAjIGdldCBuZWlnaGJvcnMKICBuZWlnaGJvdXJzID0gZ2V0TmVpZ2hib3VyKGFycmF5X3JvdywgYXJyYXlfY29sKQogIAogICMgZmlsbCB0aGUgYWRqYWNlbmN5IG1hdHJpeAogIGZvciAodGhpcy52ZWMgaW4gbmVpZ2hib3VycykgewogICAgdG1wLnAgPSByb3duYW1lcyhsb2NhbF9kZltsb2NhbF9kZiR5PT10aGlzLnZlY1sxXSAmIGxvY2FsX2RmJHg9PXRoaXMudmVjWzJdLCBdKQogICAgCiAgICBpZiAobGVuZ3RoKHRtcC5wKSA+PSAxKSB7CiAgICAgICMgdGFyZ2V0IHNwb3RzIGhhdmUgbmVpZ2hib3JzIGluIHNlbGVjdGVkIHNwb3RzCiAgICAgIGZvciAobmVpZ2guYmFyY29kZSBpbiB0bXAucCkgewogICAgICAgIEFbYmFyY29kZSwgbmVpZ2guYmFyY29kZV0gPSAxCiAgICAgIH0KICAgIH0KICB9Cn0KCkFbMTo1LCAxOjVdCndyaXRlLmNzdihBLCAnTWVsYW5vbWFfc3BhdGlhbF9zcG90X2FkamFjZW5jeV9tYXRyaXguY3N2JykKcHJpbnQoc3ByaW50Zignc2F2ZSBBZGphY2VuY3kgTWF0cml4IG9mIHNwYXRpYWwgc3BvdHMgaW50byBmaWxlICVzJywgJ01lbGFub21hX3NwYXRpYWxfc3BvdF9hZGphY2VuY3lfbWF0cml4LmNzdicpKQpgYGAKClBsb3QgQWRqYWNlbmN5IE1hdHJpeC4gRWFjaCBub2RlIGlzIHNwb3QsIHNwb3RzIHdpdGhpbiBuZWlnaGJvcmhvb2QgYXJlIGNvbm5lY3RlZCB3aXRoIGVkZ2VzLgoKYGBge3IsIGZpZy53aWR0aD0xMiwgZmlnLmhlaWdodD0xMn0KZyA9IGdyYXBoX2Zyb21fYWRqYWNlbmN5X21hdHJpeChBLCAndW5kaXJlY3RlZCcsIGFkZC5jb2xuYW1lcyA9IE5BLCBhZGQucm93bmFtZXMgPSBOQSkKIyBtYW51YWxseSBzZXQgbm9kZXMgeCBhbmQgeSBjb29yZGluYXRlcwp2ZXJ0ZXhfYXR0cihnLCBuYW1lID0gJ3gnKSA9IGxvY2FsX2RmJHgKdmVydGV4X2F0dHIoZywgbmFtZSA9ICd5JykgPSBsb2NhbF9kZiR5CnBsb3QoZywgdmVydGV4LnNpemU9NSwgZWRnZS53aWR0aD00LCBtYXJnaW49LTAuMDUpCmBgYAoKCiMgUHJvcHJvY2VzcyByZWZlcmVuY2Ugc2NSTkEtc2VxIGRhdGEKCiMjIFJlYWQgYW5kIHByZXByb2Nlc3Mgc2NSTkEtc2VxIG1ldGEgZGF0YQoKT3JpZ2luYWwgbWV0YSBkYXRhIGZpbGUgaXMgW0dTRTExNTk3OF9jZWxsLmFubm90YXRpb25zLmNzdi5nel0oaHR0cHM6Ly93d3cubmNiaS5ubG0ubmloLmdvdi9nZW8vZG93bmxvYWQvP2FjYz1HU0UxMTU5NzgmZm9ybWF0PWZpbGUmZmlsZT1HU0UxMTU5NzglNUZjZWxsJTJFYW5ub3RhdGlvbnMlMkVjc3YlMkVneikgZG93bmxvYWRlZCBmcm9tIFtHU0UxMTU5NzhdKGh0dHBzOi8vd3d3Lm5jYmkubmxtLm5paC5nb3YvZ2VvL3F1ZXJ5L2FjYy5jZ2k/YWNjPUdTRTExNTk3OCkuCgpJdCBjb250YWlucyBtZXRhIGRhdGEgb2YgNywxODYgY2VsbHMgZnJvbSBodW1hbiBtZWxhbm9tYSB0dW1vcnMuIEJhc2VkIG9uIFtUYWJsZSBTMUEuIENsaW5pY2FsIGNoYXJhY3RlcmlzdGljcyBvZiB0aGUgcGF0aWVudHMgYW5kIHNhbXBsZXMgaW4gdGhlIHNjUk5BLXNlcSBjb2hvcnRdKGh0dHBzOi8vd3d3LmNlbGwuY29tL2Ntcy8xMC4xMDE2L2ouY2VsbC4yMDE4LjA5LjAwNi9hdHRhY2htZW50LzlkNTlhZWMwLWFiYmQtNDFkYi05YzNlLTYwMWFmODE2OThjMS9tbWMxLnhsc3gpLCBXZSBzZWxlY3Qgc2FtcGxlcyBmb3IgY2VsbCB0eXBlIGRlY29udm9sdXRpb24gYnkgZm9sbG93aW5nIGNyaXRlcmlhOgoKMS4gVHJlYXRtZW50OiBOb25lOwoyLiBMZXNpb24gdHlwZTogbWV0YXN0YXNpczsKMy4gU2l0ZTogYWxsIGtpbmRzIG9mIGx5bXBoIG5vZGUuCgo4IHNhbXBsZXMgKGBNZWw3OWAsIGBNZWw4MGAsIGBNZWw4MWAsIGBNZWw4MmAsIGBNZWw4OWAsIGBNZWwxMDNgLCBgTWVsMTE2YCwgYE1lbDEyOGApIHdpdGggKioyLDQ5NSBjZWxscyoqIGFyZSBzZWxlY3RlZC4KClRoZSBjZWxsIHR5cGUgYW5ub3RhdGlvbiBpcyBzdG9yZWQgaW4gY29sdW1uIGBjZWxsLnR5cGVzYCwgd2hpY2ggaW5jbHVkZXMgdG90YWwgMTAgZGlzdGluY3QgYW5ub3RhdGlvbnMuCgpXZSBzZWxlY3RlZCA3IGNlbGwgdHlwZXMgYXMgYmVsb3c6CgoxLiBtYWxpZ25hbnQgY2VsbHM6ICJNYWwiLAoyLiBUIGNlbGxzOiAiVC5jZWxsIiArICJULkNENCIgKyAiVC5DRDgiCjMuIEIgY2VsbHM6ICJCLmNlbGwiCjQuIG5hdHVyYWwga2lsbGVyIChOSykgY2VsbHM6ICJOSyIKNS4gbWFjcm9waGFnZXM6ICJNYWNyb3BoYWdlIgo2LiBjYW5jZXItYXNzb2NpYXRlZCBmaWJyb2JsYXN0cyAoQ0FGcyk6ICJDQUYiCjcuIGVuZG90aGVsaWFsIGNlbGxzOiAiRW5kby4iCgpXZSByZS1hbmFseXNpcyB0aGUgZ2VuZSBleHByZXNzaW9uIG9mIHNlbGVjdGVkICoqMiw0OTUqKiBjZWxscywgY2x1c3RlciBhbGwgY2VsbHMgaW50byAxMCBjbHVzdGVycyB1c2luZyBUT1AgNSBQQ3MsIGFuZCByZS1sYWJlbCB0aGUgY2VsbHMgd2l0aCB1bmNsZWFyIGNlbGwgdHlwZSAiPyIgaW4gZWFjaCBjbHVzdGVyIGFzIHRoZSBkb21pbmF0ZSBjZWxsIHR5cGUgb2YgdGhlIGNsdXN0ZXIuCgpUaGUgcmVmaW5lZCBjZWxsIHR5cGUgYW5ub3RhdGlvbiBvZiBzZWxlY3RlZCAqKjIsNDk1KiogY2VsbHMgaXMgcHJvdmlkZWQgaW4gW01lbGFub21hX3JlZl9zY1JOQV9jZWxsX2NlbGx0eXBlLmNzdl0oaHR0cHM6Ly9naXRodWIuY29tL2F6N2poMi9TRGVQRVJfQW5hbHlzaXMvYmxvYi9tYWluL1JlYWxEYXRhL01lbGFub21hL01lbGFub21hX3JlZl9zY1JOQV9jZWxsX2NlbGx0eXBlLmNzdikuCgoKYGBge3J9CmZpbGVfbmFtZSA9IGZpbGUucGF0aChob21lLmRpciwgJ01lbGFub21hX3JlZl9zY1JOQV9jZWxsX2NlbGx0eXBlLmNzdicpCnJlZl9tZXRhID0gcmVhZC5jc3YoZmlsZV9uYW1lLCBzZXA9JywnLCBjaGVjay5uYW1lcyA9IEYsIGhlYWRlciA9IFQsIHJvdy5uYW1lcyA9IDEpCnByaW50KHNwcmludGYoJ2xvYWQgZGF0YSBmcm9tICVzJywgZmlsZV9uYW1lKSkKcHJpbnQoc3ByaW50ZigndG90YWwgJWQgY2VsbHMgd2l0aCBkaXN0aW5jdCAlZCBjZWxsIHR5cGUgYW5ub3RhdGlvbnMnLCBucm93KHJlZl9tZXRhKSwgbGVuZ3RoKHVuaXF1ZShyZWZfbWV0YSRuZXdfY2VsbHR5cGUpKSkpCgp0YWJsZShyZWZfbWV0YSRuZXdfY2VsbHR5cGUpCgpyZWZfbWV0YVsxOjUsICduZXdfY2VsbHR5cGUnLCBkcm9wPUZdCmBgYAoKCiMjIFJlYWQgYW5kIHByZXByb2Nlc3Mgc2NSTkEtc2VxIG5VTUkgZGF0YQoKT3JpZ2luYWwgZ2VuZSBuVU1JIGNvdW50IGRhdGEgZmlsZSBpcyBbR1NFMTE1OTc4X2NvdW50cy5jc3YuZ3pdKGh0dHBzOi8vd3d3Lm5jYmkubmxtLm5paC5nb3YvZ2VvL2Rvd25sb2FkLz9hY2M9R1NFMTE1OTc4JmZvcm1hdD1maWxlJmZpbGU9R1NFMTE1OTc4JTVGY291bnRzJTJFY3N2JTJFZ3opIGRvd25sb2FkZWQgZnJvbSBbR1NFMTE1OTc4XShodHRwczovL3d3dy5uY2JpLm5sbS5uaWguZ292L2dlby9xdWVyeS9hY2MuY2dpP2FjYz1HU0UxMTU5NzgpLiBJdCBjb250YWlucyB0b3RhbCA3LDE4NiBjZWxscyBhbmQgMjMsNjg2IGdlbmVzLgoKV2UganVzdCBzZWxlY3RlZCAyLDQ5NSBjZWxscyBvZiB0aGUgc2VsZWN0ZWQgNyBjZWxsIHR5cGVzIGJ5IGJhcmNvZGVzLCBhbmQgZGlzY2FyZCBvdGhlciBjZWxscy4gKipOTyBmaWx0ZXJpbmcgb24gZ2VuZXMqKiwgaS5lLiBhbGwgMjMsNjg2IGdlbmVzIHdpbGwgYmUgdXNlZCBmb3IgY2VsbCB0eXBlIGRlY29udm9sdXRpb24uCgpgYGB7cn0KZmlsZV9uYW1lID0gZmlsZS5wYXRoKGhvbWUuZGlyLCAnR1NFMTE1OTc4X2NvdW50cy5jc3YuZ3onKQpyZWZfZGF0YSA9IGRhdGEudGFibGU6OmZyZWFkKGZpbGVfbmFtZSwgc2VwID0gIiwiLCBjaGVjay5uYW1lcyA9IEZBTFNFKQpnZW5lX25hbWVzID0gcmVmX2RhdGEkVjEKY2VsbF9uYW1lcyA9IGNvbG5hbWVzKHJlZl9kYXRhKVsyOm5jb2wocmVmX2RhdGEpXQoKIyB0cmFuc3Bvc2UgaXQKcmVmX2RhdGEgPSBhcy5kYXRhLmZyYW1lKGRhdGEudGFibGU6OnRyYW5zcG9zZShyZWZfZGF0YSAlPiUKICBzZWxlY3QoY2VsbF9uYW1lcykpKQoKcm93Lm5hbWVzKHJlZl9kYXRhKSA9IGNlbGxfbmFtZXMKY29sbmFtZXMocmVmX2RhdGEpID0gZ2VuZV9uYW1lcwoKcHJpbnQoc3ByaW50ZignbG9hZCBkYXRhIGZyb20gJXMnLCBmaWxlX25hbWUpKQpwcmludChzcHJpbnRmKCd0b3RhbCBjZWxsczogJWQ7IGdlbmVzOiAlZCcsIG5yb3cocmVmX2RhdGEpLCBuY29sKHJlZl9kYXRhKSkpCmBgYAoKU2VsZWN0IGNlbGxzIGFuZCBzYXZlIHNjUk5BLXNlcSBuVU1JIG1hdHJpeCB0byBmaWxlIFtNZWxhbm9tYV9yZWZfc2NSTkFfY2VsbF9uVU1JLmNzdi5nel0oaHR0cHM6Ly9naXRodWIuY29tL2F6N2poMi9TRGVQRVJfQW5hbHlzaXMvYmxvYi9tYWluL1JlYWxEYXRhL01lbGFub21hL01lbGFub21hX3JlZl9zY1JOQV9jZWxsX25VTUkuY3N2Lmd6KQoKYGBge3J9CnJlZl9kYXRhID0gcmVmX2RhdGFbcm93Lm5hbWVzKHJlZl9tZXRhKSwgXQoKcmVmX2RhdGFbMTo1LCAxOjVdCgpkYXRhLnRhYmxlOjpmd3JpdGUocmVmX2RhdGEsICdNZWxhbm9tYV9yZWZfc2NSTkFfY2VsbF9uVU1JLmNzdi5neicsIHJvdy5uYW1lcyA9IFQpCnByaW50KHNwcmludGYoJ3NhdmUgblVNSSBtYXRyaXggb2YgcmVmZXJlbmNlIHNjUk5BLXNlcSBjZWxscyBpbnRvIGd6aXAgY29tcHJlc3NlZCBmaWxlICVzJywgJ01lbGFub21hX3JlZl9zY1JOQV9jZWxsX25VTUkuY3N2Lmd6JykpCmBgYAoKCg==