Schematyczna implementacja sieci GAN
W tej sekcji wyjaśnię implementację najprostszej formy sieci GAN w pakiecie Keras. Sieci GAN są zaawansowane, a więc zagłębianie się w techniczne szczegóły wykraczałoby poza zakres tematyczny tej książki. Zaprezentuję implementację głębokiej konwolucyjnej sieci GAN (DCGAN) — sieci GAN, w której generator i dyskryminator są głębokimi sieciami konwolucyjnymi. W praktyce zastosuję warstwę lay-er_conv_2d_transpose w celu zwiększenia próbkowania w generatorze.
Sieć GAN będzie trenowana na zbiorze CIFAR10 składającym się z 50 000 kolorowych obrazów o rozdzielczości 3232, podzielonych na 10 równych klas (w każdej z klas znajduje się 5000 obrazów). Dla ułatwienia będziemy korzystać tylko z obrazów należących do klasy „żaba”.
Schematycznie działanie tej sieci GAN można przedstawić w następujący sposób:
- Sieć generatora (generator) mapuje wektory o kształcie (latent_dim) na obrazy o kształcie (32, 32, 3).
- Sieć dyskryminatora (discriminator) mapuje obrazy o kształcie (32, 32, 3) na binarną wartość określającą prawdopodobieństwo tego, że obraz jest prawdziwy.
- Sieć gan tworzy łańcuch składający się z generatora i dyskryminatora: gan(x) <- discriminator(generator(x)). Sieć gan mapuje wektory niejawnej przestrzeni na oceny realizmu wystawiane przez dyskryminator.
- Trenujemy dyskryminator przy użyciu przykładów prawdziwych i sztucznych obrazów oznaczonych etykietami, tak jakbyśmy trenowali zwykły model klasyfikacji obrazów.
- W celu wytrenowania generatora korzystamy z gradien-tów wag generatora w odniesieniu do straty modelu gan. W związku z tym każdy krok trenowania ma modyfikować wagi generatora tak, aby zwiększyć prawdopodobieństwo zaklasyfikowania wygenerowanych obrazów jako prawdzi-wych. Innymi słowy, trenujemy generator tak, aby był w stanie oszukać dyskryminator.
Zbiór przydatnych rozwiązań
Proces trenowania sieci GAN i dostrajania ich implementacji jest bardzo trudny. W związku z tym warto poznać rozwiązania ułatwiające pracę z tymi sieciami. Rozwiązania te mają naturę heurystyczną i nie są teoretycznymi wskazówkami — podobnie jak z większością zagadnień uczenia głębokiego, mamy do czynienia bardziej z alchemią niż fizyką. Mają one pomóc w zrozumieniu bieżącego problemu. Są sprawdzone w działaniu, ale nie nadają się do każdego kontekstu.
Oto kilka rozwiązań zastosowanych w zaprezentowanej w tym podrozdziale implementacji generatora i dyskryminatora GAN. Nie jest to lista wszystkich możliwych rozwiązań pomocni-czych. Jeżeli zainteresował Cię ten temat, to zajrzyj do książek poświęconych sieciom GAN.
Ostatnią warstwą aktywacji modelu jest tanh, a nie wa-stwa sigmoid spotykana w większości innych modeli. Próbkowanie punktów z niejawnej przestrzeni dokonujemy przy użyciu rozkładu normalnego (rozkładu Gaussa), a nie rozkładu jednorodnego. Stochastyczność przyczynia się do uzyskania bardziej solidnego modelu. Trenowanie sieci GAN przyczynia się do powstania dynamicznej równowagi, a więc proces ten może utknąć w wielu punktach. Wprowadzanie losowości do procesu trenowania pomaga temu zapobiec. Losowość wprowadzamy na dwa sposoby: korzystając z mechanizmu odrzucania zaimplementowanego w dyskryminatorze i po-przez dodanie losowego szumu do etykiet przetwarzanych przez dyskryminator. Rzadkie gradienty mogą przeszkodzić w trenowaniu sieci GAN. W uczeniu głębokim rzadki charakter danych jest często czymś wręcz pożądanym, ale nie dotyczy to sieci GAN. Do powstawania rzadkich gradientów mogą przyczynić się dwie rzeczy: operacje maxpooling i aktywacje ReLU. Zamiast operacji maxpooling polecam stosowanie krokowych konwolucji w celu zmniejszenia objętości próbek, a zamiast aktywacji ReLU polecam stosowanie warstwy layer_activation_leaky_relu. Działa ona podobnie do warstwy ReLU, ale nie posiada tak dużych ograniczeń — pozwala na pojawianie się niewielkich ujemnych wartości aktywacji. * W wygenerowanych obrazach często pojawiają się art-fakty wyglądające jak szachownica (patrz rysunek 8.17). Powstają one w wyniku nierównego pokrycia przestrzeni pikseli w generatorze. W celu rozwiązania tego problemu, za każdym razem, gdy będziemy korzystać z kroku layer_conv_2d_transpose lub layer_conv_2d, zastosujemy rozmiar jądra podzielny przez rozmiar kroku (dotyczy to generatora i dyskryminatora).
Generator
Zacznijmy od opracowania modelu generator zamieniającego wektor pochodzący z niejawnej przestrzeni (podczas trenowania będzie on próbkowany losowo) w obraz. Jednym z typowych problemów spotykanych podczas pracy z sieciami GAN jest stałe generowanie obrazów wyglądających jak szum. Można to rozwiązać, stosując technikę odrzucania w implementacjach dyskryminatora i generatora.
r
r r
r library(keras) latent_dim <- 32 height <- 32 width <- 32 channels <- 3 generator_input <- layer_input(shape = c(latent_dim)) generator_output <- generator_input %>%
# First, transform the input into a 16x16 128-channels feature map layer_dense(units = 128 * 16 * 16) %>% layer_activation_leaky_relu() %>% layer_reshape(target_shape = c(16, 16, 128)) %>%
# Then, add a convolution layer layer_conv_2d(filters = 256, kernel_size = 5, padding = ) %>% layer_activation_leaky_relu() %>%
# Upsample to 32x32 layer_conv_2d_transpose(filters = 256, kernel_size = 4, strides = 2, padding = ) %>% layer_activation_leaky_relu() %>%
# Few more conv layers layer_conv_2d(filters = 256, kernel_size = 5, padding = ) %>% layer_activation_leaky_relu() %>% layer_conv_2d(filters = 256, kernel_size = 5, padding = ) %>% layer_activation_leaky_relu() %>%
# Produce a 32x32 1-channel feature map layer_conv_2d(filters = channels, kernel_size = 7, activation = , padding = ) generator <- keras_model(generator_input, generator_output) summary(generator)
_________________________________________________________________________________________________________
Layer (type) Output Shape Param #
=========================================================================================================
input_5 (InputLayer) (None, 32) 0
_________________________________________________________________________________________________________
dense_9 (Dense) (None, 32768) 1081344
_________________________________________________________________________________________________________
leaky_re_lu_6 (LeakyReLU) (None, 32768) 0
_________________________________________________________________________________________________________
reshape_3 (Reshape) (None, 16, 16, 128) 0
_________________________________________________________________________________________________________
conv2d_14 (Conv2D) (None, 16, 16, 256) 819456
_________________________________________________________________________________________________________
leaky_re_lu_7 (LeakyReLU) (None, 16, 16, 256) 0
_________________________________________________________________________________________________________
conv2d_transpose_3 (Conv2DTranspose) (None, 32, 32, 256) 1048832
_________________________________________________________________________________________________________
leaky_re_lu_8 (LeakyReLU) (None, 32, 32, 256) 0
_________________________________________________________________________________________________________
conv2d_15 (Conv2D) (None, 32, 32, 256) 1638656
_________________________________________________________________________________________________________
leaky_re_lu_9 (LeakyReLU) (None, 32, 32, 256) 0
_________________________________________________________________________________________________________
conv2d_16 (Conv2D) (None, 32, 32, 256) 1638656
_________________________________________________________________________________________________________
leaky_re_lu_10 (LeakyReLU) (None, 32, 32, 256) 0
_________________________________________________________________________________________________________
conv2d_17 (Conv2D) (None, 32, 32, 3) 37635
=========================================================================================================
Total params: 6,264,579
Trainable params: 6,264,579
Non-trainable params: 0
_________________________________________________________________________________________________________
Dyskryminator
Teraz możemy przystąpić do pracy nad modelem discriminator, który przyjmuje na swoim wejściu obraz (prawdziwy lub sztuczny) i klasyfikuje go do jednej z dwóch klas: „obraz wygenerowany” lub „obraz pochodzący z treningowego zbioru danych”.
r
r r
r discriminator_input <- layer_input(shape = c(height, width, channels)) discriminator_output <- discriminator_input %>% layer_conv_2d(filters = 128, kernel_size = 3) %>% layer_activation_leaky_relu() %>% layer_conv_2d(filters = 128, kernel_size = 4, strides = 2) %>% layer_activation_leaky_relu() %>% layer_conv_2d(filters = 128, kernel_size = 4, strides = 2) %>% layer_activation_leaky_relu() %>% layer_conv_2d(filters = 128, kernel_size = 4, strides = 2) %>% layer_activation_leaky_relu() %>% layer_flatten() %>% # One dropout layer - important trick! layer_dropout(rate = 0.4) %>%
# Classification layer layer_dense(units = 1, activation = ) discriminator <- keras_model(discriminator_input, discriminator_output) summary(discriminator)
_________________________________________________________________________________________________________
Layer (type) Output Shape Param #
=========================================================================================================
input_6 (InputLayer) (None, 32, 32, 3) 0
_________________________________________________________________________________________________________
conv2d_18 (Conv2D) (None, 30, 30, 128) 3584
_________________________________________________________________________________________________________
leaky_re_lu_11 (LeakyReLU) (None, 30, 30, 128) 0
_________________________________________________________________________________________________________
conv2d_19 (Conv2D) (None, 14, 14, 128) 262272
_________________________________________________________________________________________________________
leaky_re_lu_12 (LeakyReLU) (None, 14, 14, 128) 0
_________________________________________________________________________________________________________
conv2d_20 (Conv2D) (None, 6, 6, 128) 262272
_________________________________________________________________________________________________________
leaky_re_lu_13 (LeakyReLU) (None, 6, 6, 128) 0
_________________________________________________________________________________________________________
conv2d_21 (Conv2D) (None, 2, 2, 128) 262272
_________________________________________________________________________________________________________
leaky_re_lu_14 (LeakyReLU) (None, 2, 2, 128) 0
_________________________________________________________________________________________________________
flatten_3 (Flatten) (None, 512) 0
_________________________________________________________________________________________________________
dropout_1 (Dropout) (None, 512) 0
_________________________________________________________________________________________________________
dense_10 (Dense) (None, 1) 513
=========================================================================================================
Total params: 790,913
Trainable params: 790,913
Non-trainable params: 0
_________________________________________________________________________________________________________
r
r r
r # To stabilize training, we use learning rate decay # and gradient clipping (by value) in the optimizer. discriminator_optimizer <- optimizer_rmsprop( lr = 0.0008, clipvalue = 1.0, decay = 1e-8 ) discriminator %>% compile( optimizer = discriminator_optimizer, loss = _crossentropy
)
Sieć z przeciwnikiem
Teraz czas skonfigurować sieć GAN, która łączy generator z dyskryminatorem. Po wytrenowaniu model ten pchnie gene-rator w kierunku usprawniającym oszukiwanie dyskryminatora. Model ten zamienia punkty niejawnej przestrzeni w etykiety klasyfikacji: „prawdziwy” lub „sztuczny” i ma być trenowany na etykietach zawsze wskazujących prawdziwość obrazu. W związku z tym trenowanie modelu gan doprowadzi do modyfikacji wartości wag generatora tak, aby zwiększyć prawdopodobieństwo orzeczenia przez dyskryminator analizujący sztuczne obrazy tego, że są one prawdziwe. Podczas trenowania dyskryminator powinien być zamrożony (nie należy go trenować) — w czasie trenowania modelu gam wagi dyskryminatora nie będą modyfikowane. Gdyby wagi dyskryminatora były modyfikowane podczas tego procesu, to trenowalibyśmy dyskryminator tak, aby zawsze przewidywał prawdziwość obrazu, a przecież nie tego chcemy!
r
r r
r # Set discriminator weights to non-trainable # (will only apply to the gan
model) freeze_weights(discriminator) gan_input <- layer_input(shape = c(latent_dim)) gan_output <- discriminator(generator(gan_input)) gan <- keras_model(gan_input, gan_output) gan_optimizer <- optimizer_rmsprop( lr = 0.0004, clipvalue = 1.0, decay = 1e-8 ) gan %>% compile( optimizer = gan_optimizer, loss = _crossentropy
)
Trenowanie sieci DCGAN
Teraz możemy rozpocząć proces trenowania. Oto lista czynności wykonywanych podczas każdej epoki trenowania (tak właśnie powinna działać pętla trenująca model):
- Wybierz losowe punkty z niejawnej przestrzeni (losowy szum).
- Użyj generatora w celu wygenerowania obrazów zawiera-jących losowy szum.
- Połącz wygenerowane obrazy z prawdziwymi.
- Wytrenuj dyskryminator przy użyciu wylosowanych obrazów z etykietami określającymi prawdziwość obrazów.
- Wybierz kolejne losowe punkty z niejawnej przestrzeni.
- Trenuj model gan w tym celu, aby wszystkie obrazy były uznawane przez dyskryminator za prawdziwe. W tym procesie zmodyfikowane zostaną wagi generatora (podczas trenowania modelu gan wagi dyskryminatora są zamrożone), tak aby zwiększyć prawdopodobieństwo tego, że wygenerowane obrazy zostaną uznane przez dyskryminator za prawdziwe — generator jest trenowany tak, żeby był w stanie oszukać dyskryminator.
Czas zaimplementować ten mechanizm.
r
r
# Loads CIFAR10 data
cifar10 <- dataset_cifar10()
c(c(x_train, y_train), c(x_test, y_test)) %<-% cifar10
# Selects frog images (class 6)
x_train <- x_train[as.integer(y_train) == 6,,,]
# Normalizes data
x_train <- x_train / 255
iterations <- 10000
batch_size <- 20
save_dir <- \gan_images\
dir.create(save_dir)
# Start the training loop
start <- 1
for (step in 1:iterations) {
# Samples random points in the latent space
random_latent_vectors <- matrix(rnorm(batch_size * latent_dim),
nrow = batch_size, ncol = latent_dim)
# Decodes them to fake images
generated_images <- generator %>% predict(random_latent_vectors)
# Combines them with real images
stop <- start + batch_size - 1
real_images <- x_train[start:stop,,,]
rows <- nrow(real_images)
combined_images <- array(0, dim = c(rows * 2, dim(real_images)[-1]))
combined_images[1:rows,,,] <- generated_images
combined_images[(rows+1):(rows*2),,,] <- real_images
# Assembles labels discriminating real from fake images
labels <- rbind(matrix(1, nrow = batch_size, ncol = 1),
matrix(0, nrow = batch_size, ncol = 1))
# Adds random noise to the labels -- an important trick!
labels <- labels + (0.5 * array(runif(prod(dim(labels))),
dim = dim(labels)))
# Trains the discriminator
d_loss <- discriminator %>% train_on_batch(combined_images, labels)
# Samples random points in the latent space
random_latent_vectors <- matrix(rnorm(batch_size * latent_dim),
nrow = batch_size, ncol = latent_dim)
# Assembles labels that say \all real images\
misleading_targets <- array(0, dim = c(batch_size, 1))
# Trains the generator (via the gan model, where the
# discriminator weights are frozen)
a_loss <- gan %>% train_on_batch(
random_latent_vectors,
misleading_targets
)
start <- start + batch_size
if (start > (nrow(x_train) - batch_size))
start <- 1
# Occasionally saves images
if (step %% 100 == 0) {
# Saves model weights
save_model_weights_hdf5(gan, \gan.h5\)
# Prints metrics
cat(\discriminator loss:\, d_loss, \\n\)
cat(\adversarial loss:\, a_loss, \\n\)
# Saves one generated image
image_array_save(
generated_images[1,,,] * 255,
path = file.path(save_dir, paste0(\generated_frog\, step, \.png\))
)
# Saves one real image for comparison
image_array_save(
real_images[1,,,] * 255,
path = file.path(save_dir, paste0(\real_frog\, step, \.png\))
)
}
}
LS0tDQp0aXRsZTogIldwcm93YWR6ZW5pZSBkbyBnZW5lcmF0eXdueWNoIHNpZWNpIHogcHJ6ZWNpd25pa2llbSINCm91dHB1dDogDQogIGh0bWxfbm90ZWJvb2s6IA0KICAgIHRoZW1lOiBjZXJ1bGVhbg0KICAgIGhpZ2hsaWdodDogdGV4dG1hdGUNCi0tLQ0KDQpgYGB7ciBzZXR1cCwgaW5jbHVkZT1GQUxTRX0NCmtuaXRyOjpvcHRzX2NodW5rJHNldCh3YXJuaW5nID0gRkFMU0UsIG1lc3NhZ2UgPSBGQUxTRSkNCmBgYA0KDQoNCiMjIFNjaGVtYXR5Y3puYSBpbXBsZW1lbnRhY2phIHNpZWNpIEdBTg0KDQoNClcgdGVqIHNla2NqaSB3eWphxZtuacSZIGltcGxlbWVudGFjasSZIG5hanByb3N0c3plaiBmb3JteSBzaWVjaSBHQU4gdyBwYWtpZWNpZSBLZXJhcy4gU2llY2kgR0FOIHPEhSB6YWF3YW5zb3dhbmUsIGEgd2nEmWMgemFnxYLEmWJpYW5pZSBzacSZIHcgdGVjaG5pY3puZSBzemN6ZWfDs8WCeSB3eWtyYWN6YcWCb2J5IHBvemEgemFrcmVzIHRlbWF0eWN6bnkgdGVqIGtzacSFxbxraS4gWmFwcmV6ZW50dWrEmSBpbXBsZW1lbnRhY2rEmSBnxYLEmWJva2llaiBrb253b2x1Y3lqbmVqIHNpZWNpIEdBTiAoRENHQU4pIOKAlCBzaWVjaSBHQU4sIHcga3TDs3JlaiBnZW5lcmF0b3IgaSBkeXNrcnltaW5hdG9yIHPEhSBnxYLEmWJva2ltaSBzaWVjaWFtaSBrb253b2x1Y3lqbnltaS4gVyBwcmFrdHljZSB6YXN0b3N1asSZIHdhcnN0d8SZIGxheS1lcl9jb252XzJkX3RyYW5zcG9zZSB3IGNlbHUgendpxJlrc3plbmlhIHByw7Nia293YW5pYSB3IGdlbmVyYXRvcnplLg0KDQpTaWXEhyBHQU4gYsSZZHppZSB0cmVub3dhbmEgbmEgemJpb3J6ZSBDSUZBUjEwIHNrxYJhZGFqxIVjeW0gc2nEmSB6IDUwIDAwMCBrb2xvcm93eWNoIG9icmF6w7N3IG8gcm96ZHppZWxjem/Fm2NpIDMy74K0MzIsIHBvZHppZWxvbnljaCBuYSAxMCByw7N3bnljaCBrbGFzICh3IGthxbxkZWogeiBrbGFzIHpuYWpkdWplIHNpxJkgNTAwMCBvYnJhesOzdykuIERsYSB1xYJhdHdpZW5pYSBixJlkemllbXkga29yenlzdGHEhyB0eWxrbyB6IG9icmF6w7N3IG5hbGXFvMSFY3ljaCBkbyBrbGFzeSDigJ7FvGFiYeKAnS4NCg0KU2NoZW1hdHljem5pZSBkemlhxYJhbmllIHRlaiBzaWVjaSBHQU4gbW/FvG5hIHByemVkc3Rhd2nEhyB3IG5hc3TEmXB1asSFY3kgc3Bvc8OzYjoNCg0KKiAxLglTaWXEhyBnZW5lcmF0b3JhIChnZW5lcmF0b3IpIG1hcHVqZSB3ZWt0b3J5IG8ga3N6dGHFgmNpZSAobGF0ZW50X2RpbSkgbmEgb2JyYXp5IG8ga3N6dGHFgmNpZSAoMzIsIDMyLCAzKS4NCiogMi4JU2llxIcgZHlza3J5bWluYXRvcmEgKGRpc2NyaW1pbmF0b3IpIG1hcHVqZSBvYnJhenkgbyBrc3p0YcWCY2llICgzMiwgMzIsIDMpIG5hIGJpbmFybsSFIHdhcnRvxZvEhyBva3JlxZtsYWrEhWPEhSBwcmF3ZG9wb2RvYmllxYRzdHdvIHRlZ28sIMW8ZSBvYnJheiBqZXN0IHByYXdkeml3eS4NCiogMy4JU2llxIcgZ2FuIHR3b3J6eSDFgmHFhGN1Y2ggc2vFgmFkYWrEhWN5IHNpxJkgeiBnZW5lcmF0b3JhIGkgZHlza3J5bWluYXRvcmE6IGdhbih4KSA8LSBkaXNjcmltaW5hdG9yKGdlbmVyYXRvcih4KSkuIFNpZcSHIGdhbiBtYXB1amUgd2VrdG9yeSBuaWVqYXduZWogcHJ6ZXN0cnplbmkgbmEgb2NlbnkgcmVhbGl6bXUgd3lzdGF3aWFuZSBwcnpleiBkeXNrcnltaW5hdG9yLg0KKiA0LglUcmVudWplbXkgZHlza3J5bWluYXRvciBwcnp5IHXFvHljaXUgcHJ6eWvFgmFkw7N3IHByYXdkeml3eWNoIGkgc3p0dWN6bnljaCBvYnJhesOzdyBvem5hY3pvbnljaCBldHlraWV0YW1pLCB0YWsgamFrYnnFm215IHRyZW5vd2FsaSB6d3lrxYJ5IG1vZGVsIGtsYXN5ZmlrYWNqaSBvYnJhesOzdy4NCiogNS4JVyBjZWx1IHd5dHJlbm93YW5pYSBnZW5lcmF0b3JhIGtvcnp5c3RhbXkgeiBncmFkaWVuLXTDs3cgd2FnIGdlbmVyYXRvcmEgdyBvZG5pZXNpZW5pdSBkbyBzdHJhdHkgbW9kZWx1IGdhbi4gVyB6d2nEhXprdSB6IHR5bSBrYcW8ZHkga3JvayB0cmVub3dhbmlhIG1hIG1vZHlmaWtvd2HEhyB3YWdpIGdlbmVyYXRvcmEgdGFrLCBhYnkgendpxJlrc3p5xIcgcHJhd2RvcG9kb2JpZcWEc3R3byB6YWtsYXN5Zmlrb3dhbmlhIHd5Z2VuZXJvd2FueWNoIG9icmF6w7N3IGpha28gcHJhd2R6aS13eWNoLiBJbm55bWkgc8WCb3d5LCB0cmVudWplbXkgZ2VuZXJhdG9yIHRhaywgYWJ5IGJ5xYIgdyBzdGFuaWUgb3N6dWthxIcgZHlza3J5bWluYXRvci4NCg0KDQojIyBaYmnDs3IgcHJ6eWRhdG55Y2ggcm96d2nEhXphxYQNCg0KUHJvY2VzIHRyZW5vd2FuaWEgc2llY2kgR0FOIGkgZG9zdHJhamFuaWEgaWNoIGltcGxlbWVudGFjamkgamVzdCBiYXJkem8gdHJ1ZG55LiBXIHp3acSFemt1IHogdHltIHdhcnRvIHBvem5hxIcgcm96d2nEhXphbmlhIHXFgmF0d2lhasSFY2UgcHJhY8SZIHogdHltaSBzaWVjaWFtaS4gUm96d2nEhXphbmlhIHRlIG1hasSFIG5hdHVyxJkgaGV1cnlzdHljem7EhSBpIG5pZSBzxIUgdGVvcmV0eWN6bnltaSB3c2thesOzd2thbWkg4oCUIHBvZG9ibmllIGphayB6IHdpxJlrc3pvxZtjacSFIHphZ2FkbmllxYQgdWN6ZW5pYSBnxYLEmWJva2llZ28sIG1hbXkgZG8gY3p5bmllbmlhIGJhcmR6aWVqIHogYWxjaGVtacSFIG5pxbwgZml6eWvEhS4gTWFqxIUgb25lIHBvbcOzYyB3IHpyb3p1bWllbml1IGJpZcW8xIVjZWdvIHByb2JsZW11LiBTxIUgc3ByYXdkem9uZSB3IGR6aWHFgmFuaXUsIGFsZSBuaWUgbmFkYWrEhSBzacSZIGRvIGthxbxkZWdvIGtvbnRla3N0dS4NCg0KT3RvIGtpbGthIHJvendpxIV6YcWEIHphc3Rvc293YW55Y2ggdyB6YXByZXplbnRvd2FuZWogdyB0eW0gcG9kcm96ZHppYWxlIGltcGxlbWVudGFjamkgZ2VuZXJhdG9yYSBpIGR5c2tyeW1pbmF0b3JhIEdBTi4gTmllIGplc3QgdG8gbGlzdGEgd3N6eXN0a2ljaCBtb8W8bGl3eWNoIHJvendpxIV6YcWEIHBvbW9jbmktY3p5Y2guIEplxbxlbGkgemFpbnRlcmVzb3dhxYIgQ2nEmSB0ZW4gdGVtYXQsIHRvIHphanJ6eWogZG8ga3NpxIXFvGVrIHBvxZt3acSZY29ueWNoIHNpZWNpb20gR0FOLg0KDQoNCirvga4JT3N0YXRuacSFIHdhcnN0d8SFIGFrdHl3YWNqaSBtb2RlbHUgamVzdCB0YW5oLCBhIG5pZSB3YS1zdHdhIHNpZ21vaWQgc3BvdHlrYW5hIHcgd2nEmWtzem/Fm2NpIGlubnljaCBtb2RlbGkuDQoq74GuCVByw7Nia293YW5pZSBwdW5rdMOzdyB6IG5pZWphd25laiBwcnplc3RyemVuaSBkb2tvbnVqZW15IHByenkgdcW8eWNpdSByb3prxYJhZHUgbm9ybWFsbmVnbyAocm96a8WCYWR1IEdhdXNzYSksIGEgbmllIHJvemvFgmFkdSBqZWRub3JvZG5lZ28uDQoq74GuCVN0b2NoYXN0eWN6bm/Fm8SHIHByenljenluaWEgc2nEmSBkbyB1enlza2FuaWEgYmFyZHppZWogc29saWRuZWdvIG1vZGVsdS4gVHJlbm93YW5pZSBzaWVjaSBHQU4gcHJ6eWN6eW5pYSBzacSZIGRvIHBvd3N0YW5pYSBkeW5hbWljem5laiByw7N3bm93YWdpLCBhIHdpxJljIHByb2NlcyB0ZW4gbW/FvGUgdXRrbsSFxIcgdyB3aWVsdSBwdW5rdGFjaC4gV3Byb3dhZHphbmllIGxvc293b8WbY2kgZG8gcHJvY2VzdSB0cmVub3dhbmlhIHBvbWFnYSB0ZW11IHphcG9iaWVjLiBMb3Nvd2/Fm8SHIHdwcm93YWR6YW15IG5hIGR3YSBzcG9zb2J5OiBrb3J6eXN0YWrEhWMgeiBtZWNoYW5pem11IG9kcnp1Y2FuaWEgemFpbXBsZW1lbnRvd2FuZWdvIHcgZHlza3J5bWluYXRvcnplIGkgcG8tcHJ6ZXogZG9kYW5pZSBsb3Nvd2VnbyBzenVtdSBkbyBldHlraWV0IHByemV0d2FyemFueWNoIHByemV6IGR5c2tyeW1pbmF0b3IuDQoq74GuCVJ6YWRraWUgZ3JhZGllbnR5IG1vZ8SFIHByemVzemtvZHppxIcgdyB0cmVub3dhbml1IHNpZWNpIEdBTi4gVyB1Y3plbml1IGfFgsSZYm9raW0gcnphZGtpIGNoYXJha3RlciBkYW55Y2ggamVzdCBjesSZc3RvIGN6eW3FmyB3csSZY3ogcG/FvMSFZGFueW0sIGFsZSBuaWUgZG90eWN6eSB0byBzaWVjaSBHQU4uIERvIHBvd3N0YXdhbmlhIHJ6YWRraWNoIGdyYWRpZW50w7N3IG1vZ8SFIHByenljenluacSHIHNpxJkgZHdpZSByemVjenk6IG9wZXJhY2plIG1heHBvb2xpbmcgaSBha3R5d2FjamUgUmVMVS4gWmFtaWFzdCBvcGVyYWNqaSBtYXhwb29saW5nIHBvbGVjYW0gc3Rvc293YW5pZSBrcm9rb3d5Y2gga29ud29sdWNqaSB3IGNlbHUgem1uaWVqc3plbmlhIG9iasSZdG/Fm2NpIHByw7NiZWssIGEgemFtaWFzdCBha3R5d2FjamkgUmVMVSBwb2xlY2FtIHN0b3Nvd2FuaWUgd2Fyc3R3eSBsYXllcl9hY3RpdmF0aW9uX2xlYWt5X3JlbHUuIER6aWHFgmEgb25hIHBvZG9ibmllIGRvIHdhcnN0d3kgUmVMVSwgYWxlIG5pZSBwb3NpYWRhIHRhayBkdcW8eWNoIG9ncmFuaWN6ZcWEIOKAlCBwb3p3YWxhIG5hIHBvamF3aWFuaWUgc2nEmSBuaWV3aWVsa2ljaCB1amVtbnljaCB3YXJ0b8WbY2kgYWt0eXdhY2ppLg0KKu+BrglXIHd5Z2VuZXJvd2FueWNoIG9icmF6YWNoIGN6xJlzdG8gcG9qYXdpYWrEhSBzacSZIGFydC1mYWt0eSB3eWdsxIVkYWrEhWNlIGphayBzemFjaG93bmljYSAocGF0cnogcnlzdW5layA4LjE3KS4gUG93c3RhasSFIG9uZSB3IHd5bmlrdSBuaWVyw7N3bmVnbyBwb2tyeWNpYSBwcnplc3RyemVuaSBwaWtzZWxpIHcgZ2VuZXJhdG9yemUuIFcgY2VsdSByb3p3acSFemFuaWEgdGVnbyBwcm9ibGVtdSwgemEga2HFvGR5bSByYXplbSwgZ2R5IGLEmWR6aWVteSBrb3J6eXN0YcSHIHoga3Jva3UgbGF5ZXJfY29udl8yZF90cmFuc3Bvc2UgbHViIGxheWVyX2NvbnZfMmQsIHphc3Rvc3VqZW15IHJvem1pYXIgasSFZHJhIHBvZHppZWxueSBwcnpleiByb3ptaWFyIGtyb2t1IChkb3R5Y3p5IHRvIGdlbmVyYXRvcmEgaSBkeXNrcnltaW5hdG9yYSkuDQoNCg0KIyMgR2VuZXJhdG9yDQoNClphY3puaWpteSBvZCBvcHJhY293YW5pYSBtb2RlbHUgZ2VuZXJhdG9yIHphbWllbmlhasSFY2VnbyB3ZWt0b3IgcG9jaG9kesSFY3kgeiBuaWVqYXduZWogcHJ6ZXN0cnplbmkgKHBvZGN6YXMgdHJlbm93YW5pYSBixJlkemllIG9uIHByw7Nia293YW55IGxvc293bykgdyBvYnJhei4gSmVkbnltIHogdHlwb3d5Y2ggcHJvYmxlbcOzdyBzcG90eWthbnljaCBwb2RjemFzIHByYWN5IHogc2llY2lhbWkgR0FOIGplc3Qgc3RhxYJlIGdlbmVyb3dhbmllIG9icmF6w7N3IHd5Z2zEhWRhasSFY3ljaCBqYWsgc3p1bS4gTW/FvG5hIHRvIHJvendpxIV6YcSHLCBzdG9zdWrEhWMgdGVjaG5pa8SZIG9kcnp1Y2FuaWEgdyBpbXBsZW1lbnRhY2phY2ggZHlza3J5bWluYXRvcmEgaSBnZW5lcmF0b3JhLg0KDQpgYGB7cn0NCmxpYnJhcnkoa2VyYXMpDQoNCmxhdGVudF9kaW0gPC0gMzINCmhlaWdodCA8LSAzMg0Kd2lkdGggPC0gMzINCmNoYW5uZWxzIDwtIDMNCg0KZ2VuZXJhdG9yX2lucHV0IDwtIGxheWVyX2lucHV0KHNoYXBlID0gYyhsYXRlbnRfZGltKSkNCg0KZ2VuZXJhdG9yX291dHB1dCA8LSBnZW5lcmF0b3JfaW5wdXQgJT4lIA0KICANCiAgIyBaYW1pYW5hIG9iaWVrdHUgd2VqxZtjaW93ZWdvIHcgMTI4LWthbmHFgm93xIUgbWFwxJkgY2VjaCBvIHd5bWlhcmFjaCAxNu+CtDE2Lg0KICBsYXllcl9kZW5zZSh1bml0cyA9IDEyOCAqIDE2ICogMTYpICU+JQ0KICBsYXllcl9hY3RpdmF0aW9uX2xlYWt5X3JlbHUoKSAlPiUgDQogIGxheWVyX3Jlc2hhcGUodGFyZ2V0X3NoYXBlID0gYygxNiwgMTYsIDEyOCkpICU+JSANCiAgDQogICMgV2Fyc3R3YSBrb253b2x1Y3lqbmEuDQogIGxheWVyX2NvbnZfMmQoZmlsdGVycyA9IDI1Niwga2VybmVsX3NpemUgPSA1LCANCiAgICAgICAgICAgICAgICBwYWRkaW5nID0gInNhbWUiKSAlPiUgDQogIGxheWVyX2FjdGl2YXRpb25fbGVha3lfcmVsdSgpICU+JSANCiAgDQogICMgWndpxJlrc3plbmllIHJvem1pYXJ1IGRvIDMy74K0MzIuDQogIGxheWVyX2NvbnZfMmRfdHJhbnNwb3NlKGZpbHRlcnMgPSAyNTYsIGtlcm5lbF9zaXplID0gNCwgDQogICAgICAgICAgICAgICAgICAgICAgICAgIHN0cmlkZXMgPSAyLCBwYWRkaW5nID0gInNhbWUiKSAlPiUgDQogIGxheWVyX2FjdGl2YXRpb25fbGVha3lfcmVsdSgpICU+JSANCiAgDQogICMgS29sZWpuZSB3YXJzdHd5IGtvbndvbHVjeWpuZS4NCiAgbGF5ZXJfY29udl8yZChmaWx0ZXJzID0gMjU2LCBrZXJuZWxfc2l6ZSA9IDUsIA0KICAgICAgICAgICAgICAgIHBhZGRpbmcgPSAic2FtZSIpICU+JSANCiAgbGF5ZXJfYWN0aXZhdGlvbl9sZWFreV9yZWx1KCkgJT4lIA0KICBsYXllcl9jb252XzJkKGZpbHRlcnMgPSAyNTYsIGtlcm5lbF9zaXplID0gNSwgDQogICAgICAgICAgICAgICAgcGFkZGluZyA9ICJzYW1lIikgJT4lIA0KICBsYXllcl9hY3RpdmF0aW9uX2xlYWt5X3JlbHUoKSAlPiUgDQogIA0KICAjIEdlbmVydWplIGplZG5va2FuYcWCb3fEhSBtYXDEmSBjZWNoIG8gcm96bWlhcnplIDMy74K0MzIgKHJvem1pYXIgdGVuIGplc3QgdGFraSBzYW0gamFrIHJvem1pYXIgb2JyYXrDs3cgd2Nob2R6xIVjeWNoIHcgc2vFgmFkIHpiaW9ydSBDSUZBUjEwKS4NCiAgbGF5ZXJfY29udl8yZChmaWx0ZXJzID0gY2hhbm5lbHMsIGtlcm5lbF9zaXplID0gNywNCiAgICAgICAgICAgICAgICBhY3RpdmF0aW9uID0gInRhbmgiLCBwYWRkaW5nID0gInNhbWUiKQ0KDQpnZW5lcmF0b3IgPC0ga2VyYXNfbW9kZWwoZ2VuZXJhdG9yX2lucHV0LCBnZW5lcmF0b3Jfb3V0cHV0KQ0Kc3VtbWFyeShnZW5lcmF0b3IpDQpgYGANCg0KIyMgRHlza3J5bWluYXRvcg0KDQoNClRlcmF6IG1vxbxlbXkgcHJ6eXN0xIVwacSHIGRvIHByYWN5IG5hZCBtb2RlbGVtIGRpc2NyaW1pbmF0b3IsIGt0w7NyeSBwcnp5am11amUgbmEgc3dvaW0gd2VqxZtjaXUgb2JyYXogKHByYXdkeml3eSBsdWIgc3p0dWN6bnkpIGkga2xhc3lmaWt1amUgZ28gZG8gamVkbmVqIHogZHfDs2NoIGtsYXM6IOKAnm9icmF6IHd5Z2VuZXJvd2FueeKAnSBsdWIg4oCeb2JyYXogcG9jaG9kesSFY3kgeiB0cmVuaW5nb3dlZ28gemJpb3J1IGRhbnljaOKAnS4NCg0KYGBge3J9DQpkaXNjcmltaW5hdG9yX2lucHV0IDwtIGxheWVyX2lucHV0KHNoYXBlID0gYyhoZWlnaHQsIHdpZHRoLCBjaGFubmVscykpDQoNCmRpc2NyaW1pbmF0b3Jfb3V0cHV0IDwtIGRpc2NyaW1pbmF0b3JfaW5wdXQgJT4lIA0KICBsYXllcl9jb252XzJkKGZpbHRlcnMgPSAxMjgsIGtlcm5lbF9zaXplID0gMykgJT4lIA0KICBsYXllcl9hY3RpdmF0aW9uX2xlYWt5X3JlbHUoKSAlPiUgDQogIGxheWVyX2NvbnZfMmQoZmlsdGVycyA9IDEyOCwga2VybmVsX3NpemUgPSA0LCBzdHJpZGVzID0gMikgJT4lIA0KICBsYXllcl9hY3RpdmF0aW9uX2xlYWt5X3JlbHUoKSAlPiUgDQogIGxheWVyX2NvbnZfMmQoZmlsdGVycyA9IDEyOCwga2VybmVsX3NpemUgPSA0LCBzdHJpZGVzID0gMikgJT4lIA0KICBsYXllcl9hY3RpdmF0aW9uX2xlYWt5X3JlbHUoKSAlPiUgDQogIGxheWVyX2NvbnZfMmQoZmlsdGVycyA9IDEyOCwga2VybmVsX3NpemUgPSA0LCBzdHJpZGVzID0gMikgJT4lIA0KICBsYXllcl9hY3RpdmF0aW9uX2xlYWt5X3JlbHUoKSAlPiUgDQogIGxheWVyX2ZsYXR0ZW4oKSAlPiUNCiAgIyBXYXJzdHdhIG9kcnp1Y2FuaWEuIFRvIGJhcmR6byB3YcW8bmUgcm96d2nEhXphbmllLg0KICBsYXllcl9kcm9wb3V0KHJhdGUgPSAwLjQpICU+JSAgDQogICMgV2Fyc3R3YSBrbGFzeWZpa2FjamkuDQogIGxheWVyX2RlbnNlKHVuaXRzID0gMSwgYWN0aXZhdGlvbiA9ICJzaWdtb2lkIikNCg0KZGlzY3JpbWluYXRvciA8LSBrZXJhc19tb2RlbChkaXNjcmltaW5hdG9yX2lucHV0LCBkaXNjcmltaW5hdG9yX291dHB1dCkNCnN1bW1hcnkoZGlzY3JpbWluYXRvcikNCg0KIyBPcHR5bWFsaXphdG9yIGtvcnp5c3RhIHogbWVjaGFuaXptdSB1Y2luYW5pYSB3YXJ0b8WbY2kgZ3JhZGllbnR1Lg0KIyBXIGNlbHUgdXp5c2thbmlhIHN0YWJpbG5lZ28gcHJ6ZWJpZWd1IHByb2Nlc3UgdHJlbm93YW5pYSBrb3J6eXN0YW15IHogcGFyYW1ldHJ1IHJvemvFgmFkdSB3c3DDs8WCY3p5bm5pa2EgdWN6ZW5pYS4NCmRpc2NyaW1pbmF0b3Jfb3B0aW1pemVyIDwtIG9wdGltaXplcl9ybXNwcm9wKCANCiAgbHIgPSAwLjAwMDgsIA0KICBjbGlwdmFsdWUgPSAxLjAsDQogIGRlY2F5ID0gMWUtOA0KKQ0KDQpkaXNjcmltaW5hdG9yICU+JSBjb21waWxlKA0KICBvcHRpbWl6ZXIgPSBkaXNjcmltaW5hdG9yX29wdGltaXplciwNCiAgbG9zcyA9ICJiaW5hcnlfY3Jvc3NlbnRyb3B5Ig0KKQ0KYGBgDQoNCiMjIFNpZcSHIHogcHJ6ZWNpd25pa2llbQ0KDQpUZXJheiBjemFzIHNrb25maWd1cm93YcSHIHNpZcSHIEdBTiwga3TDs3JhIMWCxIVjenkgZ2VuZXJhdG9yIHogZHlza3J5bWluYXRvcmVtLiBQbyB3eXRyZW5vd2FuaXUgbW9kZWwgdGVuIHBjaG5pZSBnZW5lLXJhdG9yIHcga2llcnVua3UgdXNwcmF3bmlhasSFY3ltIG9zenVraXdhbmllIGR5c2tyeW1pbmF0b3JhLiBNb2RlbCB0ZW4gemFtaWVuaWEgcHVua3R5IG5pZWphd25laiBwcnplc3RyemVuaSB3IGV0eWtpZXR5IGtsYXN5ZmlrYWNqaTog4oCecHJhd2R6aXd54oCdIGx1YiDigJ5zenR1Y3pueeKAnSBpIG1hIGJ5xIcgdHJlbm93YW55IG5hIGV0eWtpZXRhY2ggemF3c3plIHdza2F6dWrEhWN5Y2ggcHJhd2R6aXdvxZvEhyBvYnJhenUuIFcgendpxIV6a3UgeiB0eW0gdHJlbm93YW5pZSBtb2RlbHUgZ2FuIGRvcHJvd2FkemkgZG8gbW9keWZpa2Fjamkgd2FydG/Fm2NpIHdhZyBnZW5lcmF0b3JhIHRhaywgYWJ5IHp3acSZa3N6ecSHIHByYXdkb3BvZG9iaWXFhHN0d28gb3J6ZWN6ZW5pYSBwcnpleiBkeXNrcnltaW5hdG9yIGFuYWxpenVqxIVjeSBzenR1Y3puZSBvYnJhenkgdGVnbywgxbxlIHPEhSBvbmUgcHJhd2R6aXdlLiBQb2RjemFzIHRyZW5vd2FuaWEgZHlza3J5bWluYXRvciBwb3dpbmllbiBiecSHIHphbXJvxbxvbnkgKG5pZSBuYWxlxbx5IGdvIHRyZW5vd2HEhykg4oCUIHcgY3phc2llIHRyZW5vd2FuaWEgbW9kZWx1IGdhbSB3YWdpIGR5c2tyeW1pbmF0b3JhIG5pZSBixJlkxIUgbW9keWZpa293YW5lLiBHZHlieSB3YWdpIGR5c2tyeW1pbmF0b3JhIGJ5xYJ5IG1vZHlmaWtvd2FuZSBwb2RjemFzIHRlZ28gcHJvY2VzdSwgdG8gdHJlbm93YWxpYnnFm215IGR5c2tyeW1pbmF0b3IgdGFrLCBhYnkgemF3c3plIHByemV3aWR5d2HFgiBwcmF3ZHppd2/Fm8SHIG9icmF6dSwgYSBwcnplY2llxbwgbmllIHRlZ28gY2hjZW15IQ0KDQpgYGB7cn0NCiMgVW5pZW1vxbxsaXdpYSB0cmVub3dhbmllIHdhZyBkeXNrcnltaW5hdG9yYSANCiMgKHR5bGtvIHcgbW9kZWx1IGdhbikuDQpmcmVlemVfd2VpZ2h0cyhkaXNjcmltaW5hdG9yKSANCg0KZ2FuX2lucHV0IDwtIGxheWVyX2lucHV0KHNoYXBlID0gYyhsYXRlbnRfZGltKSkNCmdhbl9vdXRwdXQgPC0gZGlzY3JpbWluYXRvcihnZW5lcmF0b3IoZ2FuX2lucHV0KSkNCmdhbiA8LSBrZXJhc19tb2RlbChnYW5faW5wdXQsIGdhbl9vdXRwdXQpDQoNCmdhbl9vcHRpbWl6ZXIgPC0gb3B0aW1pemVyX3Jtc3Byb3AoDQogIGxyID0gMC4wMDA0LCANCiAgY2xpcHZhbHVlID0gMS4wLCANCiAgZGVjYXkgPSAxZS04DQopDQoNCmdhbiAlPiUgY29tcGlsZSgNCiAgb3B0aW1pemVyID0gZ2FuX29wdGltaXplciwgDQogIGxvc3MgPSAiYmluYXJ5X2Nyb3NzZW50cm9weSINCikNCmBgYA0KDQojIyBUcmVub3dhbmllIHNpZWNpIERDR0FODQoNClRlcmF6IG1vxbxlbXkgcm96cG9jesSFxIcgcHJvY2VzIHRyZW5vd2FuaWEuIE90byBsaXN0YSBjenlubm/Fm2NpIHd5a29ueXdhbnljaCBwb2RjemFzIGthxbxkZWogZXBva2kgdHJlbm93YW5pYSAodGFrIHfFgmHFm25pZSBwb3dpbm5hIGR6aWHFgmHEhyBwxJl0bGEgdHJlbnVqxIVjYSBtb2RlbCk6DQoNCiogMS4JV3liaWVyeiBsb3Nvd2UgcHVua3R5IHogbmllamF3bmVqIHByemVzdHJ6ZW5pIChsb3Nvd3kgc3p1bSkuDQoqIDIuCVXFvHlqIGdlbmVyYXRvcmEgdyBjZWx1IHd5Z2VuZXJvd2FuaWEgb2JyYXrDs3cgemF3aWVyYS1qxIVjeWNoIGxvc293eSBzenVtLg0KKiAzLglQb8WCxIVjeiB3eWdlbmVyb3dhbmUgb2JyYXp5IHogcHJhd2R6aXd5bWkuDQoqIDQuCVd5dHJlbnVqIGR5c2tyeW1pbmF0b3IgcHJ6eSB1xbx5Y2l1IHd5bG9zb3dhbnljaCBvYnJhesOzdyB6IGV0eWtpZXRhbWkgb2tyZcWbbGFqxIVjeW1pIHByYXdkeml3b8WbxIcgb2JyYXrDs3cuDQoqIDUuCVd5Ymllcnoga29sZWpuZSBsb3Nvd2UgcHVua3R5IHogbmllamF3bmVqIHByemVzdHJ6ZW5pLg0KKiA2LglUcmVudWogbW9kZWwgZ2FuIHcgdHltIGNlbHUsIGFieSB3c3p5c3RraWUgb2JyYXp5IGJ5xYJ5IHV6bmF3YW5lIHByemV6IGR5c2tyeW1pbmF0b3IgemEgcHJhd2R6aXdlLiBXIHR5bSBwcm9jZXNpZSB6bW9keWZpa293YW5lIHpvc3RhbsSFIHdhZ2kgZ2VuZXJhdG9yYSAocG9kY3phcyB0cmVub3dhbmlhIG1vZGVsdSBnYW4gd2FnaSBkeXNrcnltaW5hdG9yYSBzxIUgemFtcm/FvG9uZSksIHRhayBhYnkgendpxJlrc3p5xIcgcHJhd2RvcG9kb2JpZcWEc3R3byB0ZWdvLCDFvGUgd3lnZW5lcm93YW5lIG9icmF6eSB6b3N0YW7EhSB1em5hbmUgcHJ6ZXogZHlza3J5bWluYXRvciB6YSBwcmF3ZHppd2Ug4oCUIGdlbmVyYXRvciBqZXN0IHRyZW5vd2FueSB0YWssIMW8ZWJ5IGJ5xYIgdyBzdGFuaWUgb3N6dWthxIcgZHlza3J5bWluYXRvci4NCg0KDQpDemFzIHphaW1wbGVtZW50b3dhxIcgdGVuIG1lY2hhbml6bS4NCg0KYGBge3IsIGVjaG89VFJVRSwgcmVzdWx0cz0naGlkZSd9DQojIMWBYWRvd2FuaWUgemJpb3J1IGRhbnljaCBDSUZBUjEwLg0KY2lmYXIxMCA8LSBkYXRhc2V0X2NpZmFyMTAoKQ0KYyhjKHhfdHJhaW4sIHlfdHJhaW4pLCBjKHhfdGVzdCwgeV90ZXN0KSkgJTwtJSBjaWZhcjEwDQoNCiMgV3liw7NyIG9icmF6w7N3IMW8YWIgKGtsYXNhIG51bWVyIDYpLg0KeF90cmFpbiA8LSB4X3RyYWluW2FzLmludGVnZXIoeV90cmFpbikgPT0gNiwsLF0gDQojIE5vcm1hbGl6YWNqYSBkYW55Y2guDQp4X3RyYWluIDwtIHhfdHJhaW4gLyAyNTUNCg0KaXRlcmF0aW9ucyA8LSAxMDAwMA0KYmF0Y2hfc2l6ZSA8LSAyMA0Kc2F2ZV9kaXIgPC0gImdhbl9pbWFnZXMiDQpkaXIuY3JlYXRlKHNhdmVfZGlyKQ0KDQojIFBvY3rEhXRlayBwxJl0bGkgdHJlbm93YW5pYS4NCnN0YXJ0IDwtIDENCg0KZm9yIChzdGVwIGluIDE6aXRlcmF0aW9ucykgew0KICANCiAgIyBQcsOzYmtvd2FuaWUgbG9zb3d5Y2ggcHVua3TDs3cgeiBuaWVqYXduZWogcHJ6ZXN0cnplbmkuDQogIHJhbmRvbV9sYXRlbnRfdmVjdG9ycyA8LSBtYXRyaXgocm5vcm0oYmF0Y2hfc2l6ZSAqIGxhdGVudF9kaW0pLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBucm93ID0gYmF0Y2hfc2l6ZSwgbmNvbCA9IGxhdGVudF9kaW0pDQogIA0KICAjIERla29kb3dhbmllIHB1bmt0w7N3IHcgY2VsdSB3eWdlbmVyb3dhbmlhIHN6dHVjem55Y2ggb2JyYXrDs3cuDQogIGdlbmVyYXRlZF9pbWFnZXMgPC0gZ2VuZXJhdG9yICU+JSBwcmVkaWN0KHJhbmRvbV9sYXRlbnRfdmVjdG9ycykNCiAgDQogICMgxYHEhWN6ZW5pZSBvYnJhesOzdyBzenR1Y3pueWNoIHogcHJhd2R6aXd5bWkuDQogIHN0b3AgPC0gc3RhcnQgKyBiYXRjaF9zaXplIC0gMSANCiAgcmVhbF9pbWFnZXMgPC0geF90cmFpbltzdGFydDpzdG9wLCwsXQ0KICByb3dzIDwtIG5yb3cocmVhbF9pbWFnZXMpDQogIGNvbWJpbmVkX2ltYWdlcyA8LSBhcnJheSgwLCBkaW0gPSBjKHJvd3MgKiAyLCBkaW0ocmVhbF9pbWFnZXMpWy0xXSkpDQogIGNvbWJpbmVkX2ltYWdlc1sxOnJvd3MsLCxdIDwtIGdlbmVyYXRlZF9pbWFnZXMNCiAgY29tYmluZWRfaW1hZ2VzWyhyb3dzKzEpOihyb3dzKjIpLCwsXSA8LSByZWFsX2ltYWdlcw0KIA0KICAjIFR3b3J6ZW5pZSBldHlraWV0IHVtb8W8bGl3aWFqxIVjeWNoIG9kcsOzxbxuaWVuaWUgb2JyYXrDs3cgcHJhd2R6aXd5Y2ggb2Qgc3p0dWN6bnljaC4NCiAgbGFiZWxzIDwtIHJiaW5kKG1hdHJpeCgxLCBucm93ID0gYmF0Y2hfc2l6ZSwgbmNvbCA9IDEpLA0KICAgICAgICAgICAgICAgICAgbWF0cml4KDAsIG5yb3cgPSBiYXRjaF9zaXplLCBuY29sID0gMSkpDQogIA0KICAjIFdhxbxueSB6YWJpZWc6IHdwcm93YWR6YW5pZSBsb3Nvd2VnbyBzenVtdSBkbyBldHlraWV0Lg0KICBsYWJlbHMgPC0gbGFiZWxzICsgKDAuNSAqIGFycmF5KHJ1bmlmKHByb2QoZGltKGxhYmVscykpKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkaW0gPSBkaW0obGFiZWxzKSkpDQogIA0KICAjIFRyZW5vd2FuaWUgZHlza3J5bWluYXRvcmEuDQogIGRfbG9zcyA8LSBkaXNjcmltaW5hdG9yICU+JSB0cmFpbl9vbl9iYXRjaChjb21iaW5lZF9pbWFnZXMsIGxhYmVscykgDQogIA0KICAjIExvc293ZSBwcsOzYmtvd2FuaWUgcHVua3TDs3cgdyBuaWVqYXduZWogcHJ6ZXN0cnplbmkuDQogIHJhbmRvbV9sYXRlbnRfdmVjdG9ycyA8LSBtYXRyaXgocm5vcm0oYmF0Y2hfc2l6ZSAqIGxhdGVudF9kaW0pLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBucm93ID0gYmF0Y2hfc2l6ZSwgbmNvbCA9IGxhdGVudF9kaW0pDQogIA0KICAjIFR3b3J6ZW5pZSBmYcWCc3p5d3ljaCBldHlraWV0IHN0d2llcmR6YWrEhWN5Y2ggb3J5Z2luYWxub8WbxIcgd3N6eXN0a2ljaCBvYnJhesOzdy4NCiAgbWlzbGVhZGluZ190YXJnZXRzIDwtIGFycmF5KDAsIGRpbSA9IGMoYmF0Y2hfc2l6ZSwgMSkpDQogIA0KICAjIFRyZW5vd2FuaWUgZ2VuZXJhdG9yYSBwcnp5IHXFvHljaXUgbW9kZWx1IGdhbiBpIHphbXJvxbxlbml1IHdhZyBkeXNrcnltaW5hdG9yYS4NCiAgYV9sb3NzIDwtIGdhbiAlPiUgdHJhaW5fb25fYmF0Y2goIA0KICAgIHJhbmRvbV9sYXRlbnRfdmVjdG9ycywgDQogICAgbWlzbGVhZGluZ190YXJnZXRzDQogICkgIA0KICANCiAgc3RhcnQgPC0gc3RhcnQgKyBiYXRjaF9zaXplDQogIGlmIChzdGFydCA+IChucm93KHhfdHJhaW4pIC0gYmF0Y2hfc2l6ZSkpDQogICAgc3RhcnQgPC0gMQ0KICANCiAgIyBPa2F6am9uYWxueSB6YXBpcyBvYnJhesOzdy4NCiAgaWYgKHN0ZXAgJSUgMTAwID09IDApIHsgDQogICAgDQogICAgIyBaYXBpcyB3YWcgbW9kZWx1Lg0KICAgIHNhdmVfbW9kZWxfd2VpZ2h0c19oZGY1KGdhbiwgImdhbi5oNSIpDQogICAgDQogICAgIyBXecWbd2lldGxhbmllIG1ldHJ5ay4NCiAgICBjYXQoInN0cmF0YSBkeXNrcnltaW5hdG9yYSB3IGtyb2t1IDoiLCBkX2xvc3MsICJcbiIpDQogICAgY2F0KCJzdHJhdGEgcHJ6ZWNpd25hOiIsIGFfbG9zcywgIlxuIikgIA0KICAgIA0KICAgICMgWmFwaXMgamVkbmVnbyB3eWdlbmVyb3dhbmVnbyBvYnJhenUuDQogICAgaW1hZ2VfYXJyYXlfc2F2ZSgNCiAgICAgIGdlbmVyYXRlZF9pbWFnZXNbMSwsLF0gKiAyNTUsIA0KICAgICAgcGF0aCA9IGZpbGUucGF0aChzYXZlX2RpciwgcGFzdGUwKCJnZW5lcmF0ZWRfZnJvZyIsIHN0ZXAsICIucG5nIikpDQogICAgKQ0KICAgDQogICAgIyBaYXBpcyBqZWRuZWdvIHByYXdkeml3ZWdvIG9icmF6dSB3IGNlbGFjaCBwb3LDs3duYXdjenljaC4NCiAgICBpbWFnZV9hcnJheV9zYXZlKA0KICAgICAgcmVhbF9pbWFnZXNbMSwsLF0gKiAyNTUsIA0KICAgICAgcGF0aCA9IGZpbGUucGF0aChzYXZlX2RpciwgcGFzdGUwKCJyZWFsX2Zyb2ciLCBzdGVwLCAiLnBuZyIpKQ0KICAgICkNCiAgfQ0KfQ0KYGBgDQoNCg0KDQo=