Austin Bike Data Exploration

Data set and description can be found here

Github page with cleaned up data and presentation here

Libraries I might use

library(ISLR)
library(tidyverse)
library(randomForest)
library(gbm)
library(MASS)
library(lubridate)
library(zoo)
library(GGally)
library(e1071) #SVM and NB library

Loading in the Data

bikedata = read.csv("Austin_MetroBike_Trips.csv")
weatherdata = read.csv("austindaily.csv")
bikedata
weatherdata

Data Cleaning

Weather Data

weatherclean = filter(weatherdata, year(X1938.06.01)>2012)
weatherclean = weatherclean[,c(1,2)]
colnames(weatherclean) <- c("Date","AvgTemp")
weatherclean$Date = as.Date(strptime(weatherclean$Date, format = '%Y-%m-%d'))
summary(weatherclean)
      Date               AvgTemp
 Min.   :2013-01-01   Min.   :-9.20
 1st Qu.:2015-02-12   1st Qu.:15.20
 Median :2017-03-26   Median :22.00
 Mean   :2017-03-26   Mean   :20.86
 3rd Qu.:2019-05-07   3rd Qu.:27.50
 Max.   :2021-06-20   Max.   :34.00
                      NA's   :88     

But we can see we have 80 dates with missing temperature values. I would like to impute an average around it, but is that a sound strategy?

set.seed(123)
weathercleannn=na.omit(weatherclean)
fakeweather = weathercleannn
nas = sample (1:nrow(weathercleannn), nrow(weathercleannn)*0.02934703) # The same rate of missing values as in our original data set
fakeweather$AvgTemp[nas] <- NA
fakeweather$AvgTemp = (na.locf(fakeweather$AvgTemp) + rev(na.locf(rev(fakeweather$AvgTemp))))/2 # Fills in missing values with the average of the weather values on either side
mean((weathercleannn$AvgTemp[nas]-fakeweather$AvgTemp[nas])^2)
[1] 5.109943

Great! We are in a reasonable range of the true temperature and can apply this method to our dataset.

weatherclean$AvgTemp = (na.locf(weatherclean$AvgTemp) + rev(na.locf(rev(weatherclean$AvgTemp))))/2
weatherclean

Bike data

bikeclean = bikedata[,c(2,4,5,7,9,10)]
bikeclean[bikeclean==""] <- NA
bikeclean$Checkout.Time=hms(bikeclean$Checkout.Time)
bikeclean$Checkout.Date = as.Date(bikeclean$Checkout.Date,format='%m/%d/%Y')
summary((bikeclean))
 Membership.Type    Checkout.Date        Checkout.Time                       Checkout.Kiosk     Return.Kiosk       Trip.Duration.Minutes
 Length:1342066     Min.   :2013-12-21   Min.   :0S                          Length:1342066     Length:1342066     Min.   :    0.00
 Class :character   1st Qu.:2015-12-11   1st Qu.:12H 3M 46S                  Class :character   Class :character   1st Qu.:    6.00
 Mode  :character   Median :2017-09-21   Median :15H 12M 0S                  Mode  :character   Mode  :character   Median :   13.00
                    Mean   :2017-06-08   Mean   :14H 46M 47.7450237171652S                                         Mean   :   30.59
                    3rd Qu.:2018-08-27   3rd Qu.:18H 12M 0S                                                        3rd Qu.:   28.00
                    Max.   :2021-02-28   Max.   :23H 59M 56S                                                       Max.   :34238.00     

Checkout Kiosk

sort(unique(bikeclean$Checkout.Kiosk))
  [1] "10th & Red River"                                                 "10th/Red River"                                                   "11th & Salina"
  [4] "11th & Salina "                                                   "11th & San Jacinto"                                               "11th/Congress @ The Texas Capitol"
  [7] "11th/Salina "                                                     "11th/San Jacinto"                                                 "12th/San Jacinto @ State Capitol Visitors Garage"
 [10] "13th & San Antonio"                                               "13th/San Antonio"                                                 "16th/San Antonio"
 [13] "17th & Guadalupe"                                                 "17th/Guadalupe"                                                   "21st & Speedway @PCL"
 [16] "21st & University"                                                "21st/Guadalupe"                                                   "21st/Speedway @ PCL"
 [19] "21st/University"                                                  "22nd & Pearl"                                                     "22nd/Pearl"
 [22] "23rd & Rio Grande"                                                "23rd & San Jacinto @ DKR Stadium"                                 "23rd/Rio Grande"
 [25] "23rd/San Jacinto @ DKR Stadium"                                   "26th/Nueces"                                                      "28th/Rio Grande"
 [28] "2nd & Congress"                                                   "2nd/Congress"                                                     "2nd/Lavaca @ City Hall"
 [31] "3rd & West"                                                       "3rd/Nueces"                                                       "3rd/Trinity @ The Convention Center"
 [34] "3rd/West"                                                         "4th & Congress"                                                   "4th/Congress"
 [37] "4th/Guadalupe @ Republic Square"                                  "4th/Neches @ MetroRail Downtown"                                  "4th/Sabine"
 [40] "5th & Bowie"                                                      "5th & Campbell"                                                   "5th & San Marcos"
 [43] "5th/Bowie"                                                        "5th/Campbell"                                                     "5th/Guadalupe @ Republic Square"
 [46] "6th & Chalmers"                                                   "6th & Chalmers "                                                  "6th & Congress"
 [49] "6th & Navasota St."                                               "6th/Brazos"                                                       "6th/Chalmers "
 [52] "6th/Congress"                                                     "6th/Lavaca"                                                       "6th/Trinity"
 [55] "6th/West"                                                         "8th & Congress"                                                   "8th & Guadalupe"
 [58] "8th & Lavaca"                                                     "8th/Congress"                                                     "8th/Lavaca"
 [61] "8th/Red River"                                                    "8th/San Jacinto"                                                  "9th/Henderson"
 [64] "ACC - Rio Grande & 12th"                                          "ACC - West & 12th"                                                "ACC - West & 12th Street"
 [67] "Barton Springs & Riverside"                                       "Barton Springs @ Kinney Ave"                                      "Barton Springs Pool"
 [70] "Barton Springs/Bouldin @ Palmer Auditorium"                       "Barton Springs/Kinney"                                            "Boardwalk West"
 [73] "Brazos & 6th"                                                     "Bullock Museum @ Congress & MLK"                                  "Capital Metro HQ - East 5th at Broadway"
 [76] "Capitol Station / Congress & 11th"                                "cesar Chavez/Congress"                                            "Cesar Chavez/Congress"
 [79] "City Hall / Lavaca & 2nd"                                         "Congress & Cesar Chavez"                                          "Convention Center / 3rd & Trinity"
 [82] "Convention Center / 4th St. @ MetroRail"                          "Convention Center/ 3rd & Trinity"                                 "Customer Service"
 [85] "Davis at Rainey Street"                                           "Dean Keeton & Speedway"                                           "Dean Keeton & Speedway "
 [88] "Dean Keeton & Whitis"                                             "Dean Keeton/Speedway"                                             "Dean Keeton/Speedway "
 [91] "Dean Keeton/Whitis"                                               "East 11th St. & San Marcos"                                       "East 11th St. at Victory Grill"
 [94] "East 11th Street at Victory Grill"                                "East 11th/San Marcos"                                             "East 11th/Victory Grill"
 [97] "East 2nd & Pedernales"                                            "East 2nd/Pedernales"                                              "East 4th & Chicon"
[100] "East 4th/Chicon"                                                  "East 5th/Broadway @ Capital Metro HQ"                             "East 5th/Shady @ Eastside Bus Plaza"
[103] "East 5th/Shady Ln"                                                "East 6th & Pedernales St."                                        "East 6th at Robert Martinez"
[106] "East 6th/Medina"                                                  "East 6th/Pedernales"                                              "East 6th/Robert T. Martinez"
[109] "East 7th & Pleasant Valley"                                       "Eeyore's 2017"                                                    "Eeyore's 2018"
[112] "Electric Drive/Sandra Muraida Way @ Pfluger Ped Bridge"           "Guadalupe & 21st"                                                 "Guadalupe & 6th"
[115] "Guadalupe/West Mall @ University Co-op"                           "Henderson & 9th"                                                  "Hollow Creek & Barton Hills"
[118] "Hollow Creek/Barton Hills"                                        "Lake Austin & Enfield"                                            "Lake Austin Blvd @ Deep Eddy"
[121] "Lake Austin Blvd/Deep Eddy"                                       "Lake Austin/Enfield"                                              "Lakeshore & Pleasant Valley"
[124] "Lakeshore @ Austin Hostel"                                        "Lakeshore/Austin Hostel"                                          "Lakeshore/Pleasant Valley"
[127] "Lavaca & 6th"                                                     "Long Center @ South 1st & Riverside"                              "Main Office"
[130] "MapJam at French Legation"                                        "MapJam at Hops & Grain Brewery"                                   "MapJam at Pan Am Park"
[133] "MapJam at Scoot Inn"                                              "Marketing Event"                                                  "Medina & East 6th"
[136] "Mobile Station"                                                   "Mobile Station @ Bike Fest"                                       "Mobile Station @ Boardwalk Opening Ceremony"
[139] "Mobile Station @ Unplugged"                                       "MoPac Pedestrian Bridge @ Veterans Drive"                         "Nash Hernandez @ RBJ South"
[142] "Nash Hernandez/East @ RBJ South"                                  "Nueces & 26th"                                                    "Nueces & 3rd"
[145] "Nueces @ 3rd"                                                     "Palmer Auditorium"                                                "Pease Park"
[148] "Pfluger Bridge @ W 2nd Street"                                    "Plaza Saltillo"                                                   "Rainey @ River St"
[151] "Rainey St @ Cummings"                                             "Rainey/Cummings"                                                  "Rainey/Davis"
[154] "Re-branding"                                                      "Red River & 8th Street"                                           "Red River @ LBJ Library"
[157] "Red River/Cesar Chavez @ The Fairmont"                            "Repair Shop"                                                      "Republic Square"
[160] "Republic Square @ 5th & Guadalupe"                                "Republic Square @ Federal Courthouse Plaza"                       "Republic Square @ Guadalupe & 4th St."
[163] "Rio Grande & 28th"                                                "Riverside @ S. Lamar"                                             "Riverside/South Lamar"
[166] "Rosewood & Angelina"                                              "Rosewood & Chicon"                                                "Rosewood/Angelina"
[169] "Rosewood/Chicon"                                                  "San Jacinto & 8th Street"                                         "Shop"
[172] "South 1st/Riverside @ Long Center"                                "South Congress & Academy"                                         "South Congress & Barton Springs at the Austin American-Statesman"
[175] "South Congress & Elizabeth"                                       "South Congress & James"                                           "South Congress @ Bouldin Creek"
[178] "South Congress/Academy"                                           "South Congress/Barton Springs @ The Austin American-Statesman"    "South Congress/Elizabeth"
[181] "South Congress/James"                                             "State Capitol @ 14th & Colorado"                                  "State Capitol Visitors Garage @ San Jacinto & 12th"
[184] "State Parking Garage @ Brazos & 18th"                             "Sterzing at Barton Springs"                                       "Sterzing/Barton Springs"
[187] "Stolen"                                                           "Toomey Rd @ South Lamar"                                          "Trinity & 6th Street"
[190] "UT West Mall @ Guadalupe"                                         "Veterans/Atlanta @ MoPac Ped Bridge"                              "Waller & 6th St."
[193] "West & 6th St."                                                   "Zilker Park"                                                      "Zilker Park at Barton Springs & William Barton Drive"
[196] "Zilker Park West"                                                

Oh no, we have a lot of cleaning to do here

tmp = tolower(bikeclean$Checkout.Kiosk)
tmp = gsub("\\.", " ",tmp)
tmp = gsub("&", "/",tmp)
tmp = gsub("street", "",tmp)
tmp = gsub(" st ", "",tmp)
tmp = gsub(" at ", "@",tmp)
tmp = gsub("@", "/",tmp)
tmp = gsub(" ", "",tmp)
tmp = data.frame(tmp)
tmp = separate(data = tmp, col = 1, into = c("1st","2nd","3rd","4th"), sep = "[^[:alnum:]]+")
Expected 4 pieces. Missing pieces filled with `NA` in 1323839 rows [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...].
tmp[is.na(tmp)] <- "ZZZZZ" # Sort doesn't like NA values, so I'm replacing with ZZZZ to go to the end of the row
tmp = data.frame(t(apply(tmp,1,sort))) # Sorts by row
tmp = unite(tmp, united, sep="/")
tmp = gsub("/ZZZZZ", "",tmp$united)
tmp = gsub("3rd/theconventioncenter/trinity","3rd/conventioncenter/trinity",tmp)
tmp = gsub("slamar","southlamar",tmp)
tmp = gsub("bartonsprings/kinneyave","bartonsprings/kinney",tmp)
tmp = gsub("branding/re","rebranding",tmp)
tmp = gsub("guadalupe/op/universityco/westmall","guadalupe/utwestmall",tmp)
tmp = gsub("east6th/roberttmartinez","east6th/robertmartinez",tmp)
bikeclean$Checkout.Kiosk = tmp

Return Kiosk

tmp = tolower(bikeclean$Return.Kiosk)
tmp = gsub("\\.", " ",tmp)
tmp = gsub("&", "/",tmp)
tmp = gsub("street", "",tmp)
tmp = gsub(" st ", "",tmp)
tmp = gsub(" at ", "@",tmp)
tmp = gsub("@", "/",tmp)
tmp = gsub(" ", "",tmp)
tmp = data.frame(tmp)
tmp = separate(data = tmp, col = 1, into = c("1st","2nd","3rd","4th"), sep = "[^[:alnum:]]+")
Expected 4 pieces. Missing pieces filled with `NA` in 1323892 rows [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...].
tmp[is.na(tmp)] <- "ZZZZZ" # Sort doesn't like NA values, so I'm replacing with ZZZZ to go to the end of the row
tmp = data.frame(t(apply(tmp,1,sort))) # Sorts by row
tmp = unite(tmp, united, sep="/")
tmp = gsub("/ZZZZZ", "",tmp$united)
tmp = gsub("3rd/theconventioncenter/trinity","3rd/conventioncenter/trinity",tmp)
tmp = gsub("slamar","southlamar",tmp)
tmp = gsub("bartonsprings/kinneyave","bartonsprings/kinney",tmp)
tmp = gsub("branding/re","rebranding",tmp)
tmp = gsub("guadalupe/op/universityco/westmall","guadalupe/utwestmall",tmp)
tmp = gsub("east6th/roberttmartinez","east6th/robertmartinez",tmp)

Let’s compare the drop off and pickup groups to see if we need to recode anything.

comparison =  data.frame(sort(unique(tmp)),c(sort(unique(bikeclean$Checkout.Kiosk)), NA, NA, NA,NA))
colnames(comparison) <- c("Return", "Checkout")
comparison$Return[!comparison$Return %in% comparison$Checkout]
[1] "acl2019dropoff"  "earthdayatx2017" "fantasyzilker"   "mainshop"        "missing"        
comparison$Checkout[!comparison$Checkout %in% comparison$Return]
[1] "eeyore/s2018" NA             NA             NA             NA            

Mainshop should be recoded to shop

tmp = gsub("mainshop","shop",tmp)
bikeclean$Return.Kiosk = tmp

Membership types

I want to encode these as the following:

Single Trip

“$1 Pay by Trip Fall Special”,“Single Trip”,“Try Before You Buy Special”,“$1 Pay by Trip Winter Special”,“RideScout Single Ride”,“Single Trip”,“Single Trip Ride”, “Pay-as-you-ride”,“Single Trip (Pay-as-you-ride)”,“24-Hour Kiosk (Austin B-cycle)”,“24 Hour Walk Up Pass”,“Walk Up”

Daily

“24-Hour-Online (Austin B-cycle)”,“24-Hour Membership (Austin B-cycle)”,“Explorer”,“Explorer ($8 plus tax)”

3-Day

“3-Day Explorer”,“Weekender”,“3-Day Weekender”,“Weekender ($15 plus tax)”

Weekly

“7-Day”,7-Day Membership (Austin B-cycle),“7-Day Membership (Austin B-cycle)”

Monthly

“Local30 ($11 plus tax)”,“Local30”,“Local31”

Annual

“Annual”,“Annual Membership”,“Annual Pass”,“Annual Plus”,“Local365”,“Local365 ($80 plus tax)”,“Local365 Youth (age 13-17 riders)- 1/2,”Local365+Guest Pass“,”Annual“,”Annual Member“,”Annual Membership“,”Annual Membership (Austin B-cycle)“,”Annual Pass (30 minute)“,”Annual Plus Membership“,”Local365- 1/2 off Anniversary Special“,”Local365 Youth (age 13-17 riders)“,Special” “Local365 Youth with helmet (age 13-17 riders)”,“Local365+Guest Pass- 1/2 off Anniversary Special”,“Membership: pay once one-year commitment”

Student

“HT Ram Membership”,“Semester Membership”,“UT Student Membership”,“Semester Membership (Austin B-cycle)”,“U.T. Student Membership”,“U.T. Student Membership”

Event

“ACL 2019 Pass”,“ACL Weekend Pass Special (Austin B-cycle)”,“FunFunFun Fest 3 Day Pass”

Share

“Annual (Broward B-cycle)”,“Annual (Denver B-cycle)”,“Annual (Kansas City B-cycle)”,“Annual (Nashville B-cycle)”,“Annual (San Antonio B-cycle)”,“Annual Member (Houston B-cycle)”,“Annual Membership (Charlotte B-cycle)”,“Annual Membership (GREENbike)”,“Denver B-cycle Founder”,“Heartland Pass (Annual Pay)”,“Madtown Monthly”,“Republic Rider”,“Annual (Cincy Red Bike)”,“Annual (Omaha B-cycle)”,“Annual (Madison B-cycle)”,“Annual (Denver Bike Sharing)”,“Annual Membership (Fort Worth Bike Sharing)”,“Heartland Pass (Monthly Pay)”,“Republic Rider (Annual)”

Other “Aluminum Access”,“Founding Member (Austin B-cycle)”,“Founding Member”

Missing NA, “PROHIBITED”, “RESTRICTED”

bikeclean$Membership.Type[is.na(bikeclean$Membership.Type)] = "missing"
bikeclean$Membership.Type[which(bikeclean$Membership.Type %in% c("PROHIBITED", "RESTRICTED"))]="missing"
bikeclean$Membership.Type[which(bikeclean$Membership.Type %in% c("3-Day Explorer","Weekender","3-Day Weekender","Weekender ($15 plus tax)"))]="3 Day"
bikeclean$Membership.Type[which(bikeclean$Membership.Type %in% c("$1 Pay by Trip Fall Special","Single Trip","Try Before You Buy Special","$1 Pay by Trip Winter Special","RideScout Single Ride","Single Trip ","Single Trip Ride", "Pay-as-you-ride","Single Trip (Pay-as-you-ride)","24-Hour Kiosk (Austin B-cycle)", "24 Hour Walk Up Pass","Walk Up"))]="Single Trip"
bikeclean$Membership.Type[which(bikeclean$Membership.Type %in% c("24-Hour-Online (Austin B-cycle)","24-Hour Membership (Austin B-cycle)","Explorer","Explorer ($8 plus tax)"))]="Daily"
bikeclean$Membership.Type[which(bikeclean$Membership.Type%in%c("7-Day","7-Day Membership (Austin B-cycle)","7-Day Membership (Austin B-cycle)"))]="Weekly"
bikeclean$Membership.Type[which(bikeclean$Membership.Type%in%c("Local30 ($11 plus tax)","Local30","Local31"))]="Monthly"
bikeclean$Membership.Type[which(bikeclean$Membership.Type%in%c("Annual ","Annual Membership ","Annual Pass","Annual Plus","Local365","Local365 ($80 plus tax)","Local365 Youth (age 13-17 riders)- 1/2" ,"Local365+Guest Pass","Annual","Annual Member","Annual Membership","Annual Membership (Austin B-cycle)","Annual Pass (30 minute)","Annual Plus Membership","Local365- 1/2 off Anniversary Special","Local365 Youth (age 13-17 riders)", "Local365 Youth with helmet (age 13-17 riders)","Local365+Guest Pass- 1/2 off Anniversary Special","Membership: pay once  one-year commitment","Local365 Youth (age 13-17 riders)- 1/2 off Special"))]="Annual"
bikeclean$Membership.Type[which(bikeclean$Membership.Type%in%c("HT Ram Membership","Semester Membership","UT Student Membership","Semester Membership (Austin B-cycle)","U.T. Student Membership","U.T. Student Membership"))]="Student"
bikeclean$Membership.Type[which(bikeclean$Membership.Type%in%c("ACL 2019 Pass","ACL Weekend Pass Special (Austin B-cycle)","FunFunFun Fest 3 Day Pass"))]="Event"
bikeclean$Membership.Type[which(bikeclean$Membership.Type%in%c("Aluminum Access","Founding Member (Austin B-cycle)","Founding Member"))]="Other"

bikeclean$Membership.Type[which(bikeclean$Membership.Type%in%c("Annual (Broward B-cycle)","Annual (Denver B-cycle)","Annual (Kansas City B-cycle)","Annual (Nashville B-cycle)","Annual (San Antonio B-cycle)","Annual Member (Houston B-cycle)","Annual Membership (Charlotte B-cycle)","Annual Membership (GREENbike)","Denver B-cycle Founder","Heartland Pass (Annual Pay)","Madtown Monthly","Republic Rider","Annual (Cincy Red Bike)","Annual (Omaha B-cycle)","Annual (Madison B-cycle)","Annual (Denver Bike Sharing)","Annual Membership (Fort Worth Bike Sharing)","Heartland Pass (Monthly Pay)","Republic Rider (Annual)", "Annual Membership (Indy - Pacers Bikeshare )", "Annual (Boulder B-cycle)"))]="Share"
sort(unique(bikeclean$Membership.Type))
 [1] "3 Day"       "Annual"      "Daily"       "Event"       "missing"     "Monthly"     "Other"       "Share"       "Single Trip" "Student"
[11] "Weekly"     

Looking good! I might try and predict our missing membership types if I have time.

Merging our Datasets

merged = merge(bikeclean,weatherclean,by.x = "Checkout.Date",by.y ="Date")
saveRDS(merged,file = 'merged.rds')

Now I don’t have to run everything above this EVERY time I need to start over.

merged = readRDS("merged.rds")
merged$Membership.Type=factor(merged$Membership.Type)

Data visualization

ggplot(merged)+
  geom_point(aes(x = Checkout.Date, y=round(Trip.Duration.Minutes/60)))+
  geom_hline(yintercept=1,color = "red")

Trips are expected to be at 1 hour in length. Any more than that and there is a fine associated with each minute over you ride. as time has gone on, more people aren’t checking their bikes in on time.

I wonder what’s up with the weird gaps in 2016.

ggplot(merged[which(year(merged$Checkout.Date)==2016),])+
    geom_point(aes(x = Checkout.Date, y=round(Trip.Duration.Minutes/60)))

ggplot(merged[which(year(merged$Checkout.Date)==2016 & month(merged$Checkout.Date)==4),])+
      geom_point(aes(x = Checkout.Date, y=round(Trip.Duration.Minutes/60)))

merged[which(year(merged$Checkout.Date)==2016 & month(merged$Checkout.Date)==4),]

Great, no data for that month.

merged[which(year(merged$Checkout.Date)==2016 & month(merged$Checkout.Date)==12),]

Same with December.

ggplot(merged)+
  geom_bar(aes(x=factor(month(Checkout.Date))), fill = "blue")+
  facet_wrap(~year(Checkout.Date))

We can see there is a cyclical nature to the number of trips taken per month. Intuitively, there are more riders when the weather is nice.

summary(merged$AvgTemp)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
  -9.20   17.30   22.40   21.81   27.20   34.00 
ggplot(merged)+
  geom_histogram(aes(x= AvgTemp), bins = 45, fill = "blue")

Most people ride between and 12 and 32 degrees Celsius

merged %>%
  group_by(Checkout.Kiosk) %>%
  summarise(Departing_Bikes = length(Checkout.Kiosk)) %>% arrange(.,desc(Departing_Bikes))
merged %>%
  ggplot() +
  geom_bar(aes(x=Checkout.Kiosk), fill = "red")+
  theme(axis.title.x=element_blank(), axis.text.x=element_blank(), axis.ticks.x=element_blank())

Bikes leave the campus Speedway at a very high rate

merged %>%
  ggplot() +
  geom_bar(aes(x=Return.Kiosk), fill = "blue")+
  theme(axis.title.x=element_blank(), axis.text.x=element_blank(), axis.ticks.x=element_blank())

merged %>%
  group_by(Return.Kiosk) %>%
  summarise(Departing_Bikes = length(Return.Kiosk)) %>% arrange(.,desc(Departing_Bikes))

Even more are returned there

Daily Differential Requiring Human Intervention

checkout = merged %>% group_by(Checkout.Date, Checkout.Kiosk) %>% tally() %>% spread(Checkout.Date, n)
checkout = checkout[-which(checkout$Checkout.Kiosk == "eeyore/s2018"),]
return = merged %>% group_by(Checkout.Date, Return.Kiosk) %>% tally() %>% spread(Checkout.Date, n)
return = return[-which(!return$Return.Kiosk %in% checkout$Checkout.Kiosk),]
checkout[is.na(checkout)] = 0
return[is.na(return)]= 0
diff = data.frame(return$Return.Kiosk,return[,-1]-checkout[,-1])
diff
daily_diff = data.frame(colSums(abs(diff[,-1])))
daily_diff <- rownames_to_column(daily_diff, "Date")
daily_diff$Date = gsub( "X", "", daily_diff$Date)
daily_diff$Date = as.Date(strptime(daily_diff$Date, format = '%Y.%m.%d'))
colnames(daily_diff) = c("Date","Diff")
daily_diff
ggplot(daily_diff)+
  geom_bar(aes(x=Date, y = Diff), stat = "identity")

daily_diff = merge(daily_diff,weatherclean,by.x = "Date",by.y ="Date")
daily_diff$month = month(daily_diff$Date)
xgrid = seq(-10,50,.001)
ggplot(daily_diff) +
  geom_point(aes(x = AvgTemp, y = Diff, color =factor(month(Date))))

This seems like one of those things we would want to predict so that we can know staffing levels. Let’s try to predict it with a random forest.

Random Forest

head(daily_diff)
set.seed(123)
train = sample (1:nrow(daily_diff), nrow(daily_diff)/2)
rf.dd= randomForest(Diff ~ Date + AvgTemp,data=daily_diff, subset=train)
yhat.rf = predict(rf.dd ,newdata=daily_diff[- train ,])
mean((yhat.rf-daily_diff[-train, "Diff"])^2)
[1] 2518.162
ggplot()+
  geom_point(aes(x=daily_diff$Diff[-train],y = yhat.rf))+
  geom_abline(color = "red")

summary(daily_diff)
      Date                 Diff          AvgTemp          month
 Min.   :2013-12-21   Min.   :  6.0   Min.   :-9.20   Min.   : 1.000
 1st Qu.:2015-09-24   1st Qu.: 90.0   1st Qu.:15.00   1st Qu.: 3.000
 Median :2017-08-26   Median :118.0   Median :22.10   Median : 6.000
 Mean   :2017-08-03   Mean   :131.3   Mean   :20.84   Mean   : 6.393
 3rd Qu.:2019-05-29   3rd Qu.:154.0   3rd Qu.:27.60   3rd Qu.: 9.000
 Max.   :2021-02-28   Max.   :726.0   Max.   :34.00   Max.   :12.000  

Not terrible, but there are some areas where the model gets it pretty wrong

Let’s try boosting

set.seed(12)
boost.dd=gbm(Diff~AvgTemp+month(Date)+year(Date)+day(Date),data=daily_diff[train ,], distribution="gaussian",n.trees=5000, shrinkage = .01,interaction.depth = 3)
best.iter <- gbm.perf(boost.dd, method = "OOB", plot = FALSE)
OOB generally underestimates the optimal number of iterations although predictive performance is reasonably competitive. Using cv_folds>1 when calling gbm usually results in improved predictive performance.
yhat.boost=predict (boost.dd ,newdata =daily_diff[-train ,], n.trees=best.iter)
mean((yhat.boost-daily_diff[-train, "Diff"])^2)
[1] 3167.117
ggplot()+
  geom_point(aes(x=daily_diff$Diff[-train],y = yhat.boost))+
  geom_abline(color = "red")

daily_diff[daily_diff$Diff>300, 'Date']
 [1] "2014-03-12" "2014-03-14" "2014-03-15" "2014-10-03" "2014-10-04" "2014-10-05" "2014-10-10" "2014-10-11" "2014-10-12" "2015-03-13" "2015-03-14"
[12] "2015-03-15" "2015-03-16" "2015-03-17" "2015-03-18" "2015-03-19" "2015-03-20" "2015-10-02" "2015-10-03" "2015-10-04" "2015-10-09" "2015-10-10"
[23] "2015-10-11" "2016-01-23" "2016-01-30" "2016-01-31" "2016-03-12" "2016-03-13" "2016-03-16" "2016-03-19" "2016-09-30" "2016-10-01" "2016-10-02"
[34] "2016-10-07" "2016-10-08" "2016-10-09" "2017-03-13" "2017-03-14" "2017-03-15" "2017-03-16" "2017-03-17" "2017-03-18" "2017-10-06" "2017-10-07"
[45] "2017-10-08" "2017-10-13" "2017-10-14" "2017-10-15" "2018-02-19" "2018-03-01" "2018-03-02" "2018-03-06" "2018-03-08" "2018-03-09" "2018-03-10"
[56] "2018-03-11" "2018-03-13" "2018-03-14" "2018-03-15" "2018-03-16" "2018-03-17" "2018-03-18" "2018-03-20" "2018-03-21" "2018-03-22" "2018-03-23"
[67] "2018-03-28" "2018-03-29" "2018-04-02" "2018-04-04" "2018-04-06" "2018-04-10" "2018-04-11" "2018-04-12" "2018-04-13" "2018-04-17" "2018-04-18"
[78] "2018-04-19" "2018-04-20" "2018-04-23" "2018-04-26" "2018-04-27" "2018-05-01" "2018-05-03" "2018-05-08" "2018-10-05" "2018-10-06" "2018-10-07"
[89] "2018-10-13"

ANN Try

library(neuralnet)
library(NeuralNetTools)
daily_diff$year = year(daily_diff$Date)
daily_diff$day = day(daily_diff$Date)
dd.scaled <- as.data.frame(scale(daily_diff[,-1]))
min.diff <- min(daily_diff$Diff)
max.diff <- max(daily_diff$Diff)
dd.scaled
dd.scaled$Diff <- scale(daily_diff$Diff, center = min.diff, scale = max.diff - min.diff)
dd.split <- sample(dim(daily_diff)[1],dim(daily_diff)[1]/2 )
# Train-test split
dd.train.scaled <- dd.scaled[dd.split, ]
dd.test.scaled <- dd.scaled[-dd.split, ]
generate.full.fmla<- function(df, response){
  names(df)[!(names(df) == response)]%>%
    paste(.,collapse = "+")%>%
     paste(response, "~", .)%>%
    formula(.)
}
dd.nn.fmla <- generate.full.fmla(dd.train.scaled, "Diff")
dd.nn.5.3 <- neuralnet(dd.nn.fmla
                           , data=dd.train.scaled
                           , hidden=c(5,3)
                           , linear.output=TRUE)
dd.nn.8 <- neuralnet(dd.nn.fmla
                           , data=dd.train.scaled
                           , hidden=8
                           , linear.output=TRUE)
rf.dd= randomForest(dd.nn.fmla,data=dd.train.scaled)
fitness.measures <- function(test, model, response){
  data.frame(y = test[, response], yhat = predict(model, newdata = test))%>%
    summarize(MSE = sum((y-yhat)^2)/n(), MAD = sum(abs(y - yhat))/n())%>%
    mutate(RMSE = sqrt(MSE))
}
fitness.measures(dd.test.scaled, dd.nn.5.3, "Diff")
fitness.measures(dd.test.scaled, dd.nn.8, "Diff")
fitness.measures(dd.test.scaled, rf.dd, "Diff")
models = list()

for (i in 3:12){
  nn <- neuralnet(dd.nn.fmla
                           , data=dd.train.scaled
                           , hidden= i
                           , linear.output=TRUE)
  models[[i]] <- nn
}
  
mse = list()
for (i in 3:12) {
 mse[i] <- (fitness.measures(dd.test.scaled, models[[i]], "Diff")[1] * ((max.diff-min.diff)**2))
}

mse[1] <- fitness.measures(dd.test.scaled, dd.nn.5.3, "Diff")[1] * ((max.diff-min.diff)**2)
mse[2] <- fitness.measures(dd.test.scaled, rf.dd, "Diff")[1] * ((max.diff-min.diff)**2)

mse
[[1]]
[1] 4037.12

[[2]]
[1] 3321.5

[[3]]
[1] 4496.449

[[4]]
[1] 4440.882

[[5]]
[1] 4367.814

[[6]]
[1] 4364.864

[[7]]
[1] 4650.306

[[8]]
[1] 4138.011

[[9]]
[1] 3994.522

[[10]]
[1] 3924.122

[[11]]
[1] 4297.862

[[12]]
[1] 4095.124
NeuralNetTools::garson(models[[10]])

NeuralNetTools::plotnet(models[[10]])

Trip duration

ggplot(merged)+
  geom_point(aes(x=Checkout.Kiosk, y = Trip.Duration.Minutes ))+
  theme(axis.title.x=element_blank(), axis.text.x=element_blank(), axis.ticks.x=element_blank())

Some stations don’t have long trips associated with them at all, while others have TONS of long trips associated with them.

ggplot(merged)+
  geom_point(aes(y=Membership.Type, x = Trip.Duration.Minutes ))+
  geom_vline(xintercept = 60, color = "red")

Single trips run long the most often, but there a lot of late trips period.

merged$late = ifelse(merged$Trip.Duration.Minutes > 60,1 ,0)
ggplot(merged)+
  geom_bar(aes(y = Membership.Type,fill = factor(late)))

ggplot(merged)+
  geom_bar(aes(x = Checkout.Kiosk,fill = factor(late)))+
  theme(axis.title.x=element_blank(), axis.text.x=element_blank(), axis.ticks.x=element_blank())

head(merged)
temp_merged = merged#[year(merged$Checkout.Date)>2018,]
temp_merged$Checkout.Kiosk = factor(temp_merged$Checkout.Kiosk)
temp_merged$hour = hour(temp_merged$Checkout.Time)
temp_merged$Membership.Type = factor(temp_merged$Membership.Type)
set.seed(123)
train = sample (1:nrow(temp_merged), nrow(temp_merged)/2)
set.seed(12)
boost.merged=gbm(Trip.Duration.Minutes~Membership.Type+hour + Checkout.Kiosk+AvgTemp,data=temp_merged[train ,], distribution="gaussian",n.trees=500, shrinkage = .1)
Error in gbm(Trip.Duration.Minutes ~ Membership.Type + hour + Checkout.Kiosk +  :
  could not find function "gbm"
summary(boost.merged)

gbm.perf(boost.merged, method = "OOB")
OOB generally underestimates the optimal number of iterations although predictive performance is reasonably competitive. Using cv_folds>1 when calling gbm usually results in improved predictive performance.
[1] 34
attr(,"smoother")
Call:
loess(formula = object$oobag.improve ~ x, enp.target = min(max(4,
    length(x)/10), 50))

Number of Observations: 500
Equivalent Number of Parameters: 39.85
Residual Standard Error: 4.938 

pred = predict.gbm(object = boost.merged,newdata = temp_merged[-train,], n.trees = 34)
mean((pred - temp_merged$Trip.Duration.Minutes[-train])^2)
[1] 54794.91
ggplot()+
  geom_point(aes(x=temp_merged$Trip.Duration.Minutes[-train], y = pred))+
  geom_abline()

head(temp_merged)
boost.merged=gbm(late~Membership.Type+hour + Checkout.Kiosk+AvgTemp,data=temp_merged[train ,], distribution="bernoulli",n.trees=500, shrinkage = .1)
summary(boost.merged)

prob = predict(boost.merged, newdata = temp_merged[-train ,], type = "response")
Using 500 trees...
yhat = ifelse(prob >.5,1,0)
sum(yhat)
[1] 1
hist(prob)

yhat = ifelse(prob >.5,1,0)
cm = t(table(yhat, temp_merged$late[-train]))
cm
   yhat
         0      1
  0 103124      1
  1  12738      0
sum(diag(cm))/sum(cm) #Accuracy
[1] 0.8900512
cm[1,1]/sum(cm[1, c(1,2)]) #Correct rate of negative predicted values
[1] 0.9999903
cm[2,2]/sum(cm[2, c(1,2)]) #Correct rate of positive predicted value
[1] 0
yhat = ifelse(prob >.15,1,0)
cm = t(table(yhat, temp_merged$late[-train]))
cm
   yhat
        0     1
  0 74522 28603
  1  3786  8952
sum(diag(cm))/sum(cm) #Accuracy
[1] 0.7204543
cm[1,1]/sum(cm[1, c(1,2)]) #Correct rate of negative predicted values
[1] 0.7226376
cm[2,2]/sum(cm[2, c(1,2)]) #Correct rate of positive predicted value
[1] 0.7027791
LS0tDQp0aXRsZTogIkF1c3RpbiBCaWtlIERhdGEgRXhwbG9yYXRpb24iDQpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sNCi0tLQ0KDQpEYXRhIHNldCBhbmQgZGVzY3JpcHRpb24gY2FuIGJlIGZvdW5kIFtoZXJlXShodHRwczovL2RhdGEuYXVzdGludGV4YXMuZ292L1RyYW5zcG9ydGF0aW9uLWFuZC1Nb2JpbGl0eS9BdXN0aW4tTWV0cm9CaWtlLVRyaXBzL3R5ZmgtNXI4cykNCg0KR2l0aHViIHBhZ2Ugd2l0aCBjbGVhbmVkIHVwIGRhdGEgYW5kIHByZXNlbnRhdGlvbiBbaGVyZV0oaHR0cHM6Ly9naXRodWIuY29tL25kbGVkL0F1c3RpbkJpa2VzKQ0KDQojIExpYnJhcmllcyBJIG1pZ2h0IHVzZQ0KDQpgYGB7cn0NCmxpYnJhcnkoSVNMUikNCmxpYnJhcnkodGlkeXZlcnNlKQ0KbGlicmFyeShyYW5kb21Gb3Jlc3QpDQpsaWJyYXJ5KGdibSkNCmxpYnJhcnkoTUFTUykNCmxpYnJhcnkobHVicmlkYXRlKQ0KbGlicmFyeSh6b28pDQpsaWJyYXJ5KEdHYWxseSkNCmxpYnJhcnkoZTEwNzEpICNTVk0gYW5kIE5CIGxpYnJhcnkNCmBgYA0KDQojIExvYWRpbmcgaW4gdGhlIERhdGENCg0KYGBge3J9DQpiaWtlZGF0YSA9IHJlYWQuY3N2KCJBdXN0aW5fTWV0cm9CaWtlX1RyaXBzLmNzdiIpDQp3ZWF0aGVyZGF0YSA9IHJlYWQuY3N2KCJhdXN0aW5kYWlseS5jc3YiKQ0KYGBgDQoNCmBgYHtyfQ0KYmlrZWRhdGENCmBgYA0KYGBge3J9DQp3ZWF0aGVyZGF0YQ0KYGBgDQoNCg0KIyBEYXRhIENsZWFuaW5nDQoNCiMjIFdlYXRoZXIgRGF0YQ0KYGBge3J9DQp3ZWF0aGVyY2xlYW4gPSBmaWx0ZXIod2VhdGhlcmRhdGEsIHllYXIoWDE5MzguMDYuMDEpPjIwMTIpDQp3ZWF0aGVyY2xlYW4gPSB3ZWF0aGVyY2xlYW5bLGMoMSwyKV0NCmNvbG5hbWVzKHdlYXRoZXJjbGVhbikgPC0gYygiRGF0ZSIsIkF2Z1RlbXAiKQ0Kd2VhdGhlcmNsZWFuJERhdGUgPSBhcy5EYXRlKHN0cnB0aW1lKHdlYXRoZXJjbGVhbiREYXRlLCBmb3JtYXQgPSAnJVktJW0tJWQnKSkNCmBgYA0KDQpgYGB7cn0NCnN1bW1hcnkod2VhdGhlcmNsZWFuKQ0KYGBgDQpCdXQgd2UgY2FuIHNlZSB3ZSBoYXZlIDgwIGRhdGVzIHdpdGggbWlzc2luZyB0ZW1wZXJhdHVyZSB2YWx1ZXMuIEkgd291bGQgbGlrZSB0byBpbXB1dGUgYW4gYXZlcmFnZSBhcm91bmQgaXQsIGJ1dCBpcyB0aGF0IGEgc291bmQgc3RyYXRlZ3k/DQoNCmBgYHtyfQ0Kc2V0LnNlZWQoMTIzKQ0Kd2VhdGhlcmNsZWFubm49bmEub21pdCh3ZWF0aGVyY2xlYW4pDQpmYWtld2VhdGhlciA9IHdlYXRoZXJjbGVhbm5uDQpuYXMgPSBzYW1wbGUgKDE6bnJvdyh3ZWF0aGVyY2xlYW5ubiksIG5yb3cod2VhdGhlcmNsZWFubm4pKjAuMDI5MzQ3MDMpICMgVGhlIHNhbWUgcmF0ZSBvZiBtaXNzaW5nIHZhbHVlcyBhcyBpbiBvdXIgb3JpZ2luYWwgZGF0YSBzZXQNCmZha2V3ZWF0aGVyJEF2Z1RlbXBbbmFzXSA8LSBOQQ0KZmFrZXdlYXRoZXIkQXZnVGVtcCA9IChuYS5sb2NmKGZha2V3ZWF0aGVyJEF2Z1RlbXApICsgcmV2KG5hLmxvY2YocmV2KGZha2V3ZWF0aGVyJEF2Z1RlbXApKSkpLzIgIyBGaWxscyBpbiBtaXNzaW5nIHZhbHVlcyB3aXRoIHRoZSBhdmVyYWdlIG9mIHRoZSB3ZWF0aGVyIHZhbHVlcyBvbiBlaXRoZXIgc2lkZQ0KbWVhbigod2VhdGhlcmNsZWFubm4kQXZnVGVtcFtuYXNdLWZha2V3ZWF0aGVyJEF2Z1RlbXBbbmFzXSleMikNCmBgYA0KDQpHcmVhdCEgV2UgYXJlIGluIGEgcmVhc29uYWJsZSByYW5nZSBvZiB0aGUgdHJ1ZSB0ZW1wZXJhdHVyZSBhbmQgY2FuIGFwcGx5IHRoaXMgbWV0aG9kIHRvIG91ciBkYXRhc2V0Lg0KDQpgYGB7cn0NCndlYXRoZXJjbGVhbiRBdmdUZW1wID0gKG5hLmxvY2Yod2VhdGhlcmNsZWFuJEF2Z1RlbXApICsgcmV2KG5hLmxvY2YocmV2KHdlYXRoZXJjbGVhbiRBdmdUZW1wKSkpKS8yDQpgYGANCg0KYGBge3J9DQp3ZWF0aGVyY2xlYW4NCmBgYA0KDQoNCiMjIEJpa2UgZGF0YQ0KYGBge3J9DQpiaWtlY2xlYW4gPSBiaWtlZGF0YVssYygyLDQsNSw3LDksMTApXQ0KYmlrZWNsZWFuW2Jpa2VjbGVhbj09IiJdIDwtIE5BDQpiaWtlY2xlYW4kQ2hlY2tvdXQuVGltZT1obXMoYmlrZWNsZWFuJENoZWNrb3V0LlRpbWUpDQpiaWtlY2xlYW4kQ2hlY2tvdXQuRGF0ZSA9IGFzLkRhdGUoYmlrZWNsZWFuJENoZWNrb3V0LkRhdGUsZm9ybWF0PSclbS8lZC8lWScpDQpgYGANCg0KYGBge3J9DQpzdW1tYXJ5KChiaWtlY2xlYW4pKQ0KYGBgDQoNCiMjIyBDaGVja291dCBLaW9zaw0KDQpgYGB7cn0NCnNvcnQodW5pcXVlKGJpa2VjbGVhbiRDaGVja291dC5LaW9zaykpDQpgYGANCk9oIG5vLCB3ZSBoYXZlIGEgbG90IG9mIGNsZWFuaW5nIHRvIGRvIGhlcmUNCg0KYGBge3J9DQp0bXAgPSB0b2xvd2VyKGJpa2VjbGVhbiRDaGVja291dC5LaW9zaykNCnRtcCA9IGdzdWIoIlxcLiIsICIgIix0bXApDQp0bXAgPSBnc3ViKCImIiwgIi8iLHRtcCkNCnRtcCA9IGdzdWIoInN0cmVldCIsICIiLHRtcCkNCnRtcCA9IGdzdWIoIiBzdCAiLCAiIix0bXApDQp0bXAgPSBnc3ViKCIgYXQgIiwgIkAiLHRtcCkNCnRtcCA9IGdzdWIoIkAiLCAiLyIsdG1wKQ0KdG1wID0gZ3N1YigiICIsICIiLHRtcCkNCmBgYA0KDQpgYGB7cn0NCnRtcCA9IGRhdGEuZnJhbWUodG1wKQ0KdG1wID0gc2VwYXJhdGUoZGF0YSA9IHRtcCwgY29sID0gMSwgaW50byA9IGMoIjFzdCIsIjJuZCIsIjNyZCIsIjR0aCIpLCBzZXAgPSAiW15bOmFsbnVtOl1dKyIpDQp0bXBbaXMubmEodG1wKV0gPC0gIlpaWlpaIiAjIFNvcnQgZG9lc24ndCBsaWtlIE5BIHZhbHVlcywgc28gSSdtIHJlcGxhY2luZyB3aXRoIFpaWlogdG8gZ28gdG8gdGhlIGVuZCBvZiB0aGUgcm93DQp0bXAgPSBkYXRhLmZyYW1lKHQoYXBwbHkodG1wLDEsc29ydCkpKSAjIFNvcnRzIGJ5IHJvdw0KdG1wID0gdW5pdGUodG1wLCB1bml0ZWQsIHNlcD0iLyIpDQp0bXAgPSBnc3ViKCIvWlpaWloiLCAiIix0bXAkdW5pdGVkKQ0KYGBgDQoNCmBgYHtyfQ0KdG1wID0gZ3N1YigiM3JkL3RoZWNvbnZlbnRpb25jZW50ZXIvdHJpbml0eSIsIjNyZC9jb252ZW50aW9uY2VudGVyL3RyaW5pdHkiLHRtcCkNCnRtcCA9IGdzdWIoInNsYW1hciIsInNvdXRobGFtYXIiLHRtcCkNCnRtcCA9IGdzdWIoImJhcnRvbnNwcmluZ3Mva2lubmV5YXZlIiwiYmFydG9uc3ByaW5ncy9raW5uZXkiLHRtcCkNCnRtcCA9IGdzdWIoImJyYW5kaW5nL3JlIiwicmVicmFuZGluZyIsdG1wKQ0KdG1wID0gZ3N1YigiZ3VhZGFsdXBlL29wL3VuaXZlcnNpdHljby93ZXN0bWFsbCIsImd1YWRhbHVwZS91dHdlc3RtYWxsIix0bXApDQp0bXAgPSBnc3ViKCJlYXN0NnRoL3JvYmVydHRtYXJ0aW5leiIsImVhc3Q2dGgvcm9iZXJ0bWFydGluZXoiLHRtcCkNCmBgYA0KDQpgYGB7cn0NCmJpa2VjbGVhbiRDaGVja291dC5LaW9zayA9IHRtcA0KYGBgDQoNCg0KIyMjIFJldHVybiBLaW9zaw0KDQpgYGB7cn0NCnRtcCA9IHRvbG93ZXIoYmlrZWNsZWFuJFJldHVybi5LaW9zaykNCnRtcCA9IGdzdWIoIlxcLiIsICIgIix0bXApDQp0bXAgPSBnc3ViKCImIiwgIi8iLHRtcCkNCnRtcCA9IGdzdWIoInN0cmVldCIsICIiLHRtcCkNCnRtcCA9IGdzdWIoIiBzdCAiLCAiIix0bXApDQp0bXAgPSBnc3ViKCIgYXQgIiwgIkAiLHRtcCkNCnRtcCA9IGdzdWIoIkAiLCAiLyIsdG1wKQ0KdG1wID0gZ3N1YigiICIsICIiLHRtcCkNCmBgYA0KDQpgYGB7cn0NCnRtcCA9IGRhdGEuZnJhbWUodG1wKQ0KdG1wID0gc2VwYXJhdGUoZGF0YSA9IHRtcCwgY29sID0gMSwgaW50byA9IGMoIjFzdCIsIjJuZCIsIjNyZCIsIjR0aCIpLCBzZXAgPSAiW15bOmFsbnVtOl1dKyIpDQp0bXBbaXMubmEodG1wKV0gPC0gIlpaWlpaIiAjIFNvcnQgZG9lc24ndCBsaWtlIE5BIHZhbHVlcywgc28gSSdtIHJlcGxhY2luZyB3aXRoIFpaWlogdG8gZ28gdG8gdGhlIGVuZCBvZiB0aGUgcm93DQp0bXAgPSBkYXRhLmZyYW1lKHQoYXBwbHkodG1wLDEsc29ydCkpKSAjIFNvcnRzIGJ5IHJvdw0KdG1wID0gdW5pdGUodG1wLCB1bml0ZWQsIHNlcD0iLyIpDQp0bXAgPSBnc3ViKCIvWlpaWloiLCAiIix0bXAkdW5pdGVkKQ0KYGBgDQoNCmBgYHtyfQ0KdG1wID0gZ3N1YigiM3JkL3RoZWNvbnZlbnRpb25jZW50ZXIvdHJpbml0eSIsIjNyZC9jb252ZW50aW9uY2VudGVyL3RyaW5pdHkiLHRtcCkNCnRtcCA9IGdzdWIoInNsYW1hciIsInNvdXRobGFtYXIiLHRtcCkNCnRtcCA9IGdzdWIoImJhcnRvbnNwcmluZ3Mva2lubmV5YXZlIiwiYmFydG9uc3ByaW5ncy9raW5uZXkiLHRtcCkNCnRtcCA9IGdzdWIoImJyYW5kaW5nL3JlIiwicmVicmFuZGluZyIsdG1wKQ0KdG1wID0gZ3N1YigiZ3VhZGFsdXBlL29wL3VuaXZlcnNpdHljby93ZXN0bWFsbCIsImd1YWRhbHVwZS91dHdlc3RtYWxsIix0bXApDQp0bXAgPSBnc3ViKCJlYXN0NnRoL3JvYmVydHRtYXJ0aW5leiIsImVhc3Q2dGgvcm9iZXJ0bWFydGluZXoiLHRtcCkNCmBgYA0KDQpMZXQncyBjb21wYXJlIHRoZSBkcm9wIG9mZiBhbmQgcGlja3VwIGdyb3VwcyB0byBzZWUgaWYgd2UgbmVlZCB0byByZWNvZGUgYW55dGhpbmcuDQoNCmBgYHtyfQ0KY29tcGFyaXNvbiA9ICBkYXRhLmZyYW1lKHNvcnQodW5pcXVlKHRtcCkpLGMoc29ydCh1bmlxdWUoYmlrZWNsZWFuJENoZWNrb3V0Lktpb3NrKSksIE5BLCBOQSwgTkEsTkEpKQ0KY29sbmFtZXMoY29tcGFyaXNvbikgPC0gYygiUmV0dXJuIiwgIkNoZWNrb3V0IikNCmBgYA0KDQpgYGB7cn0NCmNvbXBhcmlzb24kUmV0dXJuWyFjb21wYXJpc29uJFJldHVybiAlaW4lIGNvbXBhcmlzb24kQ2hlY2tvdXRdDQpjb21wYXJpc29uJENoZWNrb3V0WyFjb21wYXJpc29uJENoZWNrb3V0ICVpbiUgY29tcGFyaXNvbiRSZXR1cm5dDQpgYGANCk1haW5zaG9wIHNob3VsZCBiZSByZWNvZGVkIHRvIHNob3ANCg0KDQpgYGB7cn0NCnRtcCA9IGdzdWIoIm1haW5zaG9wIiwic2hvcCIsdG1wKQ0KYmlrZWNsZWFuJFJldHVybi5LaW9zayA9IHRtcA0KYGBgDQoNCg0KDQojIyMgTWVtYmVyc2hpcCB0eXBlcw0KDQpJIHdhbnQgdG8gZW5jb2RlIHRoZXNlIGFzIHRoZSBmb2xsb3dpbmc6DQoNCipTaW5nbGUgVHJpcCoNCg0KIiQxIFBheSBieSBUcmlwIEZhbGwgU3BlY2lhbCIsIlNpbmdsZSBUcmlwIiwiVHJ5IEJlZm9yZSBZb3UgQnV5IFNwZWNpYWwiLCIkMSBQYXkgYnkgVHJpcCBXaW50ZXIgU3BlY2lhbCIsIlJpZGVTY291dCBTaW5nbGUgUmlkZSIsIlNpbmdsZSBUcmlwICIsIlNpbmdsZSBUcmlwIFJpZGUiLCAiUGF5LWFzLXlvdS1yaWRlIiwiU2luZ2xlIFRyaXAgKFBheS1hcy15b3UtcmlkZSkiLCIyNC1Ib3VyIEtpb3NrIChBdXN0aW4gQi1jeWNsZSkiLCIyNCBIb3VyIFdhbGsgVXAgUGFzcyIsIldhbGsgVXAiDQoNCipEYWlseSoNCg0KIjI0LUhvdXItT25saW5lIChBdXN0aW4gQi1jeWNsZSkiLCIyNC1Ib3VyIE1lbWJlcnNoaXAgKEF1c3RpbiBCLWN5Y2xlKSIsIkV4cGxvcmVyIiwiRXhwbG9yZXIgKCQ4IHBsdXMgdGF4KSINCg0KKjMtRGF5Kg0KDQoiMy1EYXkgRXhwbG9yZXIiLCJXZWVrZW5kZXIiLCIzLURheSBXZWVrZW5kZXIiLCJXZWVrZW5kZXIgKCQxNSBwbHVzIHRheCkiDQoNCipXZWVrbHkqDQoNCiI3LURheSIsNy1EYXkgTWVtYmVyc2hpcCAoQXVzdGluIEItY3ljbGUpLCI3LURheSBNZW1iZXJzaGlwIChBdXN0aW4gQi1jeWNsZSkiDQoNCipNb250aGx5Kg0KDQoiTG9jYWwzMCAoJDExIHBsdXMgdGF4KSIsIkxvY2FsMzAiLCJMb2NhbDMxIg0KDQoqQW5udWFsKg0KDQoiQW5udWFsICIsIkFubnVhbCBNZW1iZXJzaGlwICIsIkFubnVhbCBQYXNzIiwiQW5udWFsIFBsdXMiLCJMb2NhbDM2NSIsIkxvY2FsMzY1ICgkODAgcGx1cyB0YXgpIiwiTG9jYWwzNjUgWW91dGggKGFnZSAxMy0xNyByaWRlcnMpLSAxLzIsIkxvY2FsMzY1K0d1ZXN0IFBhc3MiLCJBbm51YWwiLCJBbm51YWwgTWVtYmVyIiwiQW5udWFsIE1lbWJlcnNoaXAiLCJBbm51YWwgTWVtYmVyc2hpcCAoQXVzdGluIEItY3ljbGUpIiwiQW5udWFsIFBhc3MgKDMwIG1pbnV0ZSkiLCJBbm51YWwgUGx1cyBNZW1iZXJzaGlwIiwiTG9jYWwzNjUtIDEvMiBvZmYgQW5uaXZlcnNhcnkgU3BlY2lhbCIsIkxvY2FsMzY1IFlvdXRoIChhZ2UgMTMtMTcgcmlkZXJzKSIsU3BlY2lhbCIgIkxvY2FsMzY1IFlvdXRoIHdpdGggaGVsbWV0IChhZ2UgMTMtMTcgcmlkZXJzKSIsIkxvY2FsMzY1K0d1ZXN0IFBhc3MtIDEvMiBvZmYgQW5uaXZlcnNhcnkgU3BlY2lhbCIsIk1lbWJlcnNoaXA6IHBheSBvbmNlICBvbmUteWVhciBjb21taXRtZW50Ig0KDQoqU3R1ZGVudCoNCg0KIkhUIFJhbSBNZW1iZXJzaGlwIiwiU2VtZXN0ZXIgTWVtYmVyc2hpcCIsIlVUIFN0dWRlbnQgTWVtYmVyc2hpcCIsIlNlbWVzdGVyIE1lbWJlcnNoaXAgKEF1c3RpbiBCLWN5Y2xlKSIsIlUuVC4gU3R1ZGVudCBNZW1iZXJzaGlwIiwiVS5ULiBTdHVkZW50IE1lbWJlcnNoaXAiDQoNCipFdmVudCoNCg0KIkFDTCAyMDE5IFBhc3MiLCJBQ0wgV2Vla2VuZCBQYXNzIFNwZWNpYWwgKEF1c3RpbiBCLWN5Y2xlKSIsIkZ1bkZ1bkZ1biBGZXN0IDMgRGF5IFBhc3MiDQoNCipTaGFyZSoNCg0KIkFubnVhbCAoQnJvd2FyZCBCLWN5Y2xlKSIsIkFubnVhbCAoRGVudmVyIEItY3ljbGUpIiwiQW5udWFsIChLYW5zYXMgQ2l0eSBCLWN5Y2xlKSIsIkFubnVhbCAoTmFzaHZpbGxlIEItY3ljbGUpIiwiQW5udWFsIChTYW4gQW50b25pbyBCLWN5Y2xlKSIsIkFubnVhbCBNZW1iZXIgKEhvdXN0b24gQi1jeWNsZSkiLCJBbm51YWwgTWVtYmVyc2hpcCAoQ2hhcmxvdHRlIEItY3ljbGUpIiwiQW5udWFsIE1lbWJlcnNoaXAgKEdSRUVOYmlrZSkiLCJEZW52ZXIgQi1jeWNsZSBGb3VuZGVyIiwiSGVhcnRsYW5kIFBhc3MgKEFubnVhbCBQYXkpIiwiTWFkdG93biBNb250aGx5IiwiUmVwdWJsaWMgUmlkZXIiLCJBbm51YWwgKENpbmN5IFJlZCBCaWtlKSIsIkFubnVhbCAoT21haGEgQi1jeWNsZSkiLCJBbm51YWwgKE1hZGlzb24gQi1jeWNsZSkiLCJBbm51YWwgKERlbnZlciBCaWtlIFNoYXJpbmcpIiwiQW5udWFsIE1lbWJlcnNoaXAgKEZvcnQgV29ydGggQmlrZSBTaGFyaW5nKSIsIkhlYXJ0bGFuZCBQYXNzIChNb250aGx5IFBheSkiLCJSZXB1YmxpYyBSaWRlciAoQW5udWFsKSINCg0KKk90aGVyKg0KIkFsdW1pbnVtIEFjY2VzcyIsIkZvdW5kaW5nIE1lbWJlciAoQXVzdGluIEItY3ljbGUpIiwiRm91bmRpbmcgTWVtYmVyIg0KDQoqTWlzc2luZyoNCk5BLCAiUFJPSElCSVRFRCIsICJSRVNUUklDVEVEIg0KDQoNCg0KYGBge3J9DQpiaWtlY2xlYW4kTWVtYmVyc2hpcC5UeXBlW2lzLm5hKGJpa2VjbGVhbiRNZW1iZXJzaGlwLlR5cGUpXSA9ICJtaXNzaW5nIg0KYmlrZWNsZWFuJE1lbWJlcnNoaXAuVHlwZVt3aGljaChiaWtlY2xlYW4kTWVtYmVyc2hpcC5UeXBlICVpbiUgYygiUFJPSElCSVRFRCIsICJSRVNUUklDVEVEIikpXT0ibWlzc2luZyINCmJpa2VjbGVhbiRNZW1iZXJzaGlwLlR5cGVbd2hpY2goYmlrZWNsZWFuJE1lbWJlcnNoaXAuVHlwZSAlaW4lIGMoIjMtRGF5IEV4cGxvcmVyIiwiV2Vla2VuZGVyIiwiMy1EYXkgV2Vla2VuZGVyIiwiV2Vla2VuZGVyICgkMTUgcGx1cyB0YXgpIikpXT0iMyBEYXkiDQpiaWtlY2xlYW4kTWVtYmVyc2hpcC5UeXBlW3doaWNoKGJpa2VjbGVhbiRNZW1iZXJzaGlwLlR5cGUgJWluJSBjKCIkMSBQYXkgYnkgVHJpcCBGYWxsIFNwZWNpYWwiLCJTaW5nbGUgVHJpcCIsIlRyeSBCZWZvcmUgWW91IEJ1eSBTcGVjaWFsIiwiJDEgUGF5IGJ5IFRyaXAgV2ludGVyIFNwZWNpYWwiLCJSaWRlU2NvdXQgU2luZ2xlIFJpZGUiLCJTaW5nbGUgVHJpcCAiLCJTaW5nbGUgVHJpcCBSaWRlIiwgIlBheS1hcy15b3UtcmlkZSIsIlNpbmdsZSBUcmlwIChQYXktYXMteW91LXJpZGUpIiwiMjQtSG91ciBLaW9zayAoQXVzdGluIEItY3ljbGUpIiwgIjI0IEhvdXIgV2FsayBVcCBQYXNzIiwiV2FsayBVcCIpKV09IlNpbmdsZSBUcmlwIg0KYmlrZWNsZWFuJE1lbWJlcnNoaXAuVHlwZVt3aGljaChiaWtlY2xlYW4kTWVtYmVyc2hpcC5UeXBlICVpbiUgYygiMjQtSG91ci1PbmxpbmUgKEF1c3RpbiBCLWN5Y2xlKSIsIjI0LUhvdXIgTWVtYmVyc2hpcCAoQXVzdGluIEItY3ljbGUpIiwiRXhwbG9yZXIiLCJFeHBsb3JlciAoJDggcGx1cyB0YXgpIikpXT0iRGFpbHkiDQpiaWtlY2xlYW4kTWVtYmVyc2hpcC5UeXBlW3doaWNoKGJpa2VjbGVhbiRNZW1iZXJzaGlwLlR5cGUlaW4lYygiNy1EYXkiLCI3LURheSBNZW1iZXJzaGlwIChBdXN0aW4gQi1jeWNsZSkiLCI3LURheSBNZW1iZXJzaGlwIChBdXN0aW4gQi1jeWNsZSkiKSldPSJXZWVrbHkiDQpiaWtlY2xlYW4kTWVtYmVyc2hpcC5UeXBlW3doaWNoKGJpa2VjbGVhbiRNZW1iZXJzaGlwLlR5cGUlaW4lYygiTG9jYWwzMCAoJDExIHBsdXMgdGF4KSIsIkxvY2FsMzAiLCJMb2NhbDMxIikpXT0iTW9udGhseSINCmJpa2VjbGVhbiRNZW1iZXJzaGlwLlR5cGVbd2hpY2goYmlrZWNsZWFuJE1lbWJlcnNoaXAuVHlwZSVpbiVjKCJBbm51YWwgIiwiQW5udWFsIE1lbWJlcnNoaXAgIiwiQW5udWFsIFBhc3MiLCJBbm51YWwgUGx1cyIsIkxvY2FsMzY1IiwiTG9jYWwzNjUgKCQ4MCBwbHVzIHRheCkiLCJMb2NhbDM2NSBZb3V0aCAoYWdlIDEzLTE3IHJpZGVycyktIDEvMiIgLCJMb2NhbDM2NStHdWVzdCBQYXNzIiwiQW5udWFsIiwiQW5udWFsIE1lbWJlciIsIkFubnVhbCBNZW1iZXJzaGlwIiwiQW5udWFsIE1lbWJlcnNoaXAgKEF1c3RpbiBCLWN5Y2xlKSIsIkFubnVhbCBQYXNzICgzMCBtaW51dGUpIiwiQW5udWFsIFBsdXMgTWVtYmVyc2hpcCIsIkxvY2FsMzY1LSAxLzIgb2ZmIEFubml2ZXJzYXJ5IFNwZWNpYWwiLCJMb2NhbDM2NSBZb3V0aCAoYWdlIDEzLTE3IHJpZGVycykiLCAiTG9jYWwzNjUgWW91dGggd2l0aCBoZWxtZXQgKGFnZSAxMy0xNyByaWRlcnMpIiwiTG9jYWwzNjUrR3Vlc3QgUGFzcy0gMS8yIG9mZiBBbm5pdmVyc2FyeSBTcGVjaWFsIiwiTWVtYmVyc2hpcDogcGF5IG9uY2UgIG9uZS15ZWFyIGNvbW1pdG1lbnQiLCJMb2NhbDM2NSBZb3V0aCAoYWdlIDEzLTE3IHJpZGVycyktIDEvMiBvZmYgU3BlY2lhbCIpKV09IkFubnVhbCINCmJpa2VjbGVhbiRNZW1iZXJzaGlwLlR5cGVbd2hpY2goYmlrZWNsZWFuJE1lbWJlcnNoaXAuVHlwZSVpbiVjKCJIVCBSYW0gTWVtYmVyc2hpcCIsIlNlbWVzdGVyIE1lbWJlcnNoaXAiLCJVVCBTdHVkZW50IE1lbWJlcnNoaXAiLCJTZW1lc3RlciBNZW1iZXJzaGlwIChBdXN0aW4gQi1jeWNsZSkiLCJVLlQuIFN0dWRlbnQgTWVtYmVyc2hpcCIsIlUuVC4gU3R1ZGVudCBNZW1iZXJzaGlwIikpXT0iU3R1ZGVudCINCmJpa2VjbGVhbiRNZW1iZXJzaGlwLlR5cGVbd2hpY2goYmlrZWNsZWFuJE1lbWJlcnNoaXAuVHlwZSVpbiVjKCJBQ0wgMjAxOSBQYXNzIiwiQUNMIFdlZWtlbmQgUGFzcyBTcGVjaWFsIChBdXN0aW4gQi1jeWNsZSkiLCJGdW5GdW5GdW4gRmVzdCAzIERheSBQYXNzIikpXT0iRXZlbnQiDQpiaWtlY2xlYW4kTWVtYmVyc2hpcC5UeXBlW3doaWNoKGJpa2VjbGVhbiRNZW1iZXJzaGlwLlR5cGUlaW4lYygiQWx1bWludW0gQWNjZXNzIiwiRm91bmRpbmcgTWVtYmVyIChBdXN0aW4gQi1jeWNsZSkiLCJGb3VuZGluZyBNZW1iZXIiKSldPSJPdGhlciINCg0KYmlrZWNsZWFuJE1lbWJlcnNoaXAuVHlwZVt3aGljaChiaWtlY2xlYW4kTWVtYmVyc2hpcC5UeXBlJWluJWMoIkFubnVhbCAoQnJvd2FyZCBCLWN5Y2xlKSIsIkFubnVhbCAoRGVudmVyIEItY3ljbGUpIiwiQW5udWFsIChLYW5zYXMgQ2l0eSBCLWN5Y2xlKSIsIkFubnVhbCAoTmFzaHZpbGxlIEItY3ljbGUpIiwiQW5udWFsIChTYW4gQW50b25pbyBCLWN5Y2xlKSIsIkFubnVhbCBNZW1iZXIgKEhvdXN0b24gQi1jeWNsZSkiLCJBbm51YWwgTWVtYmVyc2hpcCAoQ2hhcmxvdHRlIEItY3ljbGUpIiwiQW5udWFsIE1lbWJlcnNoaXAgKEdSRUVOYmlrZSkiLCJEZW52ZXIgQi1jeWNsZSBGb3VuZGVyIiwiSGVhcnRsYW5kIFBhc3MgKEFubnVhbCBQYXkpIiwiTWFkdG93biBNb250aGx5IiwiUmVwdWJsaWMgUmlkZXIiLCJBbm51YWwgKENpbmN5IFJlZCBCaWtlKSIsIkFubnVhbCAoT21haGEgQi1jeWNsZSkiLCJBbm51YWwgKE1hZGlzb24gQi1jeWNsZSkiLCJBbm51YWwgKERlbnZlciBCaWtlIFNoYXJpbmcpIiwiQW5udWFsIE1lbWJlcnNoaXAgKEZvcnQgV29ydGggQmlrZSBTaGFyaW5nKSIsIkhlYXJ0bGFuZCBQYXNzIChNb250aGx5IFBheSkiLCJSZXB1YmxpYyBSaWRlciAoQW5udWFsKSIsICJBbm51YWwgTWVtYmVyc2hpcCAoSW5keSAtIFBhY2VycyBCaWtlc2hhcmUgKSIsICJBbm51YWwgKEJvdWxkZXIgQi1jeWNsZSkiKSldPSJTaGFyZSINCmBgYA0KDQpgYGB7cn0NCnNvcnQodW5pcXVlKGJpa2VjbGVhbiRNZW1iZXJzaGlwLlR5cGUpKQ0KYGBgDQpMb29raW5nIGdvb2QhIEkgbWlnaHQgdHJ5IGFuZCBwcmVkaWN0IG91ciBtaXNzaW5nIG1lbWJlcnNoaXAgdHlwZXMgaWYgSSBoYXZlIHRpbWUuDQoNCiMjIE1lcmdpbmcgb3VyIERhdGFzZXRzDQoNCmBgYHtyfQ0KbWVyZ2VkID0gbWVyZ2UoYmlrZWNsZWFuLHdlYXRoZXJjbGVhbixieS54ID0gIkNoZWNrb3V0LkRhdGUiLGJ5LnkgPSJEYXRlIikNCmBgYA0KDQpgYGB7cn0NCnNhdmVSRFMobWVyZ2VkLGZpbGUgPSAnbWVyZ2VkLnJkcycpDQpgYGANCk5vdyBJIGRvbid0IGhhdmUgdG8gcnVuIGV2ZXJ5dGhpbmcgYWJvdmUgdGhpcyBFVkVSWSB0aW1lIEkgbmVlZCB0byBzdGFydCBvdmVyLg0KDQpgYGB7cn0NCm1lcmdlZCA9IHJlYWRSRFMoIm1lcmdlZC5yZHMiKQ0KbWVyZ2VkJE1lbWJlcnNoaXAuVHlwZT1mYWN0b3IobWVyZ2VkJE1lbWJlcnNoaXAuVHlwZSkNCmBgYA0KDQoNCiMgRGF0YSB2aXN1YWxpemF0aW9uDQoNCg0KYGBge3J9DQpnZ3Bsb3QobWVyZ2VkKSsNCiAgZ2VvbV9wb2ludChhZXMoeCA9IENoZWNrb3V0LkRhdGUsIHk9cm91bmQoVHJpcC5EdXJhdGlvbi5NaW51dGVzLzYwKSkpKw0KICBnZW9tX2hsaW5lKHlpbnRlcmNlcHQ9MSxjb2xvciA9ICJyZWQiKQ0KYGBgDQoNClRyaXBzIGFyZSBleHBlY3RlZCB0byBiZSBhdCAxIGhvdXIgaW4gbGVuZ3RoLiBBbnkgbW9yZSB0aGFuIHRoYXQgYW5kIHRoZXJlIGlzIGEgZmluZSBhc3NvY2lhdGVkIHdpdGggZWFjaCBtaW51dGUgb3ZlciB5b3UgcmlkZS4gYXMgdGltZSBoYXMgZ29uZSBvbiwgbW9yZSBwZW9wbGUgYXJlbid0IGNoZWNraW5nIHRoZWlyIGJpa2VzIGluIG9uIHRpbWUuIA0KDQpJIHdvbmRlciB3aGF0J3MgdXAgd2l0aCB0aGUgd2VpcmQgZ2FwcyBpbiAyMDE2Lg0KDQpgYGB7cn0NCmdncGxvdChtZXJnZWRbd2hpY2goeWVhcihtZXJnZWQkQ2hlY2tvdXQuRGF0ZSk9PTIwMTYpLF0pKw0KICAgIGdlb21fcG9pbnQoYWVzKHggPSBDaGVja291dC5EYXRlLCB5PXJvdW5kKFRyaXAuRHVyYXRpb24uTWludXRlcy82MCkpKQ0KYGBgDQpgYGB7cn0NCmdncGxvdChtZXJnZWRbd2hpY2goeWVhcihtZXJnZWQkQ2hlY2tvdXQuRGF0ZSk9PTIwMTYgJiBtb250aChtZXJnZWQkQ2hlY2tvdXQuRGF0ZSk9PTQpLF0pKw0KICAgICAgZ2VvbV9wb2ludChhZXMoeCA9IENoZWNrb3V0LkRhdGUsIHk9cm91bmQoVHJpcC5EdXJhdGlvbi5NaW51dGVzLzYwKSkpDQpgYGANCg0KYGBge3J9DQptZXJnZWRbd2hpY2goeWVhcihtZXJnZWQkQ2hlY2tvdXQuRGF0ZSk9PTIwMTYgJiBtb250aChtZXJnZWQkQ2hlY2tvdXQuRGF0ZSk9PTQpLF0NCmBgYA0KR3JlYXQsIG5vIGRhdGEgZm9yIHRoYXQgbW9udGguDQoNCg0KYGBge3J9DQptZXJnZWRbd2hpY2goeWVhcihtZXJnZWQkQ2hlY2tvdXQuRGF0ZSk9PTIwMTYgJiBtb250aChtZXJnZWQkQ2hlY2tvdXQuRGF0ZSk9PTEyKSxdDQpgYGANClNhbWUgd2l0aCBEZWNlbWJlci4NCg0KYGBge3J9DQpnZ3Bsb3QobWVyZ2VkKSsNCiAgZ2VvbV9iYXIoYWVzKHg9ZmFjdG9yKG1vbnRoKENoZWNrb3V0LkRhdGUpKSksIGZpbGwgPSAiYmx1ZSIpKw0KICBmYWNldF93cmFwKH55ZWFyKENoZWNrb3V0LkRhdGUpKQ0KYGBgDQpXZSBjYW4gc2VlIHRoZXJlIGlzIGEgY3ljbGljYWwgbmF0dXJlIHRvIHRoZSBudW1iZXIgb2YgdHJpcHMgdGFrZW4gcGVyIG1vbnRoLiBJbnR1aXRpdmVseSwgdGhlcmUgYXJlIG1vcmUgcmlkZXJzIHdoZW4gdGhlIHdlYXRoZXIgaXMgbmljZS4NCmBgYHtyfQ0Kc3VtbWFyeShtZXJnZWQkQXZnVGVtcCkNCmBgYA0KDQpgYGB7cn0NCmdncGxvdChtZXJnZWQpKw0KICBnZW9tX2hpc3RvZ3JhbShhZXMoeD0gQXZnVGVtcCksIGJpbnMgPSA0NSwgZmlsbCA9ICJibHVlIikNCmBgYA0KDQpNb3N0IHBlb3BsZSByaWRlIGJldHdlZW4gYW5kIDEyIGFuZCAzMiBkZWdyZWVzIENlbHNpdXMNCg0KDQpgYGB7cn0NCm1lcmdlZCAlPiUgDQogIGdyb3VwX2J5KENoZWNrb3V0Lktpb3NrKSAlPiUNCiAgc3VtbWFyaXNlKERlcGFydGluZ19CaWtlcyA9IGxlbmd0aChDaGVja291dC5LaW9zaykpICU+JSBhcnJhbmdlKC4sZGVzYyhEZXBhcnRpbmdfQmlrZXMpKQ0KYGBgDQoNCg0KYGBge3J9DQptZXJnZWQgJT4lDQogIGdncGxvdCgpICsNCiAgZ2VvbV9iYXIoYWVzKHg9Q2hlY2tvdXQuS2lvc2spLCBmaWxsID0gInJlZCIpKw0KICB0aGVtZShheGlzLnRpdGxlLng9ZWxlbWVudF9ibGFuaygpLCBheGlzLnRleHQueD1lbGVtZW50X2JsYW5rKCksIGF4aXMudGlja3MueD1lbGVtZW50X2JsYW5rKCkpDQpgYGANCkJpa2VzIGxlYXZlIHRoZSBjYW1wdXMgU3BlZWR3YXkgYXQgYSB2ZXJ5IGhpZ2ggcmF0ZQ0KDQpgYGB7cn0NCm1lcmdlZCAlPiUNCiAgZ2dwbG90KCkgKw0KICBnZW9tX2JhcihhZXMoeD1SZXR1cm4uS2lvc2spLCBmaWxsID0gImJsdWUiKSsNCiAgdGhlbWUoYXhpcy50aXRsZS54PWVsZW1lbnRfYmxhbmsoKSwgYXhpcy50ZXh0Lng9ZWxlbWVudF9ibGFuaygpLCBheGlzLnRpY2tzLng9ZWxlbWVudF9ibGFuaygpKQ0KYGBgDQoNCmBgYHtyfQ0KbWVyZ2VkICU+JSANCiAgZ3JvdXBfYnkoUmV0dXJuLktpb3NrKSAlPiUNCiAgc3VtbWFyaXNlKERlcGFydGluZ19CaWtlcyA9IGxlbmd0aChSZXR1cm4uS2lvc2spKSAlPiUgYXJyYW5nZSguLGRlc2MoRGVwYXJ0aW5nX0Jpa2VzKSkNCmBgYA0KDQpFdmVuIG1vcmUgYXJlIHJldHVybmVkIHRoZXJlDQoNCg0KIyBEYWlseSBEaWZmZXJlbnRpYWwgUmVxdWlyaW5nIEh1bWFuIEludGVydmVudGlvbg0KDQpgYGB7cn0NCmNoZWNrb3V0ID0gbWVyZ2VkICU+JSBncm91cF9ieShDaGVja291dC5EYXRlLCBDaGVja291dC5LaW9zaykgJT4lIHRhbGx5KCkgJT4lIHNwcmVhZChDaGVja291dC5EYXRlLCBuKQ0KY2hlY2tvdXQgPSBjaGVja291dFstd2hpY2goY2hlY2tvdXQkQ2hlY2tvdXQuS2lvc2sgPT0gImVleW9yZS9zMjAxOCIpLF0NCnJldHVybiA9IG1lcmdlZCAlPiUgZ3JvdXBfYnkoQ2hlY2tvdXQuRGF0ZSwgUmV0dXJuLktpb3NrKSAlPiUgdGFsbHkoKSAlPiUgc3ByZWFkKENoZWNrb3V0LkRhdGUsIG4pDQpyZXR1cm4gPSByZXR1cm5bLXdoaWNoKCFyZXR1cm4kUmV0dXJuLktpb3NrICVpbiUgY2hlY2tvdXQkQ2hlY2tvdXQuS2lvc2spLF0NCmNoZWNrb3V0W2lzLm5hKGNoZWNrb3V0KV0gPSAwIA0KcmV0dXJuW2lzLm5hKHJldHVybildPSAwDQpgYGANCg0KYGBge3J9DQpkaWZmID0gZGF0YS5mcmFtZShyZXR1cm4kUmV0dXJuLktpb3NrLHJldHVyblssLTFdLWNoZWNrb3V0WywtMV0pDQpgYGANCg0KYGBge3J9DQpkaWZmDQpgYGANCg0KDQoNCmBgYHtyfQ0KZGFpbHlfZGlmZiA9IGRhdGEuZnJhbWUoY29sU3VtcyhhYnMoZGlmZlssLTFdKSkpDQpkYWlseV9kaWZmIDwtIHJvd25hbWVzX3RvX2NvbHVtbihkYWlseV9kaWZmLCAiRGF0ZSIpDQpkYWlseV9kaWZmJERhdGUgPSBnc3ViKCAiWCIsICIiLCBkYWlseV9kaWZmJERhdGUpDQpkYWlseV9kaWZmJERhdGUgPSBhcy5EYXRlKHN0cnB0aW1lKGRhaWx5X2RpZmYkRGF0ZSwgZm9ybWF0ID0gJyVZLiVtLiVkJykpDQpjb2xuYW1lcyhkYWlseV9kaWZmKSA9IGMoIkRhdGUiLCJEaWZmIikNCmBgYA0KDQpgYGB7cn0NCmRhaWx5X2RpZmYNCmBgYA0KDQoNCmBgYHtyfQ0KZ2dwbG90KGRhaWx5X2RpZmYpKw0KICBnZW9tX2JhcihhZXMoeD1EYXRlLCB5ID0gRGlmZiksIHN0YXQgPSAiaWRlbnRpdHkiKQ0KYGBgDQpgYGB7cn0NCmRhaWx5X2RpZmYgPSBtZXJnZShkYWlseV9kaWZmLHdlYXRoZXJjbGVhbixieS54ID0gIkRhdGUiLGJ5LnkgPSJEYXRlIikNCmRhaWx5X2RpZmYkbW9udGggPSBtb250aChkYWlseV9kaWZmJERhdGUpDQpgYGANCg0KYGBge3J9DQp4Z3JpZCA9IHNlcSgtMTAsNTAsLjAwMSkNCmdncGxvdChkYWlseV9kaWZmKSArDQogIGdlb21fcG9pbnQoYWVzKHggPSBBdmdUZW1wLCB5ID0gRGlmZiwgY29sb3IgPWZhY3Rvcihtb250aChEYXRlKSkpKQ0KYGBgDQpUaGlzIHNlZW1zIGxpa2Ugb25lIG9mIHRob3NlIHRoaW5ncyB3ZSB3b3VsZCB3YW50IHRvIHByZWRpY3Qgc28gdGhhdCB3ZSBjYW4ga25vdyBzdGFmZmluZyBsZXZlbHMuIExldCdzIHRyeSB0byBwcmVkaWN0IGl0IHdpdGggYSByYW5kb20gZm9yZXN0Lg0KDQojIyBSYW5kb20gRm9yZXN0DQoNCmBgYHtyfQ0KaGVhZChkYWlseV9kaWZmKQ0KYGBgDQoNCg0KYGBge3J9DQpzZXQuc2VlZCgxMjMpDQp0cmFpbiA9IHNhbXBsZSAoMTpucm93KGRhaWx5X2RpZmYpLCBucm93KGRhaWx5X2RpZmYpLzIpDQpgYGANCg0KDQpgYGB7cn0NCnJmLmRkPSByYW5kb21Gb3Jlc3QoRGlmZiB+IERhdGUgKyBBdmdUZW1wLGRhdGE9ZGFpbHlfZGlmZiwgc3Vic2V0PXRyYWluKQ0KeWhhdC5yZiA9IHByZWRpY3QocmYuZGQgLG5ld2RhdGE9ZGFpbHlfZGlmZlstIHRyYWluICxdKQ0KbWVhbigoeWhhdC5yZi1kYWlseV9kaWZmWy10cmFpbiwgIkRpZmYiXSleMikNCmBgYA0KDQpgYGB7cn0NCmdncGxvdCgpKw0KICBnZW9tX3BvaW50KGFlcyh4PWRhaWx5X2RpZmYkRGlmZlstdHJhaW5dLHkgPSB5aGF0LnJmKSkrDQogIGdlb21fYWJsaW5lKGNvbG9yID0gInJlZCIpDQpgYGANCg0KDQoNCg0KDQpgYGB7cn0NCnN1bW1hcnkoZGFpbHlfZGlmZikNCmBgYA0KTm90IHRlcnJpYmxlLCBidXQgdGhlcmUgYXJlIHNvbWUgYXJlYXMgd2hlcmUgdGhlIG1vZGVsIGdldHMgaXQgcHJldHR5IHdyb25nDQoNCkxldCdzIHRyeSBib29zdGluZw0KDQpgYGB7cn0NCnNldC5zZWVkKDEyKQ0KYm9vc3QuZGQ9Z2JtKERpZmZ+QXZnVGVtcCttb250aChEYXRlKSt5ZWFyKERhdGUpK2RheShEYXRlKSxkYXRhPWRhaWx5X2RpZmZbdHJhaW4gLF0sIGRpc3RyaWJ1dGlvbj0iZ2F1c3NpYW4iLG4udHJlZXM9NTAwMCwgc2hyaW5rYWdlID0gLjAxLGludGVyYWN0aW9uLmRlcHRoID0gMykNCmBgYA0KDQpgYGB7cn0NCmJlc3QuaXRlciA8LSBnYm0ucGVyZihib29zdC5kZCwgbWV0aG9kID0gIk9PQiIsIHBsb3QgPSBGQUxTRSkNCnloYXQuYm9vc3Q9cHJlZGljdCAoYm9vc3QuZGQgLG5ld2RhdGEgPWRhaWx5X2RpZmZbLXRyYWluICxdLCBuLnRyZWVzPWJlc3QuaXRlcikNCm1lYW4oKHloYXQuYm9vc3QtZGFpbHlfZGlmZlstdHJhaW4sICJEaWZmIl0pXjIpDQpgYGANCmBgYHtyfQ0KZ2dwbG90KCkrDQogIGdlb21fcG9pbnQoYWVzKHg9ZGFpbHlfZGlmZiREaWZmWy10cmFpbl0seSA9IHloYXQuYm9vc3QpKSsNCiAgZ2VvbV9hYmxpbmUoY29sb3IgPSAicmVkIikNCmBgYA0KDQoNCmBgYHtyfQ0KZGFpbHlfZGlmZltkYWlseV9kaWZmJERpZmY+MzAwLCAnRGF0ZSddDQpgYGANCg0KIyMgQU5OIFRyeQ0KDQpgYGB7cn0NCmxpYnJhcnkobmV1cmFsbmV0KQ0KbGlicmFyeShOZXVyYWxOZXRUb29scykNCmBgYA0KDQpgYGB7cn0NCmRhaWx5X2RpZmYkeWVhciA9IHllYXIoZGFpbHlfZGlmZiREYXRlKQ0KZGFpbHlfZGlmZiRkYXkgPSBkYXkoZGFpbHlfZGlmZiREYXRlKQ0KZGQuc2NhbGVkIDwtIGFzLmRhdGEuZnJhbWUoc2NhbGUoZGFpbHlfZGlmZlssLTFdKSkNCm1pbi5kaWZmIDwtIG1pbihkYWlseV9kaWZmJERpZmYpDQptYXguZGlmZiA8LSBtYXgoZGFpbHlfZGlmZiREaWZmKQ0KYGBgDQoNCmBgYHtyfQ0KZGQuc2NhbGVkDQpgYGANCg0KYGBge3J9DQpkZC5zY2FsZWQkRGlmZiA8LSBzY2FsZShkYWlseV9kaWZmJERpZmYsIGNlbnRlciA9IG1pbi5kaWZmLCBzY2FsZSA9IG1heC5kaWZmIC0gbWluLmRpZmYpDQpgYGANCg0KYGBge3J9DQpkZC5zcGxpdCA8LSBzYW1wbGUoZGltKGRhaWx5X2RpZmYpWzFdLGRpbShkYWlseV9kaWZmKVsxXS8yICkNCiMgVHJhaW4tdGVzdCBzcGxpdA0KZGQudHJhaW4uc2NhbGVkIDwtIGRkLnNjYWxlZFtkZC5zcGxpdCwgXQ0KZGQudGVzdC5zY2FsZWQgPC0gZGQuc2NhbGVkWy1kZC5zcGxpdCwgXQ0KYGBgDQoNCmBgYHtyfQ0KZ2VuZXJhdGUuZnVsbC5mbWxhPC0gZnVuY3Rpb24oZGYsIHJlc3BvbnNlKXsNCiAgbmFtZXMoZGYpWyEobmFtZXMoZGYpID09IHJlc3BvbnNlKV0lPiUNCiAgICBwYXN0ZSguLGNvbGxhcHNlID0gIisiKSU+JQ0KICAgICBwYXN0ZShyZXNwb25zZSwgIn4iLCAuKSU+JQ0KICAgIGZvcm11bGEoLikNCn0NCmRkLm5uLmZtbGEgPC0gZ2VuZXJhdGUuZnVsbC5mbWxhKGRkLnRyYWluLnNjYWxlZCwgIkRpZmYiKQ0KZGQubm4uNS4zIDwtIG5ldXJhbG5ldChkZC5ubi5mbWxhDQogICAgICAgICAgICAgICAgICAgICAgICAgICAsIGRhdGE9ZGQudHJhaW4uc2NhbGVkDQogICAgICAgICAgICAgICAgICAgICAgICAgICAsIGhpZGRlbj1jKDUsMykNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICwgbGluZWFyLm91dHB1dD1UUlVFKQ0KZGQubm4uOCA8LSBuZXVyYWxuZXQoZGQubm4uZm1sYQ0KICAgICAgICAgICAgICAgICAgICAgICAgICAgLCBkYXRhPWRkLnRyYWluLnNjYWxlZA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgLCBoaWRkZW49OA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgLCBsaW5lYXIub3V0cHV0PVRSVUUpDQpyZi5kZD0gcmFuZG9tRm9yZXN0KGRkLm5uLmZtbGEsZGF0YT1kZC50cmFpbi5zY2FsZWQpDQoNCmBgYA0KDQpgYGB7cn0NCmZpdG5lc3MubWVhc3VyZXMgPC0gZnVuY3Rpb24odGVzdCwgbW9kZWwsIHJlc3BvbnNlKXsNCiAgZGF0YS5mcmFtZSh5ID0gdGVzdFssIHJlc3BvbnNlXSwgeWhhdCA9IHByZWRpY3QobW9kZWwsIG5ld2RhdGEgPSB0ZXN0KSklPiUNCiAgICBzdW1tYXJpemUoTVNFID0gc3VtKCh5LXloYXQpXjIpL24oKSwgTUFEID0gc3VtKGFicyh5IC0geWhhdCkpL24oKSklPiUNCiAgICBtdXRhdGUoUk1TRSA9IHNxcnQoTVNFKSkNCn0NCmBgYA0KDQpgYGB7cn0NCmZpdG5lc3MubWVhc3VyZXMoZGQudGVzdC5zY2FsZWQsIGRkLm5uLjUuMywgIkRpZmYiKQ0KZml0bmVzcy5tZWFzdXJlcyhkZC50ZXN0LnNjYWxlZCwgZGQubm4uOCwgIkRpZmYiKQ0KZml0bmVzcy5tZWFzdXJlcyhkZC50ZXN0LnNjYWxlZCwgcmYuZGQsICJEaWZmIikNCmBgYA0KDQpgYGB7cn0NCm1vZGVscyA9IGxpc3QoKQ0KDQpmb3IgKGkgaW4gMzoxMil7DQogIG5uIDwtIG5ldXJhbG5ldChkZC5ubi5mbWxhDQogICAgICAgICAgICAgICAgICAgICAgICAgICAsIGRhdGE9ZGQudHJhaW4uc2NhbGVkDQogICAgICAgICAgICAgICAgICAgICAgICAgICAsIGhpZGRlbj0gaSANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICwgbGluZWFyLm91dHB1dD1UUlVFKQ0KICBtb2RlbHNbW2ldXSA8LSBubg0KfQ0KICANCmBgYA0KDQpgYGB7cn0NCm1zZSA9IGxpc3QoKQ0KZm9yIChpIGluIDM6MTIpIHsNCiBtc2VbaV0gPC0gKGZpdG5lc3MubWVhc3VyZXMoZGQudGVzdC5zY2FsZWQsIG1vZGVsc1tbaV1dLCAiRGlmZiIpWzFdICogKChtYXguZGlmZi1taW4uZGlmZikqKjIpKQ0KfQ0KDQptc2VbMV0gPC0gZml0bmVzcy5tZWFzdXJlcyhkZC50ZXN0LnNjYWxlZCwgZGQubm4uNS4zLCAiRGlmZiIpWzFdICogKChtYXguZGlmZi1taW4uZGlmZikqKjIpDQptc2VbMl0gPC0gZml0bmVzcy5tZWFzdXJlcyhkZC50ZXN0LnNjYWxlZCwgcmYuZGQsICJEaWZmIilbMV0gKiAoKG1heC5kaWZmLW1pbi5kaWZmKSoqMikNCg0KbXNlDQoNCg0KYGBgDQoNCg0KYGBge3J9DQpOZXVyYWxOZXRUb29sczo6Z2Fyc29uKG1vZGVsc1tbMTBdXSkNCmBgYA0KYGBge3J9DQpOZXVyYWxOZXRUb29sczo6cGxvdG5ldChtb2RlbHNbWzEwXV0pDQoNCmBgYA0KDQojIFRyaXAgZHVyYXRpb24NCg0KYGBge3J9DQpnZ3Bsb3QobWVyZ2VkKSsNCiAgZ2VvbV9wb2ludChhZXMoeD1DaGVja291dC5LaW9zaywgeSA9IFRyaXAuRHVyYXRpb24uTWludXRlcyApKSsNCiAgdGhlbWUoYXhpcy50aXRsZS54PWVsZW1lbnRfYmxhbmsoKSwgYXhpcy50ZXh0Lng9ZWxlbWVudF9ibGFuaygpLCBheGlzLnRpY2tzLng9ZWxlbWVudF9ibGFuaygpKQ0KYGBgDQpTb21lIHN0YXRpb25zIGRvbid0IGhhdmUgbG9uZyB0cmlwcyBhc3NvY2lhdGVkIHdpdGggdGhlbSBhdCBhbGwsIHdoaWxlIG90aGVycyBoYXZlIFRPTlMgb2YgbG9uZyB0cmlwcyBhc3NvY2lhdGVkIHdpdGggdGhlbS4NCg0KDQpgYGB7cn0NCmdncGxvdChtZXJnZWQpKw0KICBnZW9tX3BvaW50KGFlcyh5PU1lbWJlcnNoaXAuVHlwZSwgeCA9IFRyaXAuRHVyYXRpb24uTWludXRlcyApKSsNCiAgZ2VvbV92bGluZSh4aW50ZXJjZXB0ID0gNjAsIGNvbG9yID0gInJlZCIpDQpgYGANCg0KU2luZ2xlIHRyaXBzIHJ1biBsb25nIHRoZSBtb3N0IG9mdGVuLCBidXQgdGhlcmUgYSBsb3Qgb2YgbGF0ZSB0cmlwcyBwZXJpb2QuDQoNCmBgYHtyfQ0KbWVyZ2VkJGxhdGUgPSBpZmVsc2UobWVyZ2VkJFRyaXAuRHVyYXRpb24uTWludXRlcyA+IDYwLDEgLDApDQpgYGANCg0KYGBge3J9DQpnZ3Bsb3QobWVyZ2VkKSsNCiAgZ2VvbV9iYXIoYWVzKHkgPSBNZW1iZXJzaGlwLlR5cGUsZmlsbCA9IGZhY3RvcihsYXRlKSkpDQpgYGANCg0KYGBge3J9DQpnZ3Bsb3QobWVyZ2VkKSsNCiAgZ2VvbV9iYXIoYWVzKHggPSBDaGVja291dC5LaW9zayxmaWxsID0gZmFjdG9yKGxhdGUpKSkrDQogIHRoZW1lKGF4aXMudGl0bGUueD1lbGVtZW50X2JsYW5rKCksIGF4aXMudGV4dC54PWVsZW1lbnRfYmxhbmsoKSwgYXhpcy50aWNrcy54PWVsZW1lbnRfYmxhbmsoKSkNCmBgYA0KYGBge3J9DQpoZWFkKG1lcmdlZCkNCmBgYA0KDQpgYGB7cn0NCnRlbXBfbWVyZ2VkID0gbWVyZ2VkI1t5ZWFyKG1lcmdlZCRDaGVja291dC5EYXRlKT4yMDE4LF0NCnRlbXBfbWVyZ2VkJENoZWNrb3V0Lktpb3NrID0gZmFjdG9yKHRlbXBfbWVyZ2VkJENoZWNrb3V0Lktpb3NrKQ0KdGVtcF9tZXJnZWQkaG91ciA9IGhvdXIodGVtcF9tZXJnZWQkQ2hlY2tvdXQuVGltZSkNCnRlbXBfbWVyZ2VkJE1lbWJlcnNoaXAuVHlwZSA9IGZhY3Rvcih0ZW1wX21lcmdlZCRNZW1iZXJzaGlwLlR5cGUpDQpgYGANCg0KDQpgYGB7cn0NCnNldC5zZWVkKDEyMykNCnRyYWluID0gc2FtcGxlICgxOm5yb3codGVtcF9tZXJnZWQpLCBucm93KHRlbXBfbWVyZ2VkKS8yKQ0KYGBgDQoNCmBgYHtyfQ0Kc2V0LnNlZWQoMTIpDQpib29zdC5tZXJnZWQ9Z2JtKFRyaXAuRHVyYXRpb24uTWludXRlc35NZW1iZXJzaGlwLlR5cGUraG91ciArIENoZWNrb3V0Lktpb3NrK0F2Z1RlbXAsZGF0YT10ZW1wX21lcmdlZFt0cmFpbiAsXSwgZGlzdHJpYnV0aW9uPSJnYXVzc2lhbiIsbi50cmVlcz01MDAsIHNocmlua2FnZSA9IC4xKQ0KYGBgDQoNCmBgYHtyfQ0Kc3VtbWFyeShib29zdC5tZXJnZWQpDQpgYGANCg0KDQpgYGB7cn0NCmdibS5wZXJmKGJvb3N0Lm1lcmdlZCwgbWV0aG9kID0gIk9PQiIpDQpgYGANCg0KYGBge3J9DQpwcmVkID0gcHJlZGljdC5nYm0ob2JqZWN0ID0gYm9vc3QubWVyZ2VkLG5ld2RhdGEgPSB0ZW1wX21lcmdlZFstdHJhaW4sXSwgbi50cmVlcyA9IDM0KQ0KYGBgDQoNCg0KYGBge3J9DQptZWFuKChwcmVkIC0gdGVtcF9tZXJnZWQkVHJpcC5EdXJhdGlvbi5NaW51dGVzWy10cmFpbl0pXjIpDQpgYGANCg0KDQpgYGB7cn0NCmdncGxvdCgpKw0KICBnZW9tX3BvaW50KGFlcyh4PXRlbXBfbWVyZ2VkJFRyaXAuRHVyYXRpb24uTWludXRlc1stdHJhaW5dLCB5ID0gcHJlZCkpKw0KICBnZW9tX2FibGluZSgpDQpgYGANCmBgYHtyfQ0KaGVhZCh0ZW1wX21lcmdlZCkNCmBgYA0KDQoNCmBgYHtyfQ0KYm9vc3QubWVyZ2VkPWdibShsYXRlfk1lbWJlcnNoaXAuVHlwZStob3VyICsgQ2hlY2tvdXQuS2lvc2srQXZnVGVtcCxkYXRhPXRlbXBfbWVyZ2VkW3RyYWluICxdLCBkaXN0cmlidXRpb249ImJlcm5vdWxsaSIsbi50cmVlcz01MDAsIHNocmlua2FnZSA9IC4xKQ0KYGBgDQoNCmBgYHtyfQ0Kc3VtbWFyeShib29zdC5tZXJnZWQpDQpgYGANCg0KDQpgYGB7cn0NCnByb2IgPSBwcmVkaWN0KGJvb3N0Lm1lcmdlZCwgbmV3ZGF0YSA9IHRlbXBfbWVyZ2VkWy10cmFpbiAsXSwgdHlwZSA9ICJyZXNwb25zZSIpDQp5aGF0ID0gaWZlbHNlKHByb2IgPi41LDEsMCkNCnN1bSh5aGF0KQ0KYGBgDQoNCmBgYHtyfQ0KaGlzdChwcm9iKQ0KYGBgDQoNCg0KYGBge3J9DQp5aGF0ID0gaWZlbHNlKHByb2IgPi41LDEsMCkNCmNtID0gdCh0YWJsZSh5aGF0LCB0ZW1wX21lcmdlZCRsYXRlWy10cmFpbl0pKQ0KY20NCmBgYA0KYGBge3J9DQpzdW0oZGlhZyhjbSkpL3N1bShjbSkgI0FjY3VyYWN5DQpjbVsxLDFdL3N1bShjbVsxLCBjKDEsMildKSAjQ29ycmVjdCByYXRlIG9mIG5lZ2F0aXZlIHByZWRpY3RlZCB2YWx1ZXMNCmNtWzIsMl0vc3VtKGNtWzIsIGMoMSwyKV0pICNDb3JyZWN0IHJhdGUgb2YgcG9zaXRpdmUgcHJlZGljdGVkIHZhbHVlDQpgYGANCmBgYHtyfQ0KeWhhdCA9IGlmZWxzZShwcm9iID4uMTUsMSwwKQ0KY20gPSB0KHRhYmxlKHloYXQsIHRlbXBfbWVyZ2VkJGxhdGVbLXRyYWluXSkpDQpjbQ0KYGBgDQoNCmBgYHtyfQ0Kc3VtKGRpYWcoY20pKS9zdW0oY20pICNBY2N1cmFjeQ0KY21bMSwxXS9zdW0oY21bMSwgYygxLDIpXSkgI0NvcnJlY3QgcmF0ZSBvZiBuZWdhdGl2ZSBwcmVkaWN0ZWQgdmFsdWVzDQpjbVsyLDJdL3N1bShjbVsyLCBjKDEsMildKSAjQ29ycmVjdCByYXRlIG9mIHBvc2l0aXZlIHByZWRpY3RlZCB2YWx1ZQ0KDQpgYGANCg0KYGBge3J9DQoNCmBgYA0KDQoNCg0KDQoNCg==