
library(readr)
library(tidyr)
library(dplyr)
library(ggplot2)
Metode Quasi-Newton di R dapat diakses melalui fungsi optim(), yang merupakan fungsi optimasi tujuan umum. Fungsi optim() mengimplementasikan berbagai metode tetapi di bagian ini kita akan fokus pada metode "BFGS" dan "L-BFGS-B". Untuk contoh ini kita akan menggunakan konsentrasi rata-rata harian nitrogen dioksida (\(NO_2\)) untuk tahun 2016 yang ditemukan dalam file ini. Secara khusus, kita akan fokus pada data untuk monitor yang berlokasi di Negara Bagian Washington.
library(readr)
library(tidyr)
dat0 <- read_csv("daily_42602_2016.csv.bz2")
names(dat0) <- make.names(names(dat0))
dat <- filter(dat0, State.Name == "Washington") %>%
unite(site, State.Code, County.Code, Site.Num) %>%
rename(no2 = Arithmetic.Mean, date = Date.Local) %>%
select(site, date, no2)
Estimasi kepadatan kernel dari data \(NO_2\) menunjukkan distribusi berikut.
library(ggplot2)
ggplot(dat, aes(x = no2)) +
geom_density()

Sebagai langkah awal dalam mengkarakterisasi distribusi nilai NO2 (dan untuk mendemonstrasikan penggunaan optim() untuk model yang pas), kita akan mencoba menyesuaikan model Normal yang terpotong ke data. Normal yang terpotong dapat masuk akal untuk jenis data ini karena mereka benar-benar positif, membuat distribusi Normal standar tidak sesuai.
Untuk normal terpotong, terpotong dari bawah di 0, densitas data adalah \[
f(x)=\frac{\frac{1}{\sigma}\varphi\left(\frac{x-\mu}{\delta}\right)}{\int^\infty_0 \frac{1}{\sigma}\varphi\left(\frac{x-\mu}{\delta}\right)dx}
\]
Parameter yang tidak diketahui adalah \(\mu\) dan \(\sigma\). Mengingat kepadatan, kita dapat mencoba untuk memperkirakan dan dengan kemungkinan maksimum. Dalam hal ini, kita akan meminimalkan negative log-likehood dari data.
Kita dapat menggunakan fungsi deriv() untuk menghitung kemungkinan log negatif dan gradiennya secara otomatis. Karena kita menggunakan metode quasi-Newton di sini kita tidak memerlukan matriks Hessian.
nll_one <- deriv(~ -log(dnorm((x - mu)/s) / s) + log(0.5),
c("mu", "s"),
function.arg = TRUE)
Fungsi optim() bekerja sedikit berbeda dari nlm() karena alih-alih memiliki gradien sebagai atribut kemungkinan log negatif, gradien harus menjadi fungsi yang terpisah.
Pertama kemungkinan log negatif.
nll <- function(p) {
v <- nll_one(p[1], p[2])
sum(v)
}
Fungsi gradien
nll_grad <- function(p) {
v <- nll_one(p[1], p[2])
colSums(attr(v, "gradient"))
}
Sekarang kita dapat meneruskan fungsi nll() dan nll_grad() ke optim() untuk mendapatkan estimasi dan . kita akan menggunakan nilai awal \(\mu=1\) dan \(\sigma=5\). Untuk menggunakan metode quasi-Newton "BFGS", Anda perlu menentukannya dalam argumen metode. Metode default untuk optim() adalah metode simpleks Nelder-Mead. kita juga menentukan hessian = TRUE untuk memberi tahu optim() untuk menghitung secara numerik matriks Hessian pada titik optimal.
x <- dat$no2
res <- optim(c(1, 5), nll, gr = nll_grad,
method = "BFGS", hessian = TRUE)
res
## $par
## [1] 13.23731 8.26315
##
## $value
## [1] 4043.641
##
## $counts
## function gradient
## 35 19
##
## $convergence
## [1] 0
##
## $message
## NULL
##
## $hessian
## [,1] [,2]
## [1,] 2.087005e+01 5.659674e-04
## [2,] 5.659674e-04 4.174582e+01
Fungsi optim() mengembalikan daftar dengan 5 elemen (ditambah matriks Hessian jika hessian = TRUE diatur). Elemen pertama yang harus Anda periksa adalah kode konvergensi. Jika konvergensi adalah 0, itu bagus. Apa pun selain 0 dapat menunjukkan masalah, yang sifatnya tergantung pada algoritme yang Anda gunakan (lihat halaman bantuan untuk optim() untuk detail lebih lanjut). Kali ini kita juga memiliki optim() menghitung Hessian (secara numerik) pada titik optimal sehingga kita dapat memperoleh kesalahan standar asimtotik jika kita mau.
Catatan pertama bahwa ada beberapa pesan yang dicetak ke konsol saat algoritme sedang berjalan yang menunjukkan bahwa NaN dihasilkan oleh fungsi target. Ini kemungkinan karena fungsi tersebut mencoba mengambil log angka negatif. Karena kami menggunakan algoritme "BFGS", kami melakukan pengoptimalan tanpa kendala. Oleh karena itu, kemungkinan pencarian algoritme menghasilkan nilai negatif untuk \(\sigma\), yang tidak masuk akal dalam konteks ini. Untuk membatasi pencarian, kita dapat menggunakan metode "L-BFGS-B" yang merupakan algoritma BFGS “memori terbatas” dengan “kendala kotak”. Ini memungkinkan Anda untuk menempatkan batas bawah dan atas pada setiap parameter dalam model.
Perhatikan bahwa optim() memungkinkan fungsi target Anda untuk menghasilkan nilai NA atau NaN, dan memang dari output tampaknya algoritme akhirnya bertemu pada jawabannya. Tetapi karena kita tahu bahwa parameter dalam model ini dibatasi, kita dapat melanjutkan dan menggunakan pendekatan alternatif.
Di sini kita menetapkan batas bawah untuk semua parameter menjadi 0 tetapi membiarkan batas atas menjadi tak terhingga (Inf), yang merupakan default.
res <- optim(c(1, 5), nll, gr = nll_grad,
method = "L-BFGS-B", hessian = TRUE,
lower = 0)
res
## $par
## [1] 13.237470 8.263546
##
## $value
## [1] 4043.641
##
## $counts
## function gradient
## 14 14
##
## $convergence
## [1] 0
##
## $message
## [1] "CONVERGENCE: REL_REDUCTION_OF_F <= FACTR*EPSMCH"
##
## $hessian
## [,1] [,2]
## [1,] 20.868057205 -0.000250771
## [2,] -0.000250771 41.735838073
Kita dapat melihat sekarang bahwa pesan peringatan hilang, tetapi solusinya identik dengan yang dihasilkan oleh metode "BFGS" asli.
Estimasi kemungkinan maksimum dari \(\mu\) adalah 13,24 dan taksiran dari \(\sigma\) adalah 8.26. Jika kita ingin mendapatkan kesalahan standar asimtotik untuk parameter ini, kita dapat melihat matriks Hessian.
solve(res$hessian) %>%
diag %>%
sqrt
## [1] 0.2189067 0.1547909
Namun dalam hal ini, kami tidak terlalu peduli dengan kesalahan standar sehingga kami akan melanjutkan.
Kita dapat memplot kerapatan asli dari data versus model Normal terpotong yang dipasang untuk melihat seberapa baik kita mengkarakterisasi distribusi. Pertama kita akan mengevaluasi model yang dipasang pada 100 titik antara 0 dan 50.
xpts <- seq(0, 50, len = 100)
dens <- data.frame(xpts = xpts,
ypts = dnorm(xpts, res$par[1], res$par[2]))
Kemudian kita dapat melapisi model yang dipasang di atas kepadatan menggunakan geom_line().
ggplot(dat, aes(x = no2)) +
geom_density() +
geom_line(aes(x = xpts, y = ypts), data = dens, col = "steelblue",
lty = 2)

Ini tidak cocok. Melihat kepadatan halus data, jelas bahwa ada dua mode pada data, menunjukkan bahwa Normal yang terpotong mungkin tidak cukup untuk mengkarakterisasi data.
Salah satu alternatif dalam kasus ini adalah campuran dari dua Normal, yang mungkin menangkap dua mode. Untuk campuran dua komponen, densitas datanya adalah
\[
\lambda\frac{1}{\sigma}\varphi\left(\frac{x-\mu_1}{\sigma_1}\right)+(1-\lambda)\frac{1}{\sigma}\varphi\left(\frac{x-\mu_2}{\sigma_2}\right)
\]
Umumnya, kita melihat bahwa model ini cocok menggunakan algoritma yang lebih kompleks seperti algoritma EM atau metode rantai Markov Monte Carlo. Meskipun metode tersebut memberikan stabilitas yang lebih besar dalam proses estimasi (seperti yang akan kita lihat nanti), kita sebenarnya dapat menggunakan metode tipe Newton untuk memaksimalkan kemungkinan secara langsung dengan sedikit perhatian.
Pertama kita dapat menuliskan kemungkinan log negatif secara simbolis dan mengizinkan fungsi R deriv() untuk menghitung fungsi gradien.
nll_one <- deriv(~ -log(lambda * dnorm((x-mu1)/s1)/s1 + (1-lambda)*dnorm((x-mu2)/s2)/s2),
c("mu1", "mu2", "s1", "s2", "lambda"),
function.arg = TRUE)
Kemudian, seperti sebelumnya, kita dapat menentukan fungsi negative log-likelihood (nll) dan gradien R (nll_grad) secara terpisah.
nll <- function(p) {
p <- as.list(p)
v <- do.call("nll_one", p)
sum(v)
}
nll_grad <- function(p) {
v <- do.call("nll_one", as.list(p))
colSums(attr(v, "gradient"))
}
Terakhir, kita dapat meneruskan fungsi-fungsi tersebut ke optim() dengan vektor parameter awal. Di sini, kami berhati-hati untuk menentukan
- Kami menggunakan metode
"L-BFGS-B" sehingga kami menentukan batas bawah 0 untuk semua parameter dan batas atas 1 untuk parameter \(\lambda\)
- Kami mengatur opsi parscale dalam daftar parameter kontrol, yang mirip dengan argumen typsize ke
nlm(). Tujuannya di sini adalah untuk memberikan optim() skala untuk setiap parameter di sekitar titik optimal.
x <- dat$no2
pstart <- c(5, 10, 2, 3, 0.5)
res <- optim(pstart, nll, gr = nll_grad, method = "L-BFGS-B",
control = list(parscale = c(2, 2, 1, 1, 0.1)),
lower = 0, upper = c(Inf, Inf, Inf, Inf, 1))
res
## $par
## [1] 3.7606598 16.1469811 1.6419640 7.2378153 0.2348927
##
## $value
## [1] 4879.924
##
## $counts
## function gradient
## 17 17
##
## $convergence
## [1] 0
##
## $message
## [1] "CONVERGENCE: REL_REDUCTION_OF_F <= FACTR*EPSMCH"
Kode konvergensi 0 adalah pertanda baik dan estimasi parameter dalam vektor par semuanya tampak masuk akal. Kita dapat melapisi model yang dipasang ke kepadatan yang halus untuk melihat bagaimana modelnya.
xpts <- seq(0, 50, len = 100)
dens <- with(res, {
data.frame(xpts = xpts,
ypts = par[5]*dnorm(xpts, par[1], par[3]) + (1-par[5])*dnorm(xpts, par[2], par[4]))
})
ggplot(dat, aes(x = no2)) +
geom_density() +
geom_line(aes(x = xpts, y = ypts), data = dens, col = "steelblue",
lty = 2)

Kesesuaiannya masih kurang bagus, tetapi setidaknya model ini menangkap secara kasar lokasi dua mode dalam kepadatan. Juga, tampaknya model menangkap ekor kerapatan dengan cukup baik, meskipun ini perlu diperiksa lebih hati-hati dengan melihat kuantil.
Terakhir, seperti kebanyakan model dan skema pengoptimalan, biasanya ide yang baik untuk memvariasikan titik awal untuk melihat apakah perkiraan kami saat ini adalah mode lokal.
pstart <- c(1, 20, 5, 2, 0.1)
res <- optim(pstart, nll, gr = nll_grad, method = "L-BFGS-B",
control = list(parscale = c(2, 2, 1, 1, 0.1)),
lower = 0, upper = c(Inf, Inf, Inf, Inf, 1))
res
## $par
## [1] 3.760571 16.146834 1.641961 7.237776 0.234892
##
## $value
## [1] 4879.924
##
## $counts
## function gradient
## 22 22
##
## $convergence
## [1] 0
##
## $message
## [1] "CONVERGENCE: REL_REDUCTION_OF_F <= FACTR*EPSMCH"
Di sini kita melihat bahwa dengan titik awal yang sedikit berbeda, kita mendapatkan nilai yang sama dan kemungkinan log negatif minimum yang sama.
LS0tDQp0aXRsZTogIk9wdGltaXphdGlvbiINCnN1YnRpdGxlOiAiUXVhc2kgTmV3dG9uIE1ldGhvZHMiDQphdXRob3I6IA0KZGF0ZTogImByIGZvcm1hdChTeXMuRGF0ZSgpLCAnJUIgJWQsICVZJylgIg0Kb3V0cHV0OiANCiAgaHRtbF9kb2N1bWVudDogDQogICAgaHRtbF9kb2N1bWVudDogbnVsbA0KICAgIGNvZGVfZm9sZGluZzogaGlkZQ0KICAgIHRvYzogeWVzDQogICAgdG9jX2Zsb2F0Og0KICAgICAgY29sbGFwc2VkOiB5ZXMNCiAgICBudW1iZXJfc2VjdGlvbnM6IHllcw0KICAgIGNvZGVfZG93bmxvYWQ6IHllcw0KICAgIHRoZW1lOiBzYW5kc3RvbmUNCiAgICBjc3M6IEQ6L0p1bGlhbiBTYWxvbW8vTWF0YW5hLzAwMDAvc3R5bGUuY3NzDQogICAgaGlnaGxpZ2h0OiBtb25vY2hyb21lDQotLS0NCg0KYGBge3IgaW5jbHVkZT1GQUxTRX0NCmtuaXRyOjpvcHRzX2NodW5rJHNldChjbGFzcy5zb3VyY2UgPSAibm9jb3B5IiwNCiAgICAgICAgICAgICAgICAgICAgICBjbGFzcy5vdXRwdXQgPSAibm9jb3B5IiwNCiAgICAgICAgICAgICAgICAgICAgICBtZXNzYWdlID0gRiwNCiAgICAgICAgICAgICAgICAgICAgICB3YXJuaW5nID0gRikNCmBgYA0KDQpgYGB7ciBtZSwgZWNobz1GQUxTRSxmaWcuYWxpZ249J2NlbnRlcicsIG91dC53aWR0aCA9ICczMCUnfQ0Ka25pdHI6OmluY2x1ZGVfZ3JhcGhpY3MoIkQ6L0p1bGlhbiBTYWxvbW8vTWF0YW5hLzAwMDAvbG9nby5wbmciKQ0KYGBgDQoNCmBgYHtyfQ0KbGlicmFyeShyZWFkcikNCmxpYnJhcnkodGlkeXIpDQpsaWJyYXJ5KGRwbHlyKQ0KbGlicmFyeShnZ3Bsb3QyKQ0KYGBgDQoNCk1ldG9kZSBRdWFzaS1OZXd0b24gZGkgUiBkYXBhdCBkaWFrc2VzIG1lbGFsdWkgZnVuZ3NpIGBvcHRpbSgpYCwgeWFuZyBtZXJ1cGFrYW4gZnVuZ3NpIG9wdGltYXNpIHR1anVhbiB1bXVtLiBGdW5nc2kgYG9wdGltKClgIG1lbmdpbXBsZW1lbnRhc2lrYW4gYmVyYmFnYWkgbWV0b2RlIHRldGFwaSBkaSBiYWdpYW4gaW5pIGtpdGEgYWthbiBmb2t1cyBwYWRhIG1ldG9kZSBgIkJGR1MiYCBkYW4gYCJMLUJGR1MtQiJgLiBVbnR1ayBjb250b2ggaW5pIGtpdGEgYWthbiBtZW5nZ3VuYWthbiBrb25zZW50cmFzaSByYXRhLXJhdGEgaGFyaWFuIG5pdHJvZ2VuIGRpb2tzaWRhICgkTk9fMiQpIHVudHVrIHRhaHVuIDIwMTYgeWFuZyBkaXRlbXVrYW4gZGFsYW0gW2ZpbGUgaW5pXShodHRwczovL2dpdGh1Yi5jb20vZGthaGxlL2FkdnN0YXRjb21wL3Jhdy9tYXN0ZXIvZGFpbHlfNDI2MDJfMjAxNi5jc3YuYnoyKS4gU2VjYXJhIGtodXN1cywga2l0YSBha2FuIGZva3VzIHBhZGEgZGF0YSB1bnR1ayBtb25pdG9yIHlhbmcgYmVybG9rYXNpIGRpIE5lZ2FyYSBCYWdpYW4gV2FzaGluZ3Rvbi4NCg0KYGBge3J9DQpsaWJyYXJ5KHJlYWRyKQ0KbGlicmFyeSh0aWR5cikNCmRhdDAgPC0gcmVhZF9jc3YoImRhaWx5XzQyNjAyXzIwMTYuY3N2LmJ6MiIpDQpuYW1lcyhkYXQwKSA8LSBtYWtlLm5hbWVzKG5hbWVzKGRhdDApKQ0KZGF0IDwtIGZpbHRlcihkYXQwLCBTdGF0ZS5OYW1lID09ICJXYXNoaW5ndG9uIikgJT4lDQogICAgICAgIHVuaXRlKHNpdGUsIFN0YXRlLkNvZGUsIENvdW50eS5Db2RlLCBTaXRlLk51bSkgJT4lDQogICAgICAgIHJlbmFtZShubzIgPSBBcml0aG1ldGljLk1lYW4sIGRhdGUgPSBEYXRlLkxvY2FsKSAlPiUNCiAgICAgICAgc2VsZWN0KHNpdGUsIGRhdGUsIG5vMikNCmBgYA0KDQpFc3RpbWFzaSBrZXBhZGF0YW4ga2VybmVsIGRhcmkgZGF0YSAkTk9fMiQgbWVudW5qdWtrYW4gZGlzdHJpYnVzaSBiZXJpa3V0Lg0KDQpgYGB7cn0NCmxpYnJhcnkoZ2dwbG90MikNCmdncGxvdChkYXQsIGFlcyh4ID0gbm8yKSkgKyANCiAgICAgICAgZ2VvbV9kZW5zaXR5KCkNCmBgYA0KDQpTZWJhZ2FpIGxhbmdrYWggYXdhbCBkYWxhbSBtZW5na2FyYWt0ZXJpc2FzaSBkaXN0cmlidXNpIG5pbGFpIE5PMiAoZGFuIHVudHVrIG1lbmRlbW9uc3RyYXNpa2FuIHBlbmdndW5hYW4gYG9wdGltKClgIHVudHVrIG1vZGVsIHlhbmcgcGFzKSwga2l0YSBha2FuIG1lbmNvYmEgbWVueWVzdWFpa2FuIG1vZGVsIE5vcm1hbCB5YW5nIHRlcnBvdG9uZyBrZSBkYXRhLiBOb3JtYWwgeWFuZyB0ZXJwb3RvbmcgZGFwYXQgbWFzdWsgYWthbCB1bnR1ayBqZW5pcyBkYXRhIGluaSBrYXJlbmEgbWVyZWthIGJlbmFyLWJlbmFyIHBvc2l0aWYsIG1lbWJ1YXQgZGlzdHJpYnVzaSBOb3JtYWwgc3RhbmRhciB0aWRhayBzZXN1YWkuDQoNClVudHVrIG5vcm1hbCB0ZXJwb3RvbmcsIHRlcnBvdG9uZyBkYXJpIGJhd2FoIGRpIDAsIGRlbnNpdGFzIGRhdGEgYWRhbGFoDQokJA0KZih4KT1cZnJhY3tcZnJhY3sxfXtcc2lnbWF9XHZhcnBoaVxsZWZ0KFxmcmFje3gtXG11fXtcZGVsdGF9XHJpZ2h0KX17XGludF5caW5mdHlfMCBcZnJhY3sxfXtcc2lnbWF9XHZhcnBoaVxsZWZ0KFxmcmFje3gtXG11fXtcZGVsdGF9XHJpZ2h0KWR4fQ0KJCQNCg0KUGFyYW1ldGVyIHlhbmcgdGlkYWsgZGlrZXRhaHVpIGFkYWxhaCAkXG11JCBkYW4gJFxzaWdtYSQuIE1lbmdpbmdhdCBrZXBhZGF0YW4sIGtpdGEgZGFwYXQgbWVuY29iYSB1bnR1ayBtZW1wZXJraXJha2FuIGRhbiBkZW5nYW4ga2VtdW5na2luYW4gbWFrc2ltdW0uIERhbGFtIGhhbCBpbmksIGtpdGEgYWthbiBtZW1pbmltYWxrYW4gbmVnYXRpdmUgbG9nLWxpa2Vob29kIGRhcmkgZGF0YS4NCg0KS2l0YSBkYXBhdCBtZW5nZ3VuYWthbiBmdW5nc2kgZGVyaXYoKSB1bnR1ayBtZW5naGl0dW5nIGtlbXVuZ2tpbmFuIGxvZyBuZWdhdGlmIGRhbiBncmFkaWVubnlhIHNlY2FyYSBvdG9tYXRpcy4gS2FyZW5hIGtpdGEgbWVuZ2d1bmFrYW4gbWV0b2RlIHF1YXNpLU5ld3RvbiBkaSBzaW5pIGtpdGEgdGlkYWsgbWVtZXJsdWthbiBtYXRyaWtzIEhlc3NpYW4uDQoNCmBgYHtyfQ0KbmxsX29uZSA8LSBkZXJpdih+IC1sb2coZG5vcm0oKHggLSBtdSkvcykgLyBzKSArIGxvZygwLjUpLA0KICAgICAgICAgICAgICAgICBjKCJtdSIsICJzIiksIA0KICAgICAgICAgICAgICAgICBmdW5jdGlvbi5hcmcgPSBUUlVFKQ0KYGBgDQoNCkZ1bmdzaSBgb3B0aW0oKWAgYmVrZXJqYSBzZWRpa2l0IGJlcmJlZGEgZGFyaSBgbmxtKClgIGthcmVuYSBhbGloLWFsaWggbWVtaWxpa2kgZ3JhZGllbiBzZWJhZ2FpIGF0cmlidXQga2VtdW5na2luYW4gbG9nIG5lZ2F0aWYsIGdyYWRpZW4gaGFydXMgbWVuamFkaSBmdW5nc2kgeWFuZyB0ZXJwaXNhaC4NCg0KUGVydGFtYSBrZW11bmdraW5hbiBsb2cgbmVnYXRpZi4NCg0KYGBge3J9DQpubGwgPC0gZnVuY3Rpb24ocCkgew0KICAgICAgICB2IDwtIG5sbF9vbmUocFsxXSwgcFsyXSkNCiAgICAgICAgc3VtKHYpDQp9DQpgYGANCg0KRnVuZ3NpIGdyYWRpZW4NCg0KYGBge3J9DQpubGxfZ3JhZCA8LSBmdW5jdGlvbihwKSB7DQogICAgICAgIHYgPC0gbmxsX29uZShwWzFdLCBwWzJdKQ0KICAgICAgICBjb2xTdW1zKGF0dHIodiwgImdyYWRpZW50IikpDQp9DQpgYGANCg0KU2VrYXJhbmcga2l0YSBkYXBhdCBtZW5lcnVza2FuIGZ1bmdzaSBgbmxsKClgIGRhbiBgbmxsX2dyYWQoKWAga2UgYG9wdGltKClgIHVudHVrIG1lbmRhcGF0a2FuIGVzdGltYXNpIGRhbiAuIGtpdGEgYWthbiBtZW5nZ3VuYWthbiBuaWxhaSBhd2FsICRcbXU9MSQgZGFuICRcc2lnbWE9NSQuIFVudHVrIG1lbmdndW5ha2FuIG1ldG9kZSBxdWFzaS1OZXd0b24gYCJCRkdTImAsIEFuZGEgcGVybHUgbWVuZW50dWthbm55YSBkYWxhbSBhcmd1bWVuIGBtZXRvZGVgLiBNZXRvZGUgZGVmYXVsdCB1bnR1ayBgb3B0aW0oKWAgYWRhbGFoIG1ldG9kZSBzaW1wbGVrcyBOZWxkZXItTWVhZC4ga2l0YSBqdWdhIG1lbmVudHVrYW4gYGhlc3NpYW4gPSBUUlVFYCB1bnR1ayBtZW1iZXJpIHRhaHUgYG9wdGltKClgIHVudHVrIG1lbmdoaXR1bmcgc2VjYXJhIG51bWVyaWsgbWF0cmlrcyBIZXNzaWFuIHBhZGEgdGl0aWsgb3B0aW1hbC4NCg0KYGBge3J9DQp4IDwtIGRhdCRubzINCnJlcyA8LSBvcHRpbShjKDEsIDUpLCBubGwsIGdyID0gbmxsX2dyYWQsIA0KICAgICAgICAgICAgIG1ldGhvZCA9ICJCRkdTIiwgaGVzc2lhbiA9IFRSVUUpDQpyZXMNCmBgYA0KDQpGdW5nc2kgYG9wdGltKClgIG1lbmdlbWJhbGlrYW4gZGFmdGFyIGRlbmdhbiA1IGVsZW1lbiAoZGl0YW1iYWggbWF0cmlrcyBIZXNzaWFuIGppa2EgYGhlc3NpYW4gPSBUUlVFYCBkaWF0dXIpLiBFbGVtZW4gcGVydGFtYSB5YW5nIGhhcnVzIEFuZGEgcGVyaWtzYSBhZGFsYWgga29kZSBga29udmVyZ2Vuc2lgLiBKaWthIGBrb252ZXJnZW5zaWAgYWRhbGFoIDAsIGl0dSBiYWd1cy4gQXBhIHB1biBzZWxhaW4gMCBkYXBhdCBtZW51bmp1a2thbiBtYXNhbGFoLCB5YW5nIHNpZmF0bnlhIHRlcmdhbnR1bmcgcGFkYSBhbGdvcml0bWUgeWFuZyBBbmRhIGd1bmFrYW4gKGxpaGF0IGhhbGFtYW4gYmFudHVhbiB1bnR1ayBgb3B0aW0oKWAgdW50dWsgZGV0YWlsIGxlYmloIGxhbmp1dCkuIEthbGkgaW5pIGtpdGEganVnYSBtZW1pbGlraSBgb3B0aW0oKWAgbWVuZ2hpdHVuZyBIZXNzaWFuIChzZWNhcmEgbnVtZXJpaykgcGFkYSB0aXRpayBvcHRpbWFsIHNlaGluZ2dhIGtpdGEgZGFwYXQgbWVtcGVyb2xlaCBrZXNhbGFoYW4gc3RhbmRhciBhc2ltdG90aWsgamlrYSBraXRhIG1hdS4NCg0KQ2F0YXRhbiBwZXJ0YW1hIGJhaHdhIGFkYSBiZWJlcmFwYSBwZXNhbiB5YW5nIGRpY2V0YWsga2Uga29uc29sIHNhYXQgYWxnb3JpdG1lIHNlZGFuZyBiZXJqYWxhbiB5YW5nIG1lbnVuanVra2FuIGJhaHdhIGBOYU5gIGRpaGFzaWxrYW4gb2xlaCBmdW5nc2kgdGFyZ2V0LiBJbmkga2VtdW5na2luYW4ga2FyZW5hIGZ1bmdzaSB0ZXJzZWJ1dCBtZW5jb2JhIG1lbmdhbWJpbCBsb2cgYW5na2EgbmVnYXRpZi4gS2FyZW5hIGthbWkgbWVuZ2d1bmFrYW4gYWxnb3JpdG1lIGAiQkZHUyJgLCBrYW1pIG1lbGFrdWthbiBwZW5nb3B0aW1hbGFuIHRhbnBhIGtlbmRhbGEuIE9sZWgga2FyZW5hIGl0dSwga2VtdW5na2luYW4gcGVuY2FyaWFuIGFsZ29yaXRtZSBtZW5naGFzaWxrYW4gbmlsYWkgbmVnYXRpZiB1bnR1ayAkXHNpZ21hJCwgeWFuZyB0aWRhayBtYXN1ayBha2FsIGRhbGFtIGtvbnRla3MgaW5pLiBVbnR1ayBtZW1iYXRhc2kgcGVuY2FyaWFuLCBraXRhIGRhcGF0IG1lbmdndW5ha2FuIG1ldG9kZSBgIkwtQkZHUy1CImAgeWFuZyBtZXJ1cGFrYW4gYWxnb3JpdG1hIEJGR1MgIm1lbW9yaSB0ZXJiYXRhcyIgZGVuZ2FuICJrZW5kYWxhIGtvdGFrIi4gSW5pIG1lbXVuZ2tpbmthbiBBbmRhIHVudHVrIG1lbmVtcGF0a2FuIGJhdGFzIGJhd2FoIGRhbiBhdGFzIHBhZGEgc2V0aWFwIHBhcmFtZXRlciBkYWxhbSBtb2RlbC4NCg0KUGVyaGF0aWthbiBiYWh3YSBgb3B0aW0oKWAgbWVtdW5na2lua2FuIGZ1bmdzaSB0YXJnZXQgQW5kYSB1bnR1ayBtZW5naGFzaWxrYW4gbmlsYWkgYE5BYCBhdGF1IGBOYU5gLCBkYW4gbWVtYW5nIGRhcmkgb3V0cHV0IHRhbXBha255YSBhbGdvcml0bWUgYWtoaXJueWEgYmVydGVtdSBwYWRhIGphd2FiYW5ueWEuIFRldGFwaSBrYXJlbmEga2l0YSB0YWh1IGJhaHdhIHBhcmFtZXRlciBkYWxhbSBtb2RlbCBpbmkgZGliYXRhc2ksIGtpdGEgZGFwYXQgbWVsYW5qdXRrYW4gZGFuIG1lbmdndW5ha2FuIHBlbmRla2F0YW4gYWx0ZXJuYXRpZi4NCg0KRGkgc2luaSBraXRhIG1lbmV0YXBrYW4gYmF0YXMgYmF3YWggdW50dWsgc2VtdWEgcGFyYW1ldGVyIG1lbmphZGkgMCB0ZXRhcGkgbWVtYmlhcmthbiBiYXRhcyBhdGFzIG1lbmphZGkgdGFrIHRlcmhpbmdnYSAoYEluZmApLCB5YW5nIG1lcnVwYWthbiBkZWZhdWx0Lg0KDQpgYGB7cn0NCnJlcyA8LSBvcHRpbShjKDEsIDUpLCBubGwsIGdyID0gbmxsX2dyYWQsIA0KICAgICAgICAgICAgIG1ldGhvZCA9ICJMLUJGR1MtQiIsIGhlc3NpYW4gPSBUUlVFLA0KICAgICAgICAgICAgIGxvd2VyID0gMCkNCnJlcw0KYGBgDQoNCktpdGEgZGFwYXQgbWVsaWhhdCBzZWthcmFuZyBiYWh3YSBwZXNhbiBwZXJpbmdhdGFuIGhpbGFuZywgdGV0YXBpIHNvbHVzaW55YSBpZGVudGlrIGRlbmdhbiB5YW5nIGRpaGFzaWxrYW4gb2xlaCBtZXRvZGUgYCJCRkdTImAgYXNsaS4NCg0KRXN0aW1hc2kga2VtdW5na2luYW4gbWFrc2ltdW0gZGFyaSAkXG11JCBhZGFsYWggMTMsMjQgZGFuIHRha3NpcmFuIGRhcmkgJFxzaWdtYSQgYWRhbGFoIDguMjYuIEppa2Ega2l0YSBpbmdpbiBtZW5kYXBhdGthbiBrZXNhbGFoYW4gc3RhbmRhciBhc2ltdG90aWsgdW50dWsgcGFyYW1ldGVyIGluaSwga2l0YSBkYXBhdCBtZWxpaGF0IG1hdHJpa3MgSGVzc2lhbi4NCg0KYGBge3J9DQpzb2x2ZShyZXMkaGVzc2lhbikgJT4lDQogICAgICAgIGRpYWcgJT4lDQogICAgICAgIHNxcnQNCmBgYA0KDQpOYW11biBkYWxhbSBoYWwgaW5pLCBrYW1pIHRpZGFrIHRlcmxhbHUgcGVkdWxpIGRlbmdhbiBrZXNhbGFoYW4gc3RhbmRhciBzZWhpbmdnYSBrYW1pIGFrYW4gbWVsYW5qdXRrYW4uDQoNCktpdGEgZGFwYXQgbWVtcGxvdCBrZXJhcGF0YW4gYXNsaSBkYXJpIGRhdGEgdmVyc3VzIG1vZGVsIE5vcm1hbCB0ZXJwb3RvbmcgeWFuZyBkaXBhc2FuZyB1bnR1ayBtZWxpaGF0IHNlYmVyYXBhIGJhaWsga2l0YSBtZW5na2FyYWt0ZXJpc2FzaSBkaXN0cmlidXNpLiBQZXJ0YW1hIGtpdGEgYWthbiBtZW5nZXZhbHVhc2kgbW9kZWwgeWFuZyBkaXBhc2FuZyBwYWRhIDEwMCB0aXRpayBhbnRhcmEgMCBkYW4gNTAuDQoNCmBgYHtyfQ0KeHB0cyA8LSBzZXEoMCwgNTAsIGxlbiA9IDEwMCkNCmRlbnMgPC0gZGF0YS5mcmFtZSh4cHRzID0geHB0cywNCiAgICAgICAgICAgICAgICAgICB5cHRzID0gZG5vcm0oeHB0cywgcmVzJHBhclsxXSwgcmVzJHBhclsyXSkpDQpgYGANCg0KS2VtdWRpYW4ga2l0YSBkYXBhdCBtZWxhcGlzaSBtb2RlbCB5YW5nIGRpcGFzYW5nIGRpIGF0YXMga2VwYWRhdGFuIG1lbmdndW5ha2FuIGBnZW9tX2xpbmUoKWAuDQoNCmBgYHtyfQ0KZ2dwbG90KGRhdCwgYWVzKHggPSBubzIpKSArIA0KICAgICAgICBnZW9tX2RlbnNpdHkoKSArIA0KICAgICAgICBnZW9tX2xpbmUoYWVzKHggPSB4cHRzLCB5ID0geXB0cyksIGRhdGEgPSBkZW5zLCBjb2wgPSAic3RlZWxibHVlIiwNCiAgICAgICAgICAgICAgICAgIGx0eSA9IDIpDQpgYGANCg0KSW5pIHRpZGFrIGNvY29rLiBNZWxpaGF0IGtlcGFkYXRhbiBoYWx1cyBkYXRhLCBqZWxhcyBiYWh3YSBhZGEgZHVhIG1vZGUgcGFkYSBkYXRhLCBtZW51bmp1a2thbiBiYWh3YSBOb3JtYWwgeWFuZyB0ZXJwb3RvbmcgbXVuZ2tpbiB0aWRhayBjdWt1cCB1bnR1ayBtZW5na2FyYWt0ZXJpc2FzaSBkYXRhLg0KDQpTYWxhaCBzYXR1IGFsdGVybmF0aWYgZGFsYW0ga2FzdXMgaW5pIGFkYWxhaCBjYW1wdXJhbiBkYXJpIGR1YSBOb3JtYWwsIHlhbmcgbXVuZ2tpbiBtZW5hbmdrYXAgZHVhIG1vZGUuIFVudHVrIGNhbXB1cmFuIGR1YSBrb21wb25lbiwgZGVuc2l0YXMgZGF0YW55YSBhZGFsYWgNCg0KJCQNClxsYW1iZGFcZnJhY3sxfXtcc2lnbWF9XHZhcnBoaVxsZWZ0KFxmcmFje3gtXG11XzF9e1xzaWdtYV8xfVxyaWdodCkrKDEtXGxhbWJkYSlcZnJhY3sxfXtcc2lnbWF9XHZhcnBoaVxsZWZ0KFxmcmFje3gtXG11XzJ9e1xzaWdtYV8yfVxyaWdodCkNCiQkDQoNClVtdW1ueWEsIGtpdGEgbWVsaWhhdCBiYWh3YSBtb2RlbCBpbmkgY29jb2sgbWVuZ2d1bmFrYW4gYWxnb3JpdG1hIHlhbmcgbGViaWgga29tcGxla3Mgc2VwZXJ0aSBhbGdvcml0bWEgRU0gYXRhdSBtZXRvZGUgcmFudGFpIE1hcmtvdiBNb250ZSBDYXJsby4gTWVza2lwdW4gbWV0b2RlIHRlcnNlYnV0IG1lbWJlcmlrYW4gc3RhYmlsaXRhcyB5YW5nIGxlYmloIGJlc2FyIGRhbGFtIHByb3NlcyBlc3RpbWFzaSAoc2VwZXJ0aSB5YW5nIGFrYW4ga2l0YSBsaWhhdCBuYW50aSksIGtpdGEgc2ViZW5hcm55YSBkYXBhdCBtZW5nZ3VuYWthbiBtZXRvZGUgdGlwZSBOZXd0b24gdW50dWsgbWVtYWtzaW1hbGthbiBrZW11bmdraW5hbiBzZWNhcmEgbGFuZ3N1bmcgZGVuZ2FuIHNlZGlraXQgcGVyaGF0aWFuLg0KDQpQZXJ0YW1hIGtpdGEgZGFwYXQgbWVudWxpc2thbiBrZW11bmdraW5hbiBsb2cgbmVnYXRpZiBzZWNhcmEgc2ltYm9saXMgZGFuIG1lbmdpemlua2FuIGZ1bmdzaSBSIGBkZXJpdigpYCB1bnR1ayBtZW5naGl0dW5nIGZ1bmdzaSBncmFkaWVuLg0KDQpgYGB7cn0NCm5sbF9vbmUgPC0gZGVyaXYofiAtbG9nKGxhbWJkYSAqIGRub3JtKCh4LW11MSkvczEpL3MxICsgKDEtbGFtYmRhKSpkbm9ybSgoeC1tdTIpL3MyKS9zMiksIA0KICAgICAgICAgICAgICAgICBjKCJtdTEiLCAibXUyIiwgInMxIiwgInMyIiwgImxhbWJkYSIpLCANCiAgICAgICAgICAgICAgICAgZnVuY3Rpb24uYXJnID0gVFJVRSkNCmBgYA0KDQpLZW11ZGlhbiwgc2VwZXJ0aSBzZWJlbHVtbnlhLCBraXRhIGRhcGF0IG1lbmVudHVrYW4gZnVuZ3NpIG5lZ2F0aXZlIGxvZy1saWtlbGlob29kIChgbmxsYCkgZGFuIGdyYWRpZW4gUiAoYG5sbF9ncmFkYCkgc2VjYXJhIHRlcnBpc2FoLg0KDQpgYGB7cn0NCm5sbCA8LSBmdW5jdGlvbihwKSB7DQogICAgICAgIHAgPC0gYXMubGlzdChwKQ0KICAgICAgICB2IDwtIGRvLmNhbGwoIm5sbF9vbmUiLCBwKQ0KICAgICAgICBzdW0odikNCn0NCm5sbF9ncmFkIDwtIGZ1bmN0aW9uKHApIHsNCiAgICAgICAgdiA8LSBkby5jYWxsKCJubGxfb25lIiwgYXMubGlzdChwKSkNCiAgICAgICAgY29sU3VtcyhhdHRyKHYsICJncmFkaWVudCIpKQ0KfQ0KYGBgDQoNClRlcmFraGlyLCBraXRhIGRhcGF0IG1lbmVydXNrYW4gZnVuZ3NpLWZ1bmdzaSB0ZXJzZWJ1dCBrZSBgb3B0aW0oKWAgZGVuZ2FuIHZla3RvciBwYXJhbWV0ZXIgYXdhbC4gRGkgc2luaSwga2FtaSBiZXJoYXRpLWhhdGkgdW50dWsgbWVuZW50dWthbg0KDQoqIEthbWkgbWVuZ2d1bmFrYW4gbWV0b2RlIGAiTC1CRkdTLUIiYCBzZWhpbmdnYSBrYW1pIG1lbmVudHVrYW4gYmF0YXMgYmF3YWggMCB1bnR1ayBzZW11YSBwYXJhbWV0ZXIgZGFuIGJhdGFzIGF0YXMgMSB1bnR1ayBwYXJhbWV0ZXIgJFxsYW1iZGEkDQoqIEthbWkgbWVuZ2F0dXIgb3BzaSBwYXJzY2FsZSBkYWxhbSBkYWZ0YXIgcGFyYW1ldGVyIGtvbnRyb2wsIHlhbmcgbWlyaXAgZGVuZ2FuIGFyZ3VtZW4gdHlwc2l6ZSBrZSBgbmxtKClgLiBUdWp1YW5ueWEgZGkgc2luaSBhZGFsYWggdW50dWsgbWVtYmVyaWthbiBgb3B0aW0oKWAgc2thbGEgdW50dWsgc2V0aWFwIHBhcmFtZXRlciBkaSBzZWtpdGFyIHRpdGlrIG9wdGltYWwuDQoNCmBgYHtyfQ0KeCA8LSBkYXQkbm8yDQpwc3RhcnQgPC0gYyg1LCAxMCwgMiwgMywgMC41KQ0KcmVzIDwtIG9wdGltKHBzdGFydCwgbmxsLCBnciA9IG5sbF9ncmFkLCBtZXRob2QgPSAiTC1CRkdTLUIiLA0KICAgICAgICAgICAgIGNvbnRyb2wgPSBsaXN0KHBhcnNjYWxlID0gYygyLCAyLCAxLCAxLCAwLjEpKSwNCiAgICAgICAgICAgICBsb3dlciA9IDAsIHVwcGVyID0gYyhJbmYsIEluZiwgSW5mLCBJbmYsIDEpKQ0KcmVzDQpgYGANCg0KS29kZSBga29udmVyZ2Vuc2lgIDAgYWRhbGFoIHBlcnRhbmRhIGJhaWsgZGFuIGVzdGltYXNpIHBhcmFtZXRlciBkYWxhbSB2ZWt0b3IgYHBhcmAgc2VtdWFueWEgdGFtcGFrIG1hc3VrIGFrYWwuIEtpdGEgZGFwYXQgbWVsYXBpc2kgbW9kZWwgeWFuZyBkaXBhc2FuZyBrZSBrZXBhZGF0YW4geWFuZyBoYWx1cyB1bnR1ayBtZWxpaGF0IGJhZ2FpbWFuYSBtb2RlbG55YS4NCg0KYGBge3J9DQp4cHRzIDwtIHNlcSgwLCA1MCwgbGVuID0gMTAwKQ0KZGVucyA8LSB3aXRoKHJlcywgew0KICAgICAgICBkYXRhLmZyYW1lKHhwdHMgPSB4cHRzLCANCiAgICAgICAgICAgICAgICAgICB5cHRzID0gcGFyWzVdKmRub3JtKHhwdHMsIHBhclsxXSwgcGFyWzNdKSArICgxLXBhcls1XSkqZG5vcm0oeHB0cywgcGFyWzJdLCBwYXJbNF0pKQ0KfSkNCmdncGxvdChkYXQsIGFlcyh4ID0gbm8yKSkgKyANCiAgICAgICAgZ2VvbV9kZW5zaXR5KCkgKyANCiAgICAgICAgZ2VvbV9saW5lKGFlcyh4ID0geHB0cywgeSA9IHlwdHMpLCBkYXRhID0gZGVucywgY29sID0gInN0ZWVsYmx1ZSIsDQogICAgICAgICAgICAgICAgICBsdHkgPSAyKQ0KYGBgDQoNCktlc2VzdWFpYW5ueWEgbWFzaWgga3VyYW5nIGJhZ3VzLCB0ZXRhcGkgc2V0aWRha255YSBtb2RlbCBpbmkgbWVuYW5na2FwIHNlY2FyYSBrYXNhciBsb2thc2kgZHVhIG1vZGUgZGFsYW0ga2VwYWRhdGFuLiBKdWdhLCB0YW1wYWtueWEgbW9kZWwgbWVuYW5na2FwIGVrb3Iga2VyYXBhdGFuIGRlbmdhbiBjdWt1cCBiYWlrLCBtZXNraXB1biBpbmkgcGVybHUgZGlwZXJpa3NhIGxlYmloIGhhdGktaGF0aSBkZW5nYW4gbWVsaWhhdCBrdWFudGlsLg0KDQpUZXJha2hpciwgc2VwZXJ0aSBrZWJhbnlha2FuIG1vZGVsIGRhbiBza2VtYSBwZW5nb3B0aW1hbGFuLCBiaWFzYW55YSBpZGUgeWFuZyBiYWlrIHVudHVrIG1lbXZhcmlhc2lrYW4gdGl0aWsgYXdhbCB1bnR1ayBtZWxpaGF0IGFwYWthaCBwZXJraXJhYW4ga2FtaSBzYWF0IGluaSBhZGFsYWggbW9kZSBsb2thbC4NCg0KYGBge3J9DQpwc3RhcnQgPC0gYygxLCAyMCwgNSwgMiwgMC4xKQ0KcmVzIDwtIG9wdGltKHBzdGFydCwgbmxsLCBnciA9IG5sbF9ncmFkLCBtZXRob2QgPSAiTC1CRkdTLUIiLA0KICAgICAgICAgICAgIGNvbnRyb2wgPSBsaXN0KHBhcnNjYWxlID0gYygyLCAyLCAxLCAxLCAwLjEpKSwNCiAgICAgICAgICAgICBsb3dlciA9IDAsIHVwcGVyID0gYyhJbmYsIEluZiwgSW5mLCBJbmYsIDEpKQ0KcmVzDQpgYGANCg0KRGkgc2luaSBraXRhIG1lbGloYXQgYmFod2EgZGVuZ2FuIHRpdGlrIGF3YWwgeWFuZyBzZWRpa2l0IGJlcmJlZGEsIGtpdGEgbWVuZGFwYXRrYW4gbmlsYWkgeWFuZyBzYW1hIGRhbiBrZW11bmdraW5hbiBsb2cgbmVnYXRpZiBtaW5pbXVtIHlhbmcgc2FtYS4=