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 3232, 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:

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.

rr rr 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”.

rr rr 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
_________________________________________________________________________________________________________

rr rr # 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!

rr rr # 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):

Czas zaimplementować ten mechanizm.

rr

# 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=