About Jaccard Similarity
Xem xét một ví dụ giả định về 3 JAV Fants và 6 diễn viên yêu thích của họ như sau:
# Clear workspace:
rm(list = ls())
#============================
# Jaccard Similarity
#============================
# Create a fake data that describes movies viewed by 3 users:
user1 <- rep(0:1, each = 1, time = 3)
user2 <- c(0, rep(1, 3), 0, 1)
user3 <- c(1, 0, 0, 0, 1, 1)
row_names <- c("Maria Ozawa",
"Eimi Fukada",
"Nozomi Sasaki",
"Sora Aoi",
"Minori Hatsune",
"Ria Sakurai")
df_fake <- data.frame(user1 = user1, user2 = user2, user3 = user3)
row.names(df_fake) <- row_names
# Show data:
df_fake
## user1 user2 user3
## Maria Ozawa 0 0 1
## Eimi Fukada 1 1 0
## Nozomi Sasaki 0 1 0
## Sora Aoi 1 1 0
## Minori Hatsune 0 0 1
## Ria Sakurai 1 1 1
Với user1 thì Eimi Fukada, Sora Aoi, Ria Sakurai là ba diễn viên yêu thích của người này (đánh số 1). Các diễn viên khác đánh số 0 nghĩa là chưa thích. Chúng ta có thể tính mức độ tương đồng của user1 này với hai users còn lại theo thước đo Jaccard Similarity như sau:
# Jaccard Similarity between user1 and user2:
3 / (3 + 0 + 1)
## [1] 0.75
# Jaccard Similarity between user1 and user3:
1 / (1 + 2 + 2)
## [1] 0.2
Diễn giải dưới đây sẽ giúp bạn hiểu một cách trực quan những gì được viết trong các tài liệu Toán về Jaccard Similarity. Ví dụ, với cặp user1 - user2 thì con số 0.75 được đếm bằng a / (a + b + c) trong đó:
- a lượng giá trị 1 mà cột user1 và user đều có. Trong tình huống của chúng ta thì a = 2.
- b là số lượng giá trị 1 xuất hiện ở cột user1 nhưng lại là 0 ở cột user2. Trong tình huống của chúng ta thì b = 0.
- c là số lượng giá trị 1 xuất hiện ở cột user2 nhưng lại là 0 ở cột user1.
Kết quả chỉ ra rằng giữa user1 - user2 là giống nhau hơn so với cặp user1 - user3. Điều này dẫn đến một suy luận là nên khuyến nghị những diễn viên yêu thích của user2 cho user1. Đây chính là cơ sở để xây dựng Recommender Engine dựa trên chỉ số tương đồng Jaccard.
A Real-world Application
Chúng ta sử dụng bộ dữ liệu MovieLens Data Set để xây dựng một Recommender dựa trên Jaccard Similarity. Trước hết chúng ta đọc bộ dữ liệu này và convert về Binary Data (lưu ý là chúng ta có ratings cho các movies nhưng bài toán sẽ được giải quyết trên giả định chúng ta không có ratings và do vậy tất các những movies có rating chúng ta sẽ phải convert về 1):
# Import data:
library(tidyverse)
ratings <- read_csv("ratings.csv")
# Convert to real time:
library(lubridate)
ratings %>%
mutate(timestamp = as_datetime(timestamp), timestamp = date(timestamp)) -> ratings
# All ratings:
ratings$rating %>% unique() -> all_ratings
# Set all users and movies:
all_users <- ratings$userId %>% unique() %>% as.character()
all_movies <- ratings$movieId %>% unique() %>% as.character()
# convert to binary matrix:
ratings %>%
select(-timestamp) %>%
spread(key = movieId, value = rating, fill = 0) %>%
mutate_at(.vars = all_movies, function(x) {case_when(x %in% all_ratings ~ 1, TRUE ~ 0)}) %>%
mutate(userId = all_users) -> df_binary
# Show some observations:
head(df_binary[1:5, 1:5])
## # A tibble: 5 x 5
## userId `1` `2` `3` `4`
## <chr> <dbl> <dbl> <dbl> <dbl>
## 1 1 1 0 1 0
## 2 2 0 0 0 0
## 3 3 0 0 0 0
## 4 4 0 0 0 0
## 5 5 1 0 0 0
COnvert df_binary về binary matrix (cho tiện tính toán trên matrix, nếu không thích tính toán với matrix có thể bỏ qua bước này):
# Convert to transpose of df_binary:
df_binary %>%
select(-userId) %>%
t() %>%
as.matrix() %>%
as.data.frame() -> my_mat
# Set col and row names for the transpose:
colnames(my_mat) <- all_users
rownames(my_mat) <- all_movies
Viết hàm có tên jaccard_sim()
tính Jaccard Similarity giữa hai users (không loại trừ hai users này là một):
# Function calculates Jaccard Similarity:
jaccard_sim <- function(two_users) {
sums <- rowSums(my_mat[, two_users])
a <- length(sums[sums == 2])
a_b_c <- length(sums[sums == 1]) + a
jaccard_sim <- a/a_b_c
return(jaccard_sim)
}
Dưới đây là tương đồng giữa user thứ nhất và thứ hai:
# Test the function:
jaccard_sim(all_users[c(1, 2)])
## [1] 0.007722008
Đương nhiên user1 và chính nó sẽ có tương đồng là 1:
jaccard_sim(all_users[c(1, 1)])
## [1] 1
Chúng ta sẽ sử dụng hàm này cho các bước xây dựng Recommender Engine sau này. Sử dụng cách tiếp cận đã trình bày trong Part 3 chúng ta sẽ chọn mốc thời gian 2018-01-01 để chia dữ liệu thành bộ train và test:
#==================================================
# Recommender Engine using Jaccard Similarity
#==================================================
# Set time point:
time_selected <- ymd("2018-01-01")
# Split data for train and test engine:
train_raw_data <- ratings %>% filter(timestamp <= time_selected)
test_raw_data <- ratings %>% filter(timestamp > time_selected)
# All ratings in train data:
all_ratings_train <- train_raw_data$rating %>% unique()
# Set all users and movies:
all_users_train <- train_raw_data$userId %>% unique() %>% as.character()
all_movies_train <- train_raw_data$movieId %>% unique() %>% as.character()
all_users_test <- test_raw_data$userId %>% unique() %>% as.character()
# convert to binary matrix for train data:
train_raw_data %>%
select(-timestamp) %>%
spread(key = movieId, value = rating, fill = 0) %>%
mutate_at(.vars = all_movies_train, function(x) {case_when(x %in% all_ratings_train ~ 1, TRUE ~ 0)}) -> df_binary_train
# Convert to transpose:
df_binary_train %>%
select(-userId) %>%
t() %>%
as.matrix() %>%
as.data.frame() -> my_mat_train
# Set col and row names for the transpose of df_binary_train:
colnames(my_mat_train) <- all_users_train
rownames(my_mat_train) <- all_movies_train
Viết hàm top_5_jaccard()
đưa ra 5 users có Jaccard Similarity (JS) cao nhất với một user chọn trước:
# Function returns 5 users with highest Jaccard similarities:
top_5_jaccard <- function(user_selected) {
m_users <- length(all_users_train)
jaccard_sim_i_th <- NULL
for (j in 1:m_users) {
sums <- rowSums(my_mat_train[, c(user_selected, all_users_train[j])])
a <- length(sums[sums == 2])
a_b_c <- length(sums[sums == 1]) + a
jaccard_sim <- a/a_b_c
jaccard_sim_i_th <- c(jaccard_sim_i_th, jaccard_sim)
}
df_results <- data.frame(userId = all_users_train, similarity = jaccard_sim_i_th)
df_results %>%
mutate(reference = case_when(similarity == 1 ~ "yes", TRUE ~ "no")) %>%
top_n(n = 6, wt = similarity) %>%
arrange(-similarity) -> df_top5
return(df_top5)
}
JS cho tất cả các users mà sẽ có mặt cả trước và sau thời điểm 2018-01-01:
# Common users:
base::intersect(all_users_train, all_users_test) -> common_users
# For all users:
lapply(common_users, top_5_jaccard) -> jaccard_for_all
# For the first 3 users:
jaccard_for_all[1:3]
## [[1]]
## userId similarity reference
## 1 18 1.0000000 yes
## 2 305 0.2541296 no
## 3 561 0.2509458 no
## 4 573 0.2496025 no
## 5 249 0.2405583 no
## 6 63 0.2385621 no
##
## [[2]]
## userId similarity reference
## 1 50 1.0000000 yes
## 2 247 0.1311475 no
## 3 328 0.1194030 no
## 4 18 0.1035599 no
## 5 525 0.1031746 no
## 6 339 0.1011673 no
##
## [[3]]
## userId similarity reference
## 1 68 1.0000000 yes
## 2 274 0.3473463 no
## 3 608 0.3436066 no
## 4 480 0.3416068 no
## 5 414 0.3130316 no
## 6 177 0.3066502 no
Viết hàm movies_recommended_for_specific_user()
trả về movieId của những bộ phim được khuyến nghị cho một user cho trước:
# Function recommends movies for a specific user:
movies_recommended_for_specific_user <- function(userID) {
top_5_jaccard(userID) -> df_sim_user
df_sim_user %>%
filter(similarity != 1) %>%
top_n(n = 1, wt = similarity) %>%
pull(userId) %>%
as.character() -> user_that_most_sim
test_raw_data %>%
mutate(userId = as.character(userId)) %>%
filter(userId %in% user_that_most_sim) %>%
pull(movieId) %>%
unique() %>%
as.character() -> movies_recom
return(tibble(userId = userID, movies_recom = movies_recom))
}
Chúng ta có thể, ví dụ, sử dụng hàm đã có list ra danh sách các bộ phim mà user thứ nhất nên xem. Lưu ý rằng danh sách các bộ phim đó đến từ các bộ phim đã từng được xem bởi user có JS cao nhất - hay tương đồng cao nhất với user thứ nhất:
movies_recommended_for_specific_user(common_users[1]) %>% head()
## # A tibble: 6 x 2
## userId movies_recom
## <chr> <chr>
## 1 18 25
## 2 18 288
## 3 18 300
## 4 18 316
## 5 18 357
## 6 18 474
Viết hàm return_number_Recommendedmovies_viewed()
đếm số bộ phim mà Engine khuyến nghị thực tế sẽ được xem bởi một user cho trước:
# Function test
return_number_Recommendedmovies_viewed <- function(user_i_th) {
test_raw_data %>%
filter(userId == user_i_th) %>%
pull(movieId) %>%
unique() -> movies_actual
movies_recommended_for_specific_user(user_i_th) -> df_recom
df_recom %>%
pull(movies_recom) -> movies_recom
return(sum(movies_recom %in% movies_actual))
}
Với user, ví dụ, thứ 10 chẳng hạn thì số bộ phim mà người này sẽ xem trong số các bộ phim được Engine khuyến nghị là:
return_number_Recommendedmovies_viewed(common_users[10])
## [1] 3
Chúng ta có thể đo lường hiệu quả vận hành của Recommender Engine bằng các so sánh những movies được hệ thống khuyến nghị với những bộ phim mà users sẽ xem sau thời điểm 2018-01-01:
sum(sapply(common_users, return_number_Recommendedmovies_viewed) != 0) / length(common_users)
## [1] 0.35
Kết quả này nghĩa là 35% số users sau ngày 2018-01-01 sẽ xem ít nhất một movie mà hệ thống khuyến nghị cho họ.
LS0tDQp0aXRsZTogJ1JlY29tbWVuZGF0aW9uIFN5c3RlbSAoUGFydCA0KScNCmF1dGhvcjogJ0F1dGhvcjogTmd1eWVuIENoaSBEdW5nJw0Kc3VidGl0bGU6ICJSIE1hY2hpbmUgTGVhcm5pbmcgU2VyaWVzIg0Kb3V0cHV0Og0KICBodG1sX2RvY3VtZW50OiANCiAgICBjb2RlX2Rvd25sb2FkOiB0cnVlDQogICAgIyBjb2RlX2ZvbGRpbmc6IGhpZGUNCiAgICBoaWdobGlnaHQ6IHplbmJ1cm4NCiAgICAjIG51bWJlcl9zZWN0aW9uczogeWVzDQogICAgdGhlbWU6ICJmbGF0bHkiDQogICAgdG9jOiBUUlVFDQogICAgdG9jX2Zsb2F0OiBUUlVFDQotLS0NCg0KYGBge3Igc2V0dXAsaW5jbHVkZT1GQUxTRX0NCmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gVFJVRSwgd2FybmluZyA9IEZBTFNFLCBtZXNzYWdlID0gRkFMU0UsIGNhY2hlID0gVFJVRSkNCg0KYGBgDQoNCg0KIyBJbnRyb2R1Y3Rpb24NCg0KVuG7gSBSZWNvbW1lbmRhdGlvbiBTeXN0ZW0gdsOgIHjDonkgZOG7sW5nIFJlY29tbWVuZGVyIHRyb25nIGPDoWMgdMOsbmggaHXhu5FuZyB0aMaw4budbmcgcGjDoXQgc2luaCB0cm9uZyB0aOG7sWMgdOG6vyAoY8OzIHJhdGluZ3MsIGtow7RuZyBjw7MgcmF0aW5ncyBjxaluZyBuaMawIMSRw6FuaCBnacOhIGhp4buHdSBxdeG6oyBj4bunYSBSZWNvbW1lbmRlcikgY8OhYyBi4bqhbiBjw7MgdGjhu4MgdGhhbSBraOG6o28gW1BhcnQgMV0oaHR0cHM6Ly9ycHVicy5jb20vY2hpZHVuZ2t0LzYzNDMwMCksIFtQYXJ0IDJdKGh0dHBzOi8vcnB1YnMuY29tL2NoaWR1bmdrdC82Mzg3NjApIHbDoCBbUGFydCAzXShodHRwczovL3JwdWJzLmNvbS9jaGlkdW5na3QvNjM5NzYwKS4gVHJvbmcgcGjhuqduIG7DoHkgY2jDum5nIHRhIHPhur0geMOieSBk4buxbmcgUmVjb21tZW5kZXIgRW5naW5lIGThu7FhIHRyw6puIEphY2NhcmQgU2ltaWxhcml0eSDDoXAgZOG7pW5nIGNobyBCaW5hcnkgRGF0YSAtIHbhu5FuIGzDoCBt4buZdCB0w6xuaCBodeG7kW5nIHBo4buVIGJp4bq/biBraGkgbcOgIGThu68gbGnhu4d1IHbhu4EgaXRlbXMgbMOgIGtow7RuZyBjw7MgcmF0aW5ncyB0xrDGoW5nIOG7qW5nLiANCg0KIyBBYm91dCBKYWNjYXJkIFNpbWlsYXJpdHkNCg0KWGVtIHjDqXQgbeG7mXQgdsOtIGThu6UgZ2nhuqMgxJHhu4tuaCB24buBIDMgSkFWIEZhbnRzIHbDoCA2IGRp4buFbiB2acOqbiB5w6p1IHRow61jaCBj4bunYSBo4buNIG5oxrAgc2F1OiANCg0KYGBge3J9DQojIENsZWFyIHdvcmtzcGFjZTogDQoNCnJtKGxpc3QgPSBscygpKQ0KDQojPT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KIyAgICBKYWNjYXJkIFNpbWlsYXJpdHkNCiM9PT09PT09PT09PT09PT09PT09PT09PT09PT09DQoNCiMgQ3JlYXRlIGEgZmFrZSBkYXRhIHRoYXQgZGVzY3JpYmVzIG1vdmllcyB2aWV3ZWQgYnkgMyB1c2VyczogDQoNCnVzZXIxIDwtIHJlcCgwOjEsIGVhY2ggPSAxLCB0aW1lID0gMykNCnVzZXIyIDwtIGMoMCwgcmVwKDEsIDMpLCAwLCAxKQ0KdXNlcjMgPC0gYygxLCAwLCAwLCAwLCAxLCAxKQ0KDQpyb3dfbmFtZXMgPC0gYygiTWFyaWEgT3phd2EiLCANCiAgICAgICAgICAgICAgICJFaW1pIEZ1a2FkYSIsIA0KICAgICAgICAgICAgICAgIk5vem9taSBTYXNha2kiLCANCiAgICAgICAgICAgICAgICJTb3JhIEFvaSIsIA0KICAgICAgICAgICAgICAgIk1pbm9yaSBIYXRzdW5lIiwNCiAgICAgICAgICAgICAgICJSaWEgU2FrdXJhaSIpDQoNCmRmX2Zha2UgPC0gZGF0YS5mcmFtZSh1c2VyMSA9IHVzZXIxLCB1c2VyMiA9IHVzZXIyLCB1c2VyMyA9IHVzZXIzKQ0KDQpyb3cubmFtZXMoZGZfZmFrZSkgPC0gcm93X25hbWVzDQoNCiMgU2hvdyBkYXRhOiANCg0KZGZfZmFrZQ0KDQpgYGANCg0KVuG7m2kgdXNlcjEgdGjDrCBFaW1pIEZ1a2FkYSwgU29yYSBBb2ksIFJpYSBTYWt1cmFpIGzDoCBiYSBkaeG7hW4gdmnDqm4gecOqdSB0aMOtY2ggY+G7p2EgbmfGsOG7nWkgbsOgeSAoxJHDoW5oIHPhu5EgMSkuIEPDoWMgZGnhu4VuIHZpw6puIGtow6FjIMSRw6FuaCBz4buRIDAgbmdoxKlhIGzDoCBjaMawYSB0aMOtY2guIENow7puZyB0YSBjw7MgdGjhu4MgdMOtbmggbeG7qWMgxJHhu5kgdMawxqFuZyDEkeG7k25nIGPhu6dhIHVzZXIxIG7DoHkgduG7m2kgaGFpIHVzZXJzIGPDsm4gbOG6oWkgdGhlbyB0aMaw4bubYyDEkW8gW0phY2NhcmQgU2ltaWxhcml0eV0oaHR0cHM6Ly9lbi53aWtpcGVkaWEub3JnL3dpa2kvSmFjY2FyZF9pbmRleCkgbmjGsCBzYXU6IA0KDQpgYGB7cn0NCg0KIyBKYWNjYXJkIFNpbWlsYXJpdHkgYmV0d2VlbiB1c2VyMSBhbmQgdXNlcjI6IA0KDQozIC8gKDMgKyAwICsgMSkNCg0KIyBKYWNjYXJkIFNpbWlsYXJpdHkgYmV0d2VlbiB1c2VyMSBhbmQgdXNlcjM6DQoNCjEgLyAoMSArIDIgKyAyKQ0KYGBgDQoNCkRp4buFbiBnaeG6o2kgZMaw4bubaSDEkcOieSBz4bq9IGdpw7pwIGLhuqFuIGhp4buDdSBt4buZdCBjw6FjaCB0cuG7sWMgcXVhbiBuaOG7r25nIGfDrCDEkcaw4bujYyB2aeG6v3QgdHJvbmcgY8OhYyB0w6BpIGxp4buHdSBUb8OhbiB24buBIEphY2NhcmQgU2ltaWxhcml0eS4gVsOtIGThu6UsIHbhu5tpIGPhurdwIHVzZXIxIC0gdXNlcjIgdGjDrCBjb24gc+G7kSAwLjc1IMSRxrDhu6NjIMSR4bq/bSBi4bqxbmcgYSAvIChhICsgYiArIGMpIHRyb25nIMSRw7M6IA0KDQotIGEgbMaw4bujbmcgZ2nDoSB0cuG7iyAxIG3DoCBj4buZdCB1c2VyMSB2w6AgdXNlciDEkeG7gXUgY8OzLiBUcm9uZyB0w6xuaCBodeG7kW5nIGPhu6dhIGNow7puZyB0YSB0aMOsIGEgPSAyLiANCi0gYiBsw6Agc+G7kSBsxrDhu6NuZyBnacOhIHRy4buLIDEgeHXhuqV0IGhp4buHbiDhu58gY+G7mXQgdXNlcjEgbmjGsG5nIGzhuqFpIGzDoCAwIOG7nyBj4buZdCB1c2VyMi4gVHJvbmcgdMOsbmggaHXhu5FuZyBj4bunYSBjaMO6bmcgdGEgdGjDrCBiID0gMC4gDQotIGMgbMOgIHPhu5EgbMaw4bujbmcgZ2nDoSB0cuG7iyAxIHh14bqldCBoaeG7h24g4bufIGPhu5l0IHVzZXIyIG5oxrBuZyBs4bqhaSBsw6AgMCDhu58gY+G7mXQgdXNlcjEuDQoNCkvhur90IHF14bqjIGNo4buJIHJhIHLhurFuZyBnaeG7r2EgdXNlcjEgLSB1c2VyMiBsw6AgZ2nhu5FuZyBuaGF1IGjGoW4gc28gduG7m2kgY+G6t3AgdXNlcjEgLSB1c2VyMy4gxJBp4buBdSBuw6B5IGThuqtuIMSR4bq/biBt4buZdCBzdXkgbHXhuq1uIGzDoCBuw6puIGtodXnhur9uIG5naOG7iyBuaOG7r25nIGRp4buFbiB2acOqbiB5w6p1IHRow61jaCBj4bunYSB1c2VyMiBjaG8gdXNlcjEuIMSQw6J5IGNow61uaCBsw6AgY8ahIHPhu58gxJHhu4MgeMOieSBk4buxbmcgUmVjb21tZW5kZXIgRW5naW5lIGThu7FhIHRyw6puIGNo4buJIHPhu5EgdMawxqFuZyDEkeG7k25nIEphY2NhcmQuIA0KDQojIEEgUmVhbC13b3JsZCBBcHBsaWNhdGlvbg0KDQpDaMO6bmcgdGEgc+G7rSBk4bulbmcgYuG7mSBk4buvIGxp4buHdSBbTW92aWVMZW5zIERhdGEgU2V0XShodHRwczovL2dyb3VwbGVucy5vcmcvZGF0YXNldHMvbW92aWVsZW5zL2xhdGVzdC8pIMSR4buDIHjDonkgZOG7sW5nIG3hu5l0IFJlY29tbWVuZGVyIGThu7FhIHRyw6puIEphY2NhcmQgU2ltaWxhcml0eS4gVHLGsOG7m2MgaOG6v3QgY2jDum5nIHRhIMSR4buNYyBi4buZIGThu68gbGnhu4d1IG7DoHkgdsOgIGNvbnZlcnQgduG7gSBCaW5hcnkgRGF0YSAobMawdSDDvSBsw6AgY2jDum5nIHRhIGPDsyByYXRpbmdzIGNobyBjw6FjIG1vdmllcyBuaMawbmcgYsOgaSB0b8OhbiBz4bq9IMSRxrDhu6NjIGdp4bqjaSBxdXnhur90IHRyw6puIGdp4bqjIMSR4buLbmggY2jDum5nIHRhIGtow7RuZyBjw7MgcmF0aW5ncyB2w6AgZG8gduG6rXkgdOG6pXQgY8OhYyBuaOG7r25nIG1vdmllcyBjw7MgcmF0aW5nIGNow7puZyB0YSBz4bq9IHBo4bqjaSBjb252ZXJ0IHbhu4EgMSk6IA0KDQpgYGB7cn0NCiMgSW1wb3J0IGRhdGE6IA0KDQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCg0KcmF0aW5ncyA8LSByZWFkX2NzdigicmF0aW5ncy5jc3YiKQ0KDQojIENvbnZlcnQgdG8gcmVhbCB0aW1lOiANCg0KbGlicmFyeShsdWJyaWRhdGUpDQoNCnJhdGluZ3MgJT4lIA0KICBtdXRhdGUodGltZXN0YW1wID0gYXNfZGF0ZXRpbWUodGltZXN0YW1wKSwgdGltZXN0YW1wID0gZGF0ZSh0aW1lc3RhbXApKSAtPiByYXRpbmdzDQoNCiMgQWxsIHJhdGluZ3M6IA0KDQpyYXRpbmdzJHJhdGluZyAlPiUgdW5pcXVlKCkgLT4gYWxsX3JhdGluZ3MNCg0KIyBTZXQgYWxsIHVzZXJzIGFuZCBtb3ZpZXM6ICANCg0KYWxsX3VzZXJzIDwtIHJhdGluZ3MkdXNlcklkICU+JSB1bmlxdWUoKSAlPiUgYXMuY2hhcmFjdGVyKCkNCg0KYWxsX21vdmllcyA8LSByYXRpbmdzJG1vdmllSWQgJT4lIHVuaXF1ZSgpICU+JSBhcy5jaGFyYWN0ZXIoKQ0KDQojIGNvbnZlcnQgdG8gYmluYXJ5IG1hdHJpeDogDQoNCnJhdGluZ3MgJT4lIA0KICBzZWxlY3QoLXRpbWVzdGFtcCkgJT4lIA0KICBzcHJlYWQoa2V5ID0gbW92aWVJZCwgdmFsdWUgPSByYXRpbmcsIGZpbGwgPSAwKSAlPiUgDQogIG11dGF0ZV9hdCgudmFycyA9IGFsbF9tb3ZpZXMsIGZ1bmN0aW9uKHgpIHtjYXNlX3doZW4oeCAlaW4lIGFsbF9yYXRpbmdzIH4gMSwgVFJVRSB+IDApfSkgJT4lIA0KICBtdXRhdGUodXNlcklkID0gYWxsX3VzZXJzKSAtPiBkZl9iaW5hcnkNCg0KIyBTaG93IHNvbWUgb2JzZXJ2YXRpb25zOiANCg0KaGVhZChkZl9iaW5hcnlbMTo1LCAxOjVdKQ0KDQpgYGANCg0KQ09udmVydCBkZl9iaW5hcnkgduG7gSBiaW5hcnkgbWF0cml4IChjaG8gdGnhu4duIHTDrW5oIHRvw6FuIHRyw6puIG1hdHJpeCwgbuG6v3Uga2jDtG5nIHRow61jaCB0w61uaCB0b8OhbiB24bubaSBtYXRyaXggY8OzIHRo4buDIGLhu48gcXVhIGLGsOG7m2MgbsOgeSk6IA0KDQpgYGB7cn0NCg0KIyBDb252ZXJ0IHRvIHRyYW5zcG9zZSBvZiBkZl9iaW5hcnk6IA0KDQpkZl9iaW5hcnkgJT4lIA0KICBzZWxlY3QoLXVzZXJJZCkgJT4lIA0KICB0KCkgJT4lIA0KICBhcy5tYXRyaXgoKSAlPiUgDQogIGFzLmRhdGEuZnJhbWUoKSAtPiBteV9tYXQNCg0KIyBTZXQgY29sIGFuZCByb3cgbmFtZXMgZm9yIHRoZSB0cmFuc3Bvc2U6IA0KDQpjb2xuYW1lcyhteV9tYXQpIDwtIGFsbF91c2Vycw0Kcm93bmFtZXMobXlfbWF0KSA8LSBhbGxfbW92aWVzDQoNCmBgYA0KDQpWaeG6v3QgaMOgbSBjw7MgdMOqbiBgamFjY2FyZF9zaW0oKWAgdMOtbmggSmFjY2FyZCBTaW1pbGFyaXR5IGdp4buvYSBoYWkgdXNlcnMgKGtow7RuZyBsb+G6oWkgdHLhu6sgaGFpIHVzZXJzIG7DoHkgbMOgIG3hu5l0KTogDQoNCmBgYHtyfQ0KIyBGdW5jdGlvbiBjYWxjdWxhdGVzIEphY2NhcmQgU2ltaWxhcml0eTogDQoNCmphY2NhcmRfc2ltIDwtIGZ1bmN0aW9uKHR3b191c2Vycykgew0KICANCiAgc3VtcyA8LSByb3dTdW1zKG15X21hdFssIHR3b191c2Vyc10pDQogIA0KICBhIDwtIGxlbmd0aChzdW1zW3N1bXMgPT0gMl0pDQogIA0KICBhX2JfYyA8LSBsZW5ndGgoc3Vtc1tzdW1zID09IDFdKSArIGENCiAgDQogIGphY2NhcmRfc2ltIDwtIGEvYV9iX2MNCiAgDQogIHJldHVybihqYWNjYXJkX3NpbSkNCg0KfQ0KYGBgDQoNCkTGsOG7m2kgxJHDonkgbMOgIHTGsMahbmcgxJHhu5NuZyBnaeG7r2EgdXNlciB0aOG7qSBuaOG6pXQgdsOgIHRo4bupIGhhaTogDQoNCmBgYHtyfQ0KIyBUZXN0IHRoZSBmdW5jdGlvbjogDQoNCmphY2NhcmRfc2ltKGFsbF91c2Vyc1tjKDEsIDIpXSkNCg0KYGBgDQoNCsSQxrDGoW5nIG5oacOqbiB1c2VyMSB2w6AgY2jDrW5oIG7DsyBz4bq9IGPDsyB0xrDGoW5nIMSR4buTbmcgbMOgIDE6IA0KDQpgYGB7cn0NCmphY2NhcmRfc2ltKGFsbF91c2Vyc1tjKDEsIDEpXSkNCmBgYA0KDQpDaMO6bmcgdGEgc+G6vSBz4butIGThu6VuZyBow6BtIG7DoHkgY2hvIGPDoWMgYsaw4bubYyB4w6J5IGThu7FuZyBSZWNvbW1lbmRlciBFbmdpbmUgc2F1IG7DoHkuIFPhu60gZOG7pW5nIGPDoWNoIHRp4bq/cCBj4bqtbiDEkcOjIHRyw6xuaCBiw6B5IHRyb25nIFBhcnQgMyBjaMO6bmcgdGEgc+G6vSBjaOG7jW4gbeG7kWMgdGjhu51pIGdpYW4gMjAxOC0wMS0wMSDEkeG7gyBjaGlhIGThu68gbGnhu4d1IHRow6BuaCBi4buZIHRyYWluIHbDoCB0ZXN0OiANCg0KDQpgYGB7cn0NCiM9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KIyAgICBSZWNvbW1lbmRlciBFbmdpbmUgdXNpbmcgSmFjY2FyZCBTaW1pbGFyaXR5DQojPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCg0KIyBTZXQgdGltZSBwb2ludDogDQoNCnRpbWVfc2VsZWN0ZWQgPC0geW1kKCIyMDE4LTAxLTAxIikNCg0KIyBTcGxpdCBkYXRhIGZvciB0cmFpbiBhbmQgdGVzdCBlbmdpbmU6IA0KDQp0cmFpbl9yYXdfZGF0YSA8LSByYXRpbmdzICU+JSBmaWx0ZXIodGltZXN0YW1wIDw9IHRpbWVfc2VsZWN0ZWQpDQoNCnRlc3RfcmF3X2RhdGEgPC0gcmF0aW5ncyAlPiUgZmlsdGVyKHRpbWVzdGFtcCA+IHRpbWVfc2VsZWN0ZWQpDQoNCiMgQWxsIHJhdGluZ3MgaW4gdHJhaW4gZGF0YTogDQoNCmFsbF9yYXRpbmdzX3RyYWluIDwtIHRyYWluX3Jhd19kYXRhJHJhdGluZyAlPiUgdW5pcXVlKCkNCg0KIyBTZXQgYWxsIHVzZXJzIGFuZCBtb3ZpZXM6ICANCg0KYWxsX3VzZXJzX3RyYWluIDwtIHRyYWluX3Jhd19kYXRhJHVzZXJJZCAlPiUgdW5pcXVlKCkgJT4lIGFzLmNoYXJhY3RlcigpDQoNCmFsbF9tb3ZpZXNfdHJhaW4gPC0gdHJhaW5fcmF3X2RhdGEkbW92aWVJZCAlPiUgdW5pcXVlKCkgJT4lIGFzLmNoYXJhY3RlcigpDQoNCmFsbF91c2Vyc190ZXN0IDwtIHRlc3RfcmF3X2RhdGEkdXNlcklkICU+JSB1bmlxdWUoKSAlPiUgYXMuY2hhcmFjdGVyKCkNCg0KIyBjb252ZXJ0IHRvIGJpbmFyeSBtYXRyaXggZm9yIHRyYWluIGRhdGE6IA0KDQp0cmFpbl9yYXdfZGF0YSAlPiUgDQogIHNlbGVjdCgtdGltZXN0YW1wKSAlPiUgDQogIHNwcmVhZChrZXkgPSBtb3ZpZUlkLCB2YWx1ZSA9IHJhdGluZywgZmlsbCA9IDApICU+JSANCiAgbXV0YXRlX2F0KC52YXJzID0gYWxsX21vdmllc190cmFpbiwgZnVuY3Rpb24oeCkge2Nhc2Vfd2hlbih4ICVpbiUgYWxsX3JhdGluZ3NfdHJhaW4gfiAxLCBUUlVFIH4gMCl9KSAtPiBkZl9iaW5hcnlfdHJhaW4NCg0KIyBDb252ZXJ0IHRvIHRyYW5zcG9zZTogDQoNCmRmX2JpbmFyeV90cmFpbiAlPiUgDQogIHNlbGVjdCgtdXNlcklkKSAlPiUgDQogIHQoKSAlPiUgDQogIGFzLm1hdHJpeCgpICU+JSANCiAgYXMuZGF0YS5mcmFtZSgpIC0+IG15X21hdF90cmFpbg0KDQojIFNldCBjb2wgYW5kIHJvdyBuYW1lcyBmb3IgdGhlIHRyYW5zcG9zZSBvZiBkZl9iaW5hcnlfdHJhaW46IA0KDQpjb2xuYW1lcyhteV9tYXRfdHJhaW4pIDwtIGFsbF91c2Vyc190cmFpbg0Kcm93bmFtZXMobXlfbWF0X3RyYWluKSA8LSBhbGxfbW92aWVzX3RyYWluDQpgYGANCg0KVmnhur90IGjDoG0gYHRvcF81X2phY2NhcmQoKWAgxJHGsGEgcmEgNSB1c2VycyBjw7MgSmFjY2FyZCBTaW1pbGFyaXR5IChKUykgY2FvIG5o4bqldCB24bubaSBt4buZdCB1c2VyIGNo4buNbiB0csaw4bubYzogDQoNCmBgYHtyfQ0KDQojIEZ1bmN0aW9uIHJldHVybnMgNSB1c2VycyB3aXRoIGhpZ2hlc3QgSmFjY2FyZCBzaW1pbGFyaXRpZXM6IA0KDQp0b3BfNV9qYWNjYXJkIDwtIGZ1bmN0aW9uKHVzZXJfc2VsZWN0ZWQpIHsNCiAgDQogIG1fdXNlcnMgPC0gbGVuZ3RoKGFsbF91c2Vyc190cmFpbikNCiAgDQogIGphY2NhcmRfc2ltX2lfdGggPC0gTlVMTA0KICANCiAgZm9yIChqIGluIDE6bV91c2Vycykgew0KICAgIA0KICAgIHN1bXMgPC0gcm93U3VtcyhteV9tYXRfdHJhaW5bLCBjKHVzZXJfc2VsZWN0ZWQsIGFsbF91c2Vyc190cmFpbltqXSldKQ0KICAgIA0KICAgIGEgPC0gbGVuZ3RoKHN1bXNbc3VtcyA9PSAyXSkNCiAgICANCiAgICBhX2JfYyA8LSBsZW5ndGgoc3Vtc1tzdW1zID09IDFdKSArIGENCiAgICANCiAgICBqYWNjYXJkX3NpbSA8LSBhL2FfYl9jDQogICAgDQogICAgamFjY2FyZF9zaW1faV90aCA8LSBjKGphY2NhcmRfc2ltX2lfdGgsIGphY2NhcmRfc2ltKQ0KICAgIA0KICB9DQogIA0KICBkZl9yZXN1bHRzIDwtIGRhdGEuZnJhbWUodXNlcklkID0gYWxsX3VzZXJzX3RyYWluLCBzaW1pbGFyaXR5ID0gamFjY2FyZF9zaW1faV90aCkNCiAgDQogIGRmX3Jlc3VsdHMgJT4lIA0KICAgIG11dGF0ZShyZWZlcmVuY2UgPSBjYXNlX3doZW4oc2ltaWxhcml0eSA9PSAxIH4gInllcyIsIFRSVUUgfiAibm8iKSkgJT4lIA0KICAgIHRvcF9uKG4gPSA2LCB3dCA9IHNpbWlsYXJpdHkpICU+JSANCiAgICBhcnJhbmdlKC1zaW1pbGFyaXR5KSAtPiBkZl90b3A1DQogIA0KICByZXR1cm4oZGZfdG9wNSkNCiAgDQp9DQoNCmBgYA0KDQpKUyBjaG8gdOG6pXQgY+G6oyBjw6FjIHVzZXJzIG3DoCBz4bq9IGPDsyBt4bq3dCBj4bqjIHRyxrDhu5tjIHbDoCBzYXUgdGjhu51pIMSRaeG7g20gMjAxOC0wMS0wMTogIA0KDQpgYGB7cn0NCiMgQ29tbW9uIHVzZXJzOiANCg0KYmFzZTo6aW50ZXJzZWN0KGFsbF91c2Vyc190cmFpbiwgYWxsX3VzZXJzX3Rlc3QpIC0+IGNvbW1vbl91c2Vycw0KDQojIEZvciBhbGwgdXNlcnM6IA0KDQpsYXBwbHkoY29tbW9uX3VzZXJzLCB0b3BfNV9qYWNjYXJkKSAtPiBqYWNjYXJkX2Zvcl9hbGwNCg0KIyBGb3IgdGhlIGZpcnN0IDMgdXNlcnM6IA0KDQpqYWNjYXJkX2Zvcl9hbGxbMTozXQ0KDQpgYGANCg0KVmnhur90IGjDoG0gYG1vdmllc19yZWNvbW1lbmRlZF9mb3Jfc3BlY2lmaWNfdXNlcigpYCB0cuG6oyB24buBIG1vdmllSWQgY+G7p2Egbmjhu69uZyBi4buZIHBoaW0gxJHGsOG7o2Mga2h1eeG6v24gbmdo4buLIGNobyBt4buZdCB1c2VyIGNobyB0csaw4bubYzogDQoNCmBgYHtyfQ0KIyBGdW5jdGlvbiByZWNvbW1lbmRzIG1vdmllcyBmb3IgYSBzcGVjaWZpYyB1c2VyOiANCg0KbW92aWVzX3JlY29tbWVuZGVkX2Zvcl9zcGVjaWZpY191c2VyIDwtIGZ1bmN0aW9uKHVzZXJJRCkgew0KICANCiAgdG9wXzVfamFjY2FyZCh1c2VySUQpIC0+IGRmX3NpbV91c2VyDQogIA0KICBkZl9zaW1fdXNlciAlPiUgDQogICAgZmlsdGVyKHNpbWlsYXJpdHkgIT0gMSkgJT4lIA0KICAgIHRvcF9uKG4gPSAxLCB3dCA9IHNpbWlsYXJpdHkpICU+JSANCiAgICBwdWxsKHVzZXJJZCkgJT4lIA0KICAgIGFzLmNoYXJhY3RlcigpIC0+IHVzZXJfdGhhdF9tb3N0X3NpbQ0KICANCiAgdGVzdF9yYXdfZGF0YSAlPiUgDQogICAgbXV0YXRlKHVzZXJJZCA9IGFzLmNoYXJhY3Rlcih1c2VySWQpKSAlPiUgDQogICAgZmlsdGVyKHVzZXJJZCAlaW4lIHVzZXJfdGhhdF9tb3N0X3NpbSkgJT4lIA0KICAgIHB1bGwobW92aWVJZCkgJT4lIA0KICAgIHVuaXF1ZSgpICU+JSANCiAgICBhcy5jaGFyYWN0ZXIoKSAtPiBtb3ZpZXNfcmVjb20NCiAgDQogIHJldHVybih0aWJibGUodXNlcklkID0gdXNlcklELCBtb3ZpZXNfcmVjb20gPSBtb3ZpZXNfcmVjb20pKQ0KICANCn0NCmBgYA0KDQpDaMO6bmcgdGEgY8OzIHRo4buDLCB2w60gZOG7pSwgc+G7rSBk4bulbmcgaMOgbSDEkcOjIGPDsyBsaXN0IHJhIGRhbmggc8OhY2ggY8OhYyBi4buZIHBoaW0gbcOgIHVzZXIgdGjhu6kgbmjhuqV0IG7Dqm4geGVtLiBMxrB1IMO9IHLhurFuZyBkYW5oIHPDoWNoIGPDoWMgYuG7mSBwaGltIMSRw7MgxJHhur9uIHThu6sgY8OhYyBi4buZIHBoaW0gxJHDoyB04burbmcgxJHGsOG7o2MgeGVtIGLhu59pIHVzZXIgY8OzIEpTIGNhbyBuaOG6pXQgLSBoYXkgdMawxqFuZyDEkeG7k25nIGNhbyBuaOG6pXQgduG7m2kgdXNlciB0aOG7qSBuaOG6pXQ6IA0KDQpgYGB7cn0NCm1vdmllc19yZWNvbW1lbmRlZF9mb3Jfc3BlY2lmaWNfdXNlcihjb21tb25fdXNlcnNbMV0pICU+JSBoZWFkKCkNCmBgYA0KDQpWaeG6v3QgaMOgbSBgcmV0dXJuX251bWJlcl9SZWNvbW1lbmRlZG1vdmllc192aWV3ZWQoKWAgxJHhur9tIHPhu5EgYuG7mSBwaGltIG3DoCBFbmdpbmUga2h1eeG6v24gbmdo4buLIHRo4buxYyB04bq/IHPhur0gxJHGsOG7o2MgeGVtIGLhu59pIG3hu5l0IHVzZXIgY2hvIHRyxrDhu5tjOiANCg0KYGBge3J9DQoNCiMgRnVuY3Rpb24gdGVzdA0KDQpyZXR1cm5fbnVtYmVyX1JlY29tbWVuZGVkbW92aWVzX3ZpZXdlZCA8LSBmdW5jdGlvbih1c2VyX2lfdGgpIHsNCiAgDQogIHRlc3RfcmF3X2RhdGEgJT4lIA0KICAgIGZpbHRlcih1c2VySWQgPT0gdXNlcl9pX3RoKSAlPiUgDQogICAgcHVsbChtb3ZpZUlkKSAlPiUNCiAgICB1bmlxdWUoKSAtPiBtb3ZpZXNfYWN0dWFsDQogIA0KICBtb3ZpZXNfcmVjb21tZW5kZWRfZm9yX3NwZWNpZmljX3VzZXIodXNlcl9pX3RoKSAtPiBkZl9yZWNvbQ0KICANCiAgZGZfcmVjb20gJT4lIA0KICAgIHB1bGwobW92aWVzX3JlY29tKSAtPiBtb3ZpZXNfcmVjb20NCiAgDQogIHJldHVybihzdW0obW92aWVzX3JlY29tICVpbiUgbW92aWVzX2FjdHVhbCkpDQp9DQoNCg0KYGBgDQogDQpW4bubaSB1c2VyLCB2w60gZOG7pSwgdGjhu6kgMTAgY2jhurNuZyBo4bqhbiB0aMOsIHPhu5EgYuG7mSBwaGltIG3DoCBuZ8aw4budaSBuw6B5IHPhur0geGVtIHRyb25nIHPhu5EgY8OhYyBi4buZIHBoaW0gxJHGsOG7o2MgRW5naW5lIGtodXnhur9uIG5naOG7iyBsw6A6IA0KDQpgYGB7cn0NCnJldHVybl9udW1iZXJfUmVjb21tZW5kZWRtb3ZpZXNfdmlld2VkKGNvbW1vbl91c2Vyc1sxMF0pDQpgYGANCg0KQ2jDum5nIHRhIGPDsyB0aOG7gyDEkW8gbMaw4budbmcgaGnhu4d1IHF14bqjIHbhuq1uIGjDoG5oIGPhu6dhIFJlY29tbWVuZGVyIEVuZ2luZSBi4bqxbmcgY8OhYyBzbyBzw6FuaCBuaOG7r25nIG1vdmllcyDEkcaw4bujYyBo4buHIHRo4buRbmcga2h1eeG6v24gbmdo4buLIHbhu5tpIG5o4buvbmcgYuG7mSBwaGltIG3DoCB1c2VycyBz4bq9IHhlbSBzYXUgdGjhu51pIMSRaeG7g20gMjAxOC0wMS0wMTogDQoNCmBgYHtyfQ0Kc3VtKHNhcHBseShjb21tb25fdXNlcnMsIHJldHVybl9udW1iZXJfUmVjb21tZW5kZWRtb3ZpZXNfdmlld2VkKSAhPSAwKSAvIGxlbmd0aChjb21tb25fdXNlcnMpDQpgYGANCiANCkvhur90IHF14bqjIG7DoHkgbmdoxKlhIGzDoCAzNSUgc+G7kSB1c2VycyBzYXUgbmfDoHkgMjAxOC0wMS0wMSBz4bq9IHhlbSDDrXQgbmjhuqV0IG3hu5l0IG1vdmllIG3DoCBo4buHIHRo4buRbmcga2h1eeG6v24gbmdo4buLIGNobyBo4buNLg0KDQojIFN1bW1hcnkNCg0KUmVjb21tZW5kZXIgRW5naW5lIMSRxrDhu6NjIHjDonkgZOG7sW5nIGThu7FhIHRyw6puIGThu68gbGnhu4d1IGPDsyB0csaw4bubYyBuZ8OgeSAyMDE4LTAxLTAxIMSR4buDIMSRxrBhIHJhIGPDoWMgYuG7mSBwaGltIMSRxrDhu6NjIGtodXnhur9uIG5naOG7iyBjaG8gY8OhYyB1c2VycyBk4buxYSB0csOqbiDEkeG7mSB0xrDGoW5nIMSR4buTbmcgSlMuIENo4bqldCBsxrDhu6NuZyBj4bunYSBFbmdpbmUgxJHGsOG7o2MgdMOtbmggdG/DoW4gYuG6sW5nIGPDoWNoIHNvIHPDoW5oIGPDoWMgYuG7mSBwaGltIGtodXnhur9uIG5naOG7iyBi4bufaSBFbmdpbmUgdsOgIGPDoWMgYuG7mSBwaGltIHRo4buxYyB04bq/IHPhur0gxJHGsOG7o2MgeGVtIGLhu59pIGPDoWMgdXNlcnMuIEPDoWMgY8OhY2ggdGnhur9wIGPhuq1uIGtow6FjIGNobyB2aeG7h2MgxJHDoW5oIGdpw6EgRW5naW5lIGPDoWMgYuG6oW4gY8OzIHRo4buDIHPhu60gZOG7pW5nIGPDoWMgaMOgbSBz4bq1biBjw7MgY+G7p2EgdGjGsCB2aeG7h24gW3JlY29tbWVuZGVybGFiXShodHRwczovL2NyYW4uci1wcm9qZWN0Lm9yZy93ZWIvcGFja2FnZXMvcmVjb21tZW5kZXJsYWIvaW5kZXguaHRtbCkgY+G7p2EgW01pY2hhZWwgSGFoc2xlcl0oaHR0cHM6Ly9zMi5zbXUuZWR1L0lEQS9yZWNvbW1lbmRlcmxhYi8pIHbhu5FuIMSRxrDhu6NjIHRyw6xuaCBiw6B5IHRyb25nIG5oaeG7gXUgdGV4dGJvb2sgdsOgIHPhur0ga2jDtG5nIMSRxrDhu6NjIHRyw6xuaCBiw6B5IOG7nyDEkcOieS4gDQoNCiMgUmVmZXJlbmNlcw0KDQoxLiBbTWluaW5nIG9mIE1hc3NpdmUgRGF0YXNldHMsIENoYXB0ZXIgM10oaHR0cDovL2luZm9sYWIuc3RhbmZvcmQuZWR1L351bGxtYW4vbW1kcy9jaDMucGRmKQ0KMi4gW0EgU3VydmV5IG9mIEFjY3VyYWN5IEV2YWx1YXRpb24gTWV0cmljcyBvZiBSZWNvbW1lbmRhdGlvbiBUYXNrc10oaHR0cDovL2ptbHIuY3NhaWwubWl0LmVkdS9wYXBlcnMvdm9sdW1lMTAvZ3VuYXdhcmRhbmEwOWEvZ3VuYXdhcmRhbmEwOWEucGRmKS4gDQozLiBbRXZhbHVhdGluZyBSZWNvbW1lbmRhdGlvbiBTeXN0ZW1zXShodHRwOi8vd3d3LmJndS5hYy5pbC9+c2hhbmlndS9QdWJsaWNhdGlvbnMvRXZhbHVhdGlvbk1ldHJpY3MuMTcucGRmKS4gDQo0LiBbRXZhbHVhdGluZyBDb2xsYWJvcmF0aXZlIEZpbHRlcmluZyBSZWNvbW1lbmRlciBTeXN0ZW1zXShodHRwczovL2dyb3VwbGVucy5vcmcvc2l0ZS1jb250ZW50L3VwbG9hZHMvZXZhbHVhdGluZy1UT0lTLTIwMDQxLnBkZikuIA0KNS4gW1JlY29tbWVuZGVyIFN5c3RlbXM6IEFuIEludHJvZHVjdGlvbl0oaHR0cHM6Ly93d3cuYW1hem9uLmNvbS9SZWNvbW1lbmRlci1TeXN0ZW1zLUludHJvZHVjdGlvbi1EaWV0bWFyLUphbm5hY2gvZHAvMDUyMTQ5MzM2Ni9yZWY9cGRfc2JzXzE0Xzc/X2VuY29kaW5nPVVURjgmcGRfcmRfaT0wNTIxNDkzMzY2JnBkX3JkX3I9YzgxZWFjYTctMzUzYS00MzY3LWE5YjEtZmE1ZTc3ZDg0MjJjJnBkX3JkX3c9R3kzT1YmcGRfcmRfd2c9eUJ5V1cmcGZfcmRfcD1iYzA3NDA1MS04MWQxLTQ4NzQtYTNmZC1mZDBjODY3Y2UzYjQmcGZfcmRfcj1TMlJLVDlWV1BYTllDWTlRUEVNVyZwc2M9MSZyZWZSSUQ9UzJSS1Q5VldQWE5ZQ1k5UVBFTVcpLiANCiA=