This notebook contains the second code sample found in Chapter 6, Section 4 of Deep Learning with R. Note that the original text features far more content, in particular further explanations and figures: in this notebook, you will only find source code and related comments.
Implementing a 1D convnet
In Keras, you use a 1D convnet via the layer_conv_1d()
function, which has an interface similar to layer_conv_2d()
. It takes as input 3D tensors with shape (samples, time, features)
and returns similarly shaped 3D tensors. The convolution window is a 1D window on the temporal axis: the second axis in the input tensor.
Let’s build a simple two-layer 1D convnet and apply it to the IMDB sentiment-classification task you’re already familiar with. As a reminder, this is the code for obtaining and preprocessing the data.
library(keras)
max_features <- 10000
max_len <- 500
cat("Loading data...\n")
Loading data...
imdb <- dataset_imdb(num_words = max_features)
Using TensorFlow backend.
2017-11-23 11:43:44.246774: I tensorflow/core/platform/cpu_feature_guard.cc:137] Your CPU supports instructions that this TensorFlow binary was not compiled to use: SSE4.1 SSE4.2 AVX AVX2 FMA
2017-11-23 11:43:46.912966: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:892] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2017-11-23 11:43:46.913301: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1030] Found device 0 with properties:
name: Tesla K80 major: 3 minor: 7 memoryClockRate(GHz): 0.8235
pciBusID: 0000:00:1e.0
totalMemory: 11.17GiB freeMemory: 11.10GiB
2017-11-23 11:43:46.913327: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1120] Creating TensorFlow device (/device:GPU:0) -> (device: 0, name: Tesla K80, pci bus id: 0000:00:1e.0, compute capability: 3.7)
c(c(x_train, y_train), c(x_test, y_test)) %<-% imdb
cat(length(x_train), "train sequences\n")
25000 train sequences
cat(length(x_test), "test sequences")
25000 test sequences
cat("Pad sequences (samples x time)\n")
Pad sequences (samples x time)
x_train <- pad_sequences(x_train, maxlen = max_len)
x_test <- pad_sequences(x_test, maxlen = max_len)
cat("x_train shape:", dim(x_train), "\n")
x_train shape: 25000 500
cat("x_test shape:", dim(x_test), "\n")
x_test shape: 25000 500
1D convnets are structured in the same way as their 2D counterparts, which you used in chapter 5: they consist of a stack of layer_conv_1d()
and layer_max_pooling_1d()
, ending in either a global pooling layer or a layer_flatten()
, that turn the 3D outputs into 2D outputs, allowing you to add one or more dense layers to the model for classification or regression.
One difference, though, is the fact that you can afford to use larger convolution windows with 1D convnets. With a 2D convolution layer, a 3 × 3 convolution window contains 3 * 3 = 9 feature vectors; but with a 1D convolution layer, a convolution window of size 3 contains only 3 feature vectors. You can thus easily afford 1D convolution windows of size 7 or 9.
This is the example 1D convnet for the IMDB dataset.
model <- keras_model_sequential() %>%
layer_embedding(input_dim = max_features, output_dim = 128,
input_length = max_len) %>%
layer_conv_1d(filters = 32, kernel_size = 7, activation = "relu") %>%
layer_max_pooling_1d(pool_size = 5) %>%
layer_conv_1d(filters = 32, kernel_size = 7, activation = "relu") %>%
layer_global_max_pooling_1d() %>%
layer_dense(units = 1)
summary(model)
_______________________________________________________________________________________
Layer (type) Output Shape Param #
=======================================================================================
embedding_1 (Embedding) (None, 500, 128) 1280000
_______________________________________________________________________________________
conv1d_1 (Conv1D) (None, 494, 32) 28704
_______________________________________________________________________________________
max_pooling1d_1 (MaxPooling1D) (None, 98, 32) 0
_______________________________________________________________________________________
conv1d_2 (Conv1D) (None, 92, 32) 7200
_______________________________________________________________________________________
global_max_pooling1d_1 (GlobalMaxPooli (None, 32) 0
_______________________________________________________________________________________
dense_1 (Dense) (None, 1) 33
=======================================================================================
Total params: 1,315,937
Trainable params: 1,315,937
Non-trainable params: 0
_______________________________________________________________________________________
Here are our training and validation results: validation accuracy is somewhat lower than that of the LSTM we used two sections ago, but runtime is faster, both on CPU and GPU (albeit the exact speedup will vary greatly depending on your exact configuration). At that point, we could re-train this model for the right number of epochs (8), and run it on the test set. This is a convincing demonstration that a 1D convnet can offer a fast, cheap alternative to a recurrent network on a word-level sentiment classification task.
plot(history)

Combining CNNs and RNNs to process long sequences
Because 1D convnets process input patches independently, they are not sensitive to the order of the timesteps (beyond a local scale, the size of the convolution windows), unlike RNNs. Of course, in order to be able to recognize longer-term patterns, one could stack many convolution layers and pooling layers, resulting in upper layers that would “see” long chunks of the original inputs – but that’s still a fairly weak way to induce order-sensitivity. One way to evidence this weakness is to try 1D convnets on the temperature forecasting problem from the previous section, where order-sensitivity was key to produce good predictions. Let’s see:
library(tibble)
library(readr)
data_dir <- "~/Downloads/jena_climate"
fname <- file.path(data_dir, "jena_climate_2009_2016.csv")
data <- read_csv(fname)
data <- data.matrix(data[,-1])
train_data <- data[1:200000,]
mean <- apply(train_data, 2, mean)
std <- apply(train_data, 2, sd)
data <- scale(data, center = mean, scale = std)
generator <- function(data, lookback, delay, min_index, max_index,
shuffle = FALSE, batch_size = 128, step = 6) {
if (is.null(max_index))
max_index <- nrow(data) - delay - 1
i <- min_index + lookback
function() {
if (shuffle) {
rows <- sample(c((min_index+lookback):max_index), size = batch_size)
} else {
if (i + batch_size >= max_index)
i <<- min_index + lookback
rows <- c(i:min(i+batch_size, max_index))
i <<- i + length(rows)
}
samples <- array(0, dim = c(length(rows),
lookback / step,
dim(data)[[-1]]))
targets <- array(0, dim = c(length(rows)))
for (j in 1:length(rows)) {
indices <- seq(rows[[j]] - lookback, rows[[j]],
length.out = dim(samples)[[2]])
samples[j,,] <- data[indices,]
targets[[j]] <- data[rows[[j]] + delay,2]
}
list(samples, targets)
}
}
lookback <- 1440
step <- 6
delay <- 144
batch_size <- 128
train_gen <- generator(
data,
lookback = lookback,
delay = delay,
min_index = 1,
max_index = 200000,
shuffle = TRUE,
step = step,
batch_size = batch_size
)
val_gen = generator(
data,
lookback = lookback,
delay = delay,
min_index = 200001,
max_index = 300000,
step = step,
batch_size = batch_size
)
test_gen <- generator(
data,
lookback = lookback,
delay = delay,
min_index = 300001,
max_index = NULL,
step = step,
batch_size = batch_size
)
# This is how many steps to draw from `val_gen`
# in order to see the whole validation set:
val_steps <- (300000 - 200001 - lookback) / batch_size
# This is how many steps to draw from `test_gen`
# in order to see the whole test set:
test_steps <- (nrow(data) - 300001 - lookback) / batch_size
model <- keras_model_sequential() %>%
layer_conv_1d(filters = 32, kernel_size = 5, activation = "relu",
input_shape = list(NULL, dim(data)[[-1]])) %>%
layer_max_pooling_1d(pool_size = 3) %>%
layer_conv_1d(filters = 32, kernel_size = 5, activation = "relu") %>%
layer_max_pooling_1d(pool_size = 3) %>%
layer_conv_1d(filters = 32, kernel_size = 5, activation = "relu") %>%
layer_global_max_pooling_1d() %>%
layer_dense(units = 1)
model %>% compile(
optimizer = optimizer_rmsprop(),
loss = "mae"
)
history <- model %>% fit_generator(
train_gen,
steps_per_epoch = 500,
epochs = 20,
validation_data = val_gen,
validation_steps = val_steps
)
Here are our training and validation Mean Absolute Errors:
plot(history)

The validation MAE stays in the 0.40s: you can’t even beat the common-sense baseline using the small convnet. Again, this is because the convnet looks for patterns anywhere in the input timeseries and has no knowledge of the temporal position of a pattern it sees (toward the beginning, toward the end, and so on). Because more recent data points should be interpreted differently from older datapoints in the case of this specific forecasting problem, the convnet fails at producing meaningful results. This limitation of convnets isn’t an issue with the IMDB data, because patterns of keywords associated with a positive or negative sentiment are informative independently of where they’re found in the input sentences.
One strategy to combine the speed and lightness of convnets with the order-sensitivity of RNNs is to use a 1D convnet as a preprocessing step before a RNN (see figure 6.30). This is especially beneficial when you’re dealing with sequences that are so long they can’t realistically be processed with RNNs, such as sequences with thousands of steps. The convnet will turn the long input sequence into much shorter (downsampled) sequences of higher-level features. This sequence of extracted features then becomes the input to the RNN part of the network.
This technique isn’t seen often in research papers and practical applications, possibly because it isn’t well known. It’s effective and ought to be more common. Let’s try it on the temperature-forecasting dataset. Because this strategy allows you to manipulate much longer sequences, you can either look at data from longer ago (by increasing the lookback
parameter of the data generator) or look at high-resolution timeseries (by decreasing the step
parameter of the generator). Here, somewhat arbitrarily, you’ll use a step
that’s half as large, resulting in a timeseries twice as long, where the weather data is sampled at a rate of 1 point per 30 minutes. The example reuses the generator
function defined earlier.
# This was previously set to 6 (one point per hour).
# Now 3 (one point per 30 min).
step <- 3
lookback <- 720 # Unchanged
delay <- 144 # Unchanged
train_gen <- generator(
data,
lookback = lookback,
delay = delay,
min_index = 1,
max_index = 200000,
shuffle = TRUE,
step = step
)
val_gen <- generator(
data,
lookback = lookback,
delay = delay,
min_index = 200001,
max_index = 300000,
step = step
)
test_gen <- generator(
data,
lookback = lookback,
delay = delay,
min_index = 300001,
max_index = NULL,
step = step
)
val_steps <- (300000 - 200001 - lookback) / 128
test_steps <- (nrow(data) - 300001 - lookback) / 128
This is the model, starting with two layer_conv_1d()
and following up with a layer_gru()
:
model <- keras_model_sequential() %>%
layer_conv_1d(filters = 32, kernel_size = 5, activation = "relu",
input_shape = list(NULL, dim(data)[[-1]])) %>%
layer_max_pooling_1d(pool_size = 3) %>%
layer_conv_1d(filters = 32, kernel_size = 5, activation = "relu") %>%
layer_gru(units = 32, dropout = 0.1, recurrent_dropout = 0.5) %>%
layer_dense(units = 1)
summary(model)
_______________________________________________________________________________________
Layer (type) Output Shape Param #
=======================================================================================
conv1d_6 (Conv1D) (None, None, 32) 2272
_______________________________________________________________________________________
max_pooling1d_4 (MaxPooling1D) (None, None, 32) 0
_______________________________________________________________________________________
conv1d_7 (Conv1D) (None, None, 32) 5152
_______________________________________________________________________________________
gru_1 (GRU) (None, 32) 6240
_______________________________________________________________________________________
dense_3 (Dense) (None, 1) 33
=======================================================================================
Total params: 13,697
Trainable params: 13,697
Non-trainable params: 0
_______________________________________________________________________________________
model %>% compile(
optimizer = optimizer_rmsprop(),
loss = "mae"
)
history <- model %>% fit_generator(
train_gen,
steps_per_epoch = 500,
epochs = 20,
validation_data = val_gen,
validation_steps = val_steps
)
plot(history)

Judging from the validation loss, this setup is not quite as good as the regularized GRU alone, but it’s significantly faster. It is looking at twice more data, which in this case doesn’t appear to be hugely helpful, but may be important for other datasets.
Wrapping up
Here’s what you should take away from this section:
- In the same way that 2D convnets perform well for processing visual patterns in 2D space, 1D convnets perform well for processing temporal patterns. They offer a faster alternative to RNNs on some problems, in particular natural-lanuage processing tasks.
- Typically, 1D convnets are structured much like their 2D equivalents from the world of computer vision:they consist of stacks of
layer_conv_1d()
and layer_max_pooling_1d()
, ending in a global pooling operation or flattening operation.
- Because RNNs are extremely expensive for processing very long sequences, but 1D convnets are cheap, it can be a good idea to use a 1D convnet as a preprocessing step before a RNN, shortening the sequence and extracting useful representations for the RNN to process.
One useful and important concept that we won’t cover in these pages is that of 1D convolution with dilated kernels.
LS0tCnRpdGxlOiAiU2VxdWVuY2UgcHJvY2Vzc2luZyB3aXRoIGNvbnZuZXRzIgpvdXRwdXQ6IAogIGh0bWxfbm90ZWJvb2s6IAogICAgdGhlbWU6IGNlcnVsZWFuCiAgICBoaWdobGlnaHQ6IHRleHRtYXRlCi0tLQoKYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9CmtuaXRyOjpvcHRzX2NodW5rJHNldCh3YXJuaW5nID0gRkFMU0UsIG1lc3NhZ2UgPSBGQUxTRSkKYGBgCgoqKioKClRoaXMgbm90ZWJvb2sgY29udGFpbnMgdGhlIHNlY29uZCBjb2RlIHNhbXBsZSBmb3VuZCBpbiBDaGFwdGVyIDYsIFNlY3Rpb24gNCBvZiBbRGVlcCBMZWFybmluZyB3aXRoIFJdKGh0dHBzOi8vd3d3Lm1hbm5pbmcuY29tL2Jvb2tzL2RlZXAtbGVhcm5pbmctd2l0aC1yKS4gTm90ZSB0aGF0IHRoZSBvcmlnaW5hbCB0ZXh0IGZlYXR1cmVzIGZhciBtb3JlIGNvbnRlbnQsIGluIHBhcnRpY3VsYXIgZnVydGhlciBleHBsYW5hdGlvbnMgYW5kIGZpZ3VyZXM6IGluIHRoaXMgbm90ZWJvb2ssIHlvdSB3aWxsIG9ubHkgZmluZCBzb3VyY2UgY29kZSBhbmQgcmVsYXRlZCBjb21tZW50cy4KCioqKgoKIyMgSW1wbGVtZW50aW5nIGEgMUQgY29udm5ldAogIApJbiBLZXJhcywgeW91IHVzZSBhIDFEIGNvbnZuZXQgdmlhIHRoZSBgbGF5ZXJfY29udl8xZCgpYCBmdW5jdGlvbiwgd2hpY2ggaGFzIGFuIGludGVyZmFjZSBzaW1pbGFyIHRvIGBsYXllcl9jb252XzJkKClgLiBJdCB0YWtlcyBhcyBpbnB1dCAzRCB0ZW5zb3JzIHdpdGggc2hhcGUgYChzYW1wbGVzLCB0aW1lLCBmZWF0dXJlcylgIGFuZCByZXR1cm5zIHNpbWlsYXJseSBzaGFwZWQgM0QgdGVuc29ycy4gVGhlIGNvbnZvbHV0aW9uIHdpbmRvdyBpcyBhIDFEIHdpbmRvdyBvbiB0aGUgdGVtcG9yYWwgYXhpczogdGhlIHNlY29uZCBheGlzIGluIHRoZSBpbnB1dCB0ZW5zb3IuCgpMZXQncyBidWlsZCBhIHNpbXBsZSB0d28tbGF5ZXIgMUQgY29udm5ldCBhbmQgYXBwbHkgaXQgdG8gdGhlIElNREIgc2VudGltZW50LWNsYXNzaWZpY2F0aW9uIHRhc2sgeW91J3JlIGFscmVhZHkgZmFtaWxpYXIgd2l0aC4gQXMgYSByZW1pbmRlciwgdGhpcyBpcyB0aGUgY29kZSBmb3Igb2J0YWluaW5nIGFuZCBwcmVwcm9jZXNzaW5nIHRoZSBkYXRhLgoKYGBge3J9CmxpYnJhcnkoa2VyYXMpCgptYXhfZmVhdHVyZXMgPC0gMTAwMDAKbWF4X2xlbiA8LSA1MDAKCmNhdCgiTG9hZGluZyBkYXRhLi4uXG4iKQppbWRiIDwtIGRhdGFzZXRfaW1kYihudW1fd29yZHMgPSBtYXhfZmVhdHVyZXMpCmMoYyh4X3RyYWluLCB5X3RyYWluKSwgYyh4X3Rlc3QsIHlfdGVzdCkpICU8LSUgaW1kYiAKY2F0KGxlbmd0aCh4X3RyYWluKSwgInRyYWluIHNlcXVlbmNlc1xuIikKY2F0KGxlbmd0aCh4X3Rlc3QpLCAidGVzdCBzZXF1ZW5jZXMiKQoKY2F0KCJQYWQgc2VxdWVuY2VzIChzYW1wbGVzIHggdGltZSlcbiIpCnhfdHJhaW4gPC0gcGFkX3NlcXVlbmNlcyh4X3RyYWluLCBtYXhsZW4gPSBtYXhfbGVuKQp4X3Rlc3QgPC0gcGFkX3NlcXVlbmNlcyh4X3Rlc3QsIG1heGxlbiA9IG1heF9sZW4pCmNhdCgieF90cmFpbiBzaGFwZToiLCBkaW0oeF90cmFpbiksICJcbiIpCmNhdCgieF90ZXN0IHNoYXBlOiIsIGRpbSh4X3Rlc3QpLCAiXG4iKQpgYGAKCjFEIGNvbnZuZXRzIGFyZSBzdHJ1Y3R1cmVkIGluIHRoZSBzYW1lIHdheSBhcyB0aGVpciAyRCBjb3VudGVycGFydHMsIHdoaWNoIHlvdSB1c2VkIGluIGNoYXB0ZXIgNTogdGhleSBjb25zaXN0IG9mIGEgc3RhY2sgb2YgYGxheWVyX2NvbnZfMWQoKWAgYW5kIGBsYXllcl9tYXhfcG9vbGluZ18xZCgpYCwgZW5kaW5nIGluIGVpdGhlciBhIGdsb2JhbCBwb29saW5nIGxheWVyIG9yIGEgYGxheWVyX2ZsYXR0ZW4oKWAsIHRoYXQgdHVybiB0aGUgM0Qgb3V0cHV0cyBpbnRvIDJEIG91dHB1dHMsIGFsbG93aW5nIHlvdSB0byBhZGQgb25lIG9yIG1vcmUgZGVuc2UgbGF5ZXJzIHRvIHRoZSBtb2RlbCBmb3IgY2xhc3NpZmljYXRpb24gb3IgcmVncmVzc2lvbi4KCk9uZSBkaWZmZXJlbmNlLCB0aG91Z2gsIGlzIHRoZSBmYWN0IHRoYXQgeW91IGNhbiBhZmZvcmQgdG8gdXNlIGxhcmdlciBjb252b2x1dGlvbiB3aW5kb3dzIHdpdGggMUQgY29udm5ldHMuIFdpdGggYSAyRCBjb252b2x1dGlvbiBsYXllciwgYSAzIMOXIDMgY29udm9sdXRpb24gd2luZG93IGNvbnRhaW5zIDMgKiAzID0gOSBmZWF0dXJlIHZlY3RvcnM7IGJ1dCB3aXRoIGEgMUQgY29udm9sdXRpb24gbGF5ZXIsIGEgY29udm9sdXRpb24gd2luZG93IG9mIHNpemUgMyBjb250YWlucyBvbmx5IDMgZmVhdHVyZSB2ZWN0b3JzLiBZb3UgY2FuIHRodXMgZWFzaWx5IGFmZm9yZCAxRCBjb252b2x1dGlvbiB3aW5kb3dzIG9mIHNpemUgNyBvciA5LgoKVGhpcyBpcyB0aGUgZXhhbXBsZSAxRCBjb252bmV0IGZvciB0aGUgSU1EQiBkYXRhc2V0LgoKYGBge3J9Cm1vZGVsIDwtIGtlcmFzX21vZGVsX3NlcXVlbnRpYWwoKSAlPiUgCiAgbGF5ZXJfZW1iZWRkaW5nKGlucHV0X2RpbSA9IG1heF9mZWF0dXJlcywgb3V0cHV0X2RpbSA9IDEyOCwKICAgICAgICAgICAgICAgICAgaW5wdXRfbGVuZ3RoID0gbWF4X2xlbikgJT4lIAogIGxheWVyX2NvbnZfMWQoZmlsdGVycyA9IDMyLCBrZXJuZWxfc2l6ZSA9IDcsIGFjdGl2YXRpb24gPSAicmVsdSIpICU+JSAKICBsYXllcl9tYXhfcG9vbGluZ18xZChwb29sX3NpemUgPSA1KSAlPiUgCiAgbGF5ZXJfY29udl8xZChmaWx0ZXJzID0gMzIsIGtlcm5lbF9zaXplID0gNywgYWN0aXZhdGlvbiA9ICJyZWx1IikgJT4lIAogIGxheWVyX2dsb2JhbF9tYXhfcG9vbGluZ18xZCgpICU+JSAKICBsYXllcl9kZW5zZSh1bml0cyA9IDEpCgpzdW1tYXJ5KG1vZGVsKQpgYGAKCmBgYHtyLCBlY2hvPUZBTFNFLCByZXN1bHRzPSdoaWRlJ30KCm1vZGVsICU+JSBjb21waWxlKAogIG9wdGltaXplciA9IG9wdGltaXplcl9ybXNwcm9wKGxyID0gMWUtNCksCiAgbG9zcyA9ICJiaW5hcnlfY3Jvc3NlbnRyb3B5IiwKICBtZXRyaWNzID0gYygiYWNjIikKKQoKaGlzdG9yeSA8LSBtb2RlbCAlPiUgZml0KAogIHhfdHJhaW4sIHlfdHJhaW4sCiAgZXBvY2hzID0gMTAsCiAgYmF0Y2hfc2l6ZSA9IDEyOCwKICB2YWxpZGF0aW9uX3NwbGl0ID0gMC4yCikKYGBgCgpIZXJlIGFyZSBvdXIgdHJhaW5pbmcgYW5kIHZhbGlkYXRpb24gcmVzdWx0czogdmFsaWRhdGlvbiBhY2N1cmFjeSBpcyBzb21ld2hhdCBsb3dlciB0aGFuIHRoYXQgb2YgdGhlIExTVE0gd2UgdXNlZCB0d28gc2VjdGlvbnMgYWdvLCBidXQgcnVudGltZSBpcyBmYXN0ZXIsIGJvdGggb24gQ1BVIGFuZCBHUFUgKGFsYmVpdCB0aGUgZXhhY3Qgc3BlZWR1cCB3aWxsIHZhcnkgZ3JlYXRseSBkZXBlbmRpbmcgb24geW91ciBleGFjdCBjb25maWd1cmF0aW9uKS4gQXQgdGhhdCBwb2ludCwgd2UgY291bGQgcmUtdHJhaW4gdGhpcyBtb2RlbCBmb3IgdGhlIHJpZ2h0IG51bWJlciBvZiBlcG9jaHMgKDgpLCBhbmQgcnVuIGl0IG9uIHRoZSB0ZXN0IHNldC4gVGhpcyBpcyBhIGNvbnZpbmNpbmcgZGVtb25zdHJhdGlvbiB0aGF0IGEgMUQgY29udm5ldCBjYW4gb2ZmZXIgYSBmYXN0LCBjaGVhcCBhbHRlcm5hdGl2ZSB0byBhIHJlY3VycmVudCBuZXR3b3JrIG9uIGEgd29yZC1sZXZlbCBzZW50aW1lbnQgY2xhc3NpZmljYXRpb24gdGFzay4KCmBgYHtyfQpwbG90KGhpc3RvcnkpCmBgYAoKIyMgQ29tYmluaW5nIENOTnMgYW5kIFJOTnMgdG8gcHJvY2VzcyBsb25nIHNlcXVlbmNlcwoKCkJlY2F1c2UgMUQgY29udm5ldHMgcHJvY2VzcyBpbnB1dCBwYXRjaGVzIGluZGVwZW5kZW50bHksIHRoZXkgYXJlIG5vdCBzZW5zaXRpdmUgdG8gdGhlIG9yZGVyIG9mIHRoZSB0aW1lc3RlcHMgKGJleW9uZCBhIGxvY2FsIHNjYWxlLCB0aGUgc2l6ZSBvZiB0aGUgY29udm9sdXRpb24gd2luZG93cyksIHVubGlrZSBSTk5zLiBPZiBjb3Vyc2UsIGluIG9yZGVyIHRvIGJlIGFibGUgdG8gcmVjb2duaXplIGxvbmdlci10ZXJtIHBhdHRlcm5zLCBvbmUgY291bGQgc3RhY2sgbWFueSBjb252b2x1dGlvbiBsYXllcnMgYW5kIHBvb2xpbmcgbGF5ZXJzLCByZXN1bHRpbmcgaW4gdXBwZXIgbGF5ZXJzIHRoYXQgd291bGQgInNlZSIgbG9uZyBjaHVua3Mgb2YgdGhlIG9yaWdpbmFsIGlucHV0cyAtLSBidXQgdGhhdCdzIHN0aWxsIGEgZmFpcmx5IHdlYWsgd2F5IHRvIGluZHVjZSBvcmRlci1zZW5zaXRpdml0eS4gT25lIHdheSB0byBldmlkZW5jZSB0aGlzIHdlYWtuZXNzIGlzIHRvIHRyeSAxRCBjb252bmV0cyBvbiB0aGUgdGVtcGVyYXR1cmUgZm9yZWNhc3RpbmcgcHJvYmxlbSBmcm9tIHRoZSBwcmV2aW91cyBzZWN0aW9uLCB3aGVyZSBvcmRlci1zZW5zaXRpdml0eSB3YXMga2V5IHRvIHByb2R1Y2UgZ29vZCBwcmVkaWN0aW9ucy4gTGV0J3Mgc2VlOgoKYGBge3IsIHJlc3VsdHM9J2hpZGUnfQpsaWJyYXJ5KHRpYmJsZSkKbGlicmFyeShyZWFkcikKCmRhdGFfZGlyIDwtICJ+L0Rvd25sb2Fkcy9qZW5hX2NsaW1hdGUiCmZuYW1lIDwtIGZpbGUucGF0aChkYXRhX2RpciwgImplbmFfY2xpbWF0ZV8yMDA5XzIwMTYuY3N2IikKZGF0YSA8LSByZWFkX2NzdihmbmFtZSkKCmRhdGEgPC0gZGF0YS5tYXRyaXgoZGF0YVssLTFdKQoKdHJhaW5fZGF0YSA8LSBkYXRhWzE6MjAwMDAwLF0KbWVhbiA8LSBhcHBseSh0cmFpbl9kYXRhLCAyLCBtZWFuKQpzdGQgPC0gYXBwbHkodHJhaW5fZGF0YSwgMiwgc2QpCmRhdGEgPC0gc2NhbGUoZGF0YSwgY2VudGVyID0gbWVhbiwgc2NhbGUgPSBzdGQpCgpnZW5lcmF0b3IgPC0gZnVuY3Rpb24oZGF0YSwgbG9va2JhY2ssIGRlbGF5LCBtaW5faW5kZXgsIG1heF9pbmRleCwKICAgICAgICAgICAgICAgICAgICAgIHNodWZmbGUgPSBGQUxTRSwgYmF0Y2hfc2l6ZSA9IDEyOCwgc3RlcCA9IDYpIHsKICBpZiAoaXMubnVsbChtYXhfaW5kZXgpKQogICAgbWF4X2luZGV4IDwtIG5yb3coZGF0YSkgLSBkZWxheSAtIDEKICBpIDwtIG1pbl9pbmRleCArIGxvb2tiYWNrCiAgZnVuY3Rpb24oKSB7CiAgICBpZiAoc2h1ZmZsZSkgewogICAgICByb3dzIDwtIHNhbXBsZShjKChtaW5faW5kZXgrbG9va2JhY2spOm1heF9pbmRleCksIHNpemUgPSBiYXRjaF9zaXplKQogICAgfSBlbHNlIHsKICAgICAgaWYgKGkgKyBiYXRjaF9zaXplID49IG1heF9pbmRleCkKICAgICAgICBpIDw8LSBtaW5faW5kZXggKyBsb29rYmFjawogICAgICByb3dzIDwtIGMoaTptaW4oaStiYXRjaF9zaXplLCBtYXhfaW5kZXgpKQogICAgICBpIDw8LSBpICsgbGVuZ3RoKHJvd3MpCiAgICB9CiAgICAKICAgIHNhbXBsZXMgPC0gYXJyYXkoMCwgZGltID0gYyhsZW5ndGgocm93cyksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGxvb2tiYWNrIC8gc3RlcCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkaW0oZGF0YSlbWy0xXV0pKQogICAgdGFyZ2V0cyA8LSBhcnJheSgwLCBkaW0gPSBjKGxlbmd0aChyb3dzKSkpCiAgICAgICAgICAgICAgICAgICAgIAogICAgZm9yIChqIGluIDE6bGVuZ3RoKHJvd3MpKSB7CiAgICAgIGluZGljZXMgPC0gc2VxKHJvd3NbW2pdXSAtIGxvb2tiYWNrLCByb3dzW1tqXV0sIAogICAgICAgICAgICAgICAgICAgICBsZW5ndGgub3V0ID0gZGltKHNhbXBsZXMpW1syXV0pCiAgICAgIHNhbXBsZXNbaiwsXSA8LSBkYXRhW2luZGljZXMsXQogICAgICB0YXJnZXRzW1tqXV0gPC0gZGF0YVtyb3dzW1tqXV0gKyBkZWxheSwyXQogICAgfSAgICAgICAgICAgIAogICAgCiAgICBsaXN0KHNhbXBsZXMsIHRhcmdldHMpCiAgfQp9Cgpsb29rYmFjayA8LSAxNDQwCnN0ZXAgPC0gNgpkZWxheSA8LSAxNDQKYmF0Y2hfc2l6ZSA8LSAxMjgKCnRyYWluX2dlbiA8LSBnZW5lcmF0b3IoCiAgZGF0YSwKICBsb29rYmFjayA9IGxvb2tiYWNrLAogIGRlbGF5ID0gZGVsYXksCiAgbWluX2luZGV4ID0gMSwKICBtYXhfaW5kZXggPSAyMDAwMDAsCiAgc2h1ZmZsZSA9IFRSVUUsCiAgc3RlcCA9IHN0ZXAsIAogIGJhdGNoX3NpemUgPSBiYXRjaF9zaXplCikKCnZhbF9nZW4gPSBnZW5lcmF0b3IoCiAgZGF0YSwKICBsb29rYmFjayA9IGxvb2tiYWNrLAogIGRlbGF5ID0gZGVsYXksCiAgbWluX2luZGV4ID0gMjAwMDAxLAogIG1heF9pbmRleCA9IDMwMDAwMCwKICBzdGVwID0gc3RlcCwKICBiYXRjaF9zaXplID0gYmF0Y2hfc2l6ZQopCgp0ZXN0X2dlbiA8LSBnZW5lcmF0b3IoCiAgZGF0YSwKICBsb29rYmFjayA9IGxvb2tiYWNrLAogIGRlbGF5ID0gZGVsYXksCiAgbWluX2luZGV4ID0gMzAwMDAxLAogIG1heF9pbmRleCA9IE5VTEwsCiAgc3RlcCA9IHN0ZXAsCiAgYmF0Y2hfc2l6ZSA9IGJhdGNoX3NpemUKKQoKIyBUaGlzIGlzIGhvdyBtYW55IHN0ZXBzIHRvIGRyYXcgZnJvbSBgdmFsX2dlbmAKIyBpbiBvcmRlciB0byBzZWUgdGhlIHdob2xlIHZhbGlkYXRpb24gc2V0Ogp2YWxfc3RlcHMgPC0gKDMwMDAwMCAtIDIwMDAwMSAtIGxvb2tiYWNrKSAvIGJhdGNoX3NpemUKCiAgIyBUaGlzIGlzIGhvdyBtYW55IHN0ZXBzIHRvIGRyYXcgZnJvbSBgdGVzdF9nZW5gCiMgaW4gb3JkZXIgdG8gc2VlIHRoZSB3aG9sZSB0ZXN0IHNldDoKdGVzdF9zdGVwcyA8LSAobnJvdyhkYXRhKSAtIDMwMDAwMSAtIGxvb2tiYWNrKSAvIGJhdGNoX3NpemUKYGBgCgpgYGB7ciwgZWNobz1UUlVFLCByZXN1bHRzPSdoaWRlJ30KbW9kZWwgPC0ga2VyYXNfbW9kZWxfc2VxdWVudGlhbCgpICU+JSAKICBsYXllcl9jb252XzFkKGZpbHRlcnMgPSAzMiwga2VybmVsX3NpemUgPSA1LCBhY3RpdmF0aW9uID0gInJlbHUiLAogICAgICAgICAgICAgICAgaW5wdXRfc2hhcGUgPSBsaXN0KE5VTEwsIGRpbShkYXRhKVtbLTFdXSkpICU+JSAKICBsYXllcl9tYXhfcG9vbGluZ18xZChwb29sX3NpemUgPSAzKSAlPiUgCiAgbGF5ZXJfY29udl8xZChmaWx0ZXJzID0gMzIsIGtlcm5lbF9zaXplID0gNSwgYWN0aXZhdGlvbiA9ICJyZWx1IikgJT4lIAogIGxheWVyX21heF9wb29saW5nXzFkKHBvb2xfc2l6ZSA9IDMpICU+JSAKICBsYXllcl9jb252XzFkKGZpbHRlcnMgPSAzMiwga2VybmVsX3NpemUgPSA1LCBhY3RpdmF0aW9uID0gInJlbHUiKSAlPiUgCiAgbGF5ZXJfZ2xvYmFsX21heF9wb29saW5nXzFkKCkgJT4lIAogIGxheWVyX2RlbnNlKHVuaXRzID0gMSkKCgptb2RlbCAlPiUgY29tcGlsZSgKICBvcHRpbWl6ZXIgPSBvcHRpbWl6ZXJfcm1zcHJvcCgpLAogIGxvc3MgPSAibWFlIgopCgpoaXN0b3J5IDwtIG1vZGVsICU+JSBmaXRfZ2VuZXJhdG9yKAogIHRyYWluX2dlbiwKICBzdGVwc19wZXJfZXBvY2ggPSA1MDAsCiAgZXBvY2hzID0gMjAsCiAgdmFsaWRhdGlvbl9kYXRhID0gdmFsX2dlbiwKICB2YWxpZGF0aW9uX3N0ZXBzID0gdmFsX3N0ZXBzCikKYGBgCgpIZXJlIGFyZSBvdXIgdHJhaW5pbmcgYW5kIHZhbGlkYXRpb24gTWVhbiBBYnNvbHV0ZSBFcnJvcnM6CgpgYGB7cn0KcGxvdChoaXN0b3J5KQpgYGAKClRoZSB2YWxpZGF0aW9uIE1BRSBzdGF5cyBpbiB0aGUgMC40MHM6IHlvdSBjYW4ndCBldmVuIGJlYXQgdGhlIGNvbW1vbi1zZW5zZSBiYXNlbGluZSB1c2luZyB0aGUgc21hbGwgY29udm5ldC4gQWdhaW4sIHRoaXMgaXMgYmVjYXVzZSB0aGUgY29udm5ldCBsb29rcyBmb3IgcGF0dGVybnMgYW55d2hlcmUgaW4gdGhlIGlucHV0IHRpbWVzZXJpZXMgYW5kIGhhcyBubyBrbm93bGVkZ2Ugb2YgdGhlIHRlbXBvcmFsIHBvc2l0aW9uIG9mIGEgcGF0dGVybiBpdCBzZWVzICh0b3dhcmQgdGhlIGJlZ2lubmluZywgdG93YXJkIHRoZSBlbmQsIGFuZCBzbyBvbikuIEJlY2F1c2UgbW9yZSByZWNlbnQgZGF0YSBwb2ludHMgc2hvdWxkIGJlIGludGVycHJldGVkIGRpZmZlcmVudGx5IGZyb20gb2xkZXIgZGF0YXBvaW50cyBpbiB0aGUgY2FzZSBvZiB0aGlzIHNwZWNpZmljIGZvcmVjYXN0aW5nIHByb2JsZW0sIHRoZSBjb252bmV0IGZhaWxzIGF0IHByb2R1Y2luZyBtZWFuaW5nZnVsIHJlc3VsdHMuIFRoaXMgbGltaXRhdGlvbiBvZiBjb252bmV0cyBpc24ndCBhbiBpc3N1ZSB3aXRoIHRoZSBJTURCIGRhdGEsIGJlY2F1c2UgcGF0dGVybnMgb2Yga2V5d29yZHMgYXNzb2NpYXRlZCB3aXRoIGEgcG9zaXRpdmUgb3IgbmVnYXRpdmUgc2VudGltZW50IGFyZSBpbmZvcm1hdGl2ZSBpbmRlcGVuZGVudGx5IG9mIHdoZXJlIHRoZXkncmUgZm91bmQgaW4gdGhlIGlucHV0IHNlbnRlbmNlcy4KCk9uZSBzdHJhdGVneSB0byBjb21iaW5lIHRoZSBzcGVlZCBhbmQgbGlnaHRuZXNzIG9mIGNvbnZuZXRzIHdpdGggdGhlIG9yZGVyLXNlbnNpdGl2aXR5IG9mIFJOTnMgaXMgdG8gdXNlIGEgMUQgY29udm5ldCBhcyBhIHByZXByb2Nlc3Npbmcgc3RlcCBiZWZvcmUgYSBSTk4gKHNlZSBmaWd1cmUgNi4zMCkuIFRoaXMgaXMgZXNwZWNpYWxseSBiZW5lZmljaWFsIHdoZW4geW91J3JlIGRlYWxpbmcgd2l0aCBzZXF1ZW5jZXMgdGhhdCBhcmUgc28gbG9uZyB0aGV5IGNhbid0IHJlYWxpc3RpY2FsbHkgYmUgcHJvY2Vzc2VkIHdpdGggUk5Ocywgc3VjaCBhcyBzZXF1ZW5jZXMgd2l0aCB0aG91c2FuZHMgb2Ygc3RlcHMuIFRoZSBjb252bmV0IHdpbGwgdHVybiB0aGUgbG9uZyBpbnB1dCBzZXF1ZW5jZSBpbnRvIG11Y2ggc2hvcnRlciAoZG93bnNhbXBsZWQpIHNlcXVlbmNlcyBvZiBoaWdoZXItbGV2ZWwgZmVhdHVyZXMuIFRoaXMgc2VxdWVuY2Ugb2YgZXh0cmFjdGVkIGZlYXR1cmVzIHRoZW4gYmVjb21lcyB0aGUgaW5wdXQgdG8gdGhlIFJOTiBwYXJ0IG9mIHRoZSBuZXR3b3JrLgoKVGhpcyB0ZWNobmlxdWUgaXNuJ3Qgc2VlbiBvZnRlbiBpbiByZXNlYXJjaCBwYXBlcnMgYW5kIHByYWN0aWNhbCBhcHBsaWNhdGlvbnMsIHBvc3NpYmx5IGJlY2F1c2UgaXQgaXNuJ3Qgd2VsbCBrbm93bi4gSXQncyBlZmZlY3RpdmUgYW5kIG91Z2h0IHRvIGJlIG1vcmUgY29tbW9uLiBMZXQncyB0cnkgaXQgb24gdGhlIHRlbXBlcmF0dXJlLWZvcmVjYXN0aW5nIGRhdGFzZXQuIEJlY2F1c2UgdGhpcyBzdHJhdGVneSBhbGxvd3MgeW91IHRvIG1hbmlwdWxhdGUgbXVjaCBsb25nZXIgc2VxdWVuY2VzLCB5b3UgY2FuIGVpdGhlciBsb29rIGF0IGRhdGEgZnJvbSBsb25nZXIgYWdvIChieSBpbmNyZWFzaW5nIHRoZSBgbG9va2JhY2tgIHBhcmFtZXRlciBvZiB0aGUgZGF0YSBnZW5lcmF0b3IpIG9yIGxvb2sgYXQgaGlnaC1yZXNvbHV0aW9uIHRpbWVzZXJpZXMgKGJ5IGRlY3JlYXNpbmcgdGhlIGBzdGVwYCBwYXJhbWV0ZXIgb2YgdGhlIGdlbmVyYXRvcikuIEhlcmUsIHNvbWV3aGF0IGFyYml0cmFyaWx5LCB5b3UnbGwgdXNlIGEgYHN0ZXBgIHRoYXQncyBoYWxmIGFzIGxhcmdlLCByZXN1bHRpbmcgaW4gYSB0aW1lc2VyaWVzIHR3aWNlIGFzIGxvbmcsIHdoZXJlIHRoZSB3ZWF0aGVyIGRhdGEgaXMgc2FtcGxlZCBhdCBhIHJhdGUgb2YgMSBwb2ludCBwZXIgMzAgbWludXRlcy4gVGhlIGV4YW1wbGUgcmV1c2VzIHRoZSBgZ2VuZXJhdG9yYCBmdW5jdGlvbiBkZWZpbmVkIGVhcmxpZXIuCgoKYGBge3J9CiMgVGhpcyB3YXMgcHJldmlvdXNseSBzZXQgdG8gNiAob25lIHBvaW50IHBlciBob3VyKS4KIyBOb3cgMyAob25lIHBvaW50IHBlciAzMCBtaW4pLgpzdGVwIDwtIDMgCmxvb2tiYWNrIDwtIDcyMCAgIyBVbmNoYW5nZWQKZGVsYXkgPC0gMTQ0ICAjIFVuY2hhbmdlZAogIAp0cmFpbl9nZW4gPC0gZ2VuZXJhdG9yKAogIGRhdGEsCiAgbG9va2JhY2sgPSBsb29rYmFjaywKICBkZWxheSA9IGRlbGF5LAogIG1pbl9pbmRleCA9IDEsCiAgbWF4X2luZGV4ID0gMjAwMDAwLAogIHNodWZmbGUgPSBUUlVFLAogIHN0ZXAgPSBzdGVwCikKCnZhbF9nZW4gPC0gZ2VuZXJhdG9yKAogIGRhdGEsCiAgbG9va2JhY2sgPSBsb29rYmFjaywKICBkZWxheSA9IGRlbGF5LAogIG1pbl9pbmRleCA9IDIwMDAwMSwKICBtYXhfaW5kZXggPSAzMDAwMDAsCiAgc3RlcCA9IHN0ZXAKKQoKdGVzdF9nZW4gPC0gZ2VuZXJhdG9yKAogIGRhdGEsCiAgbG9va2JhY2sgPSBsb29rYmFjaywKICBkZWxheSA9IGRlbGF5LAogIG1pbl9pbmRleCA9IDMwMDAwMSwKICBtYXhfaW5kZXggPSBOVUxMLAogIHN0ZXAgPSBzdGVwCikKCnZhbF9zdGVwcyA8LSAoMzAwMDAwIC0gMjAwMDAxIC0gbG9va2JhY2spIC8gMTI4CnRlc3Rfc3RlcHMgPC0gKG5yb3coZGF0YSkgLSAzMDAwMDEgLSBsb29rYmFjaykgLyAxMjgKYGBgCgpUaGlzIGlzIHRoZSBtb2RlbCwgc3RhcnRpbmcgd2l0aCB0d28gYGxheWVyX2NvbnZfMWQoKWAgYW5kIGZvbGxvd2luZyB1cCB3aXRoIGEgYGxheWVyX2dydSgpYDoKCmBgYHtyfQptb2RlbCA8LSBrZXJhc19tb2RlbF9zZXF1ZW50aWFsKCkgJT4lIAogIGxheWVyX2NvbnZfMWQoZmlsdGVycyA9IDMyLCBrZXJuZWxfc2l6ZSA9IDUsIGFjdGl2YXRpb24gPSAicmVsdSIsCiAgICAgICAgICAgICAgICBpbnB1dF9zaGFwZSA9IGxpc3QoTlVMTCwgZGltKGRhdGEpW1stMV1dKSkgJT4lIAogIGxheWVyX21heF9wb29saW5nXzFkKHBvb2xfc2l6ZSA9IDMpICU+JSAKICBsYXllcl9jb252XzFkKGZpbHRlcnMgPSAzMiwga2VybmVsX3NpemUgPSA1LCBhY3RpdmF0aW9uID0gInJlbHUiKSAlPiUgCiAgbGF5ZXJfZ3J1KHVuaXRzID0gMzIsIGRyb3BvdXQgPSAwLjEsIHJlY3VycmVudF9kcm9wb3V0ID0gMC41KSAlPiUgCiAgbGF5ZXJfZGVuc2UodW5pdHMgPSAxKQoKc3VtbWFyeShtb2RlbCkKYGBgCgpgYGB7ciwgZWNobz1UUlVFLCByZXN1bHRzPSdoaWRlJ30KbW9kZWwgJT4lIGNvbXBpbGUoCiAgb3B0aW1pemVyID0gb3B0aW1pemVyX3Jtc3Byb3AoKSwKICBsb3NzID0gIm1hZSIKKQoKaGlzdG9yeSA8LSBtb2RlbCAlPiUgZml0X2dlbmVyYXRvcigKICB0cmFpbl9nZW4sCiAgc3RlcHNfcGVyX2Vwb2NoID0gNTAwLAogIGVwb2NocyA9IDIwLAogIHZhbGlkYXRpb25fZGF0YSA9IHZhbF9nZW4sCiAgdmFsaWRhdGlvbl9zdGVwcyA9IHZhbF9zdGVwcwopCmBgYAoKYGBge3J9CnBsb3QoaGlzdG9yeSkKYGBgCgpKdWRnaW5nIGZyb20gdGhlIHZhbGlkYXRpb24gbG9zcywgdGhpcyBzZXR1cCBpcyBub3QgcXVpdGUgYXMgZ29vZCBhcyB0aGUgcmVndWxhcml6ZWQgR1JVIGFsb25lLCBidXQgaXQncyBzaWduaWZpY2FudGx5IGZhc3Rlci4gSXQgaXMgbG9va2luZyBhdCB0d2ljZSBtb3JlIGRhdGEsIHdoaWNoIGluIHRoaXMgY2FzZSBkb2Vzbid0IGFwcGVhciB0byBiZSBodWdlbHkgaGVscGZ1bCwgYnV0IG1heSBiZSBpbXBvcnRhbnQgZm9yIG90aGVyIGRhdGFzZXRzLgoKIyMgV3JhcHBpbmcgdXAKCkhlcmUncyB3aGF0IHlvdSBzaG91bGQgdGFrZSBhd2F5IGZyb20gdGhpcyBzZWN0aW9uOgoKKiBJbiB0aGUgc2FtZSB3YXkgdGhhdCAyRCBjb252bmV0cyBwZXJmb3JtIHdlbGwgZm9yIHByb2Nlc3NpbmcgdmlzdWFsIHBhdHRlcm5zIGluIDJEIHNwYWNlLCAxRCBjb252bmV0cyBwZXJmb3JtIHdlbGwgZm9yIHByb2Nlc3NpbmcgdGVtcG9yYWwgcGF0dGVybnMuIFRoZXkgb2ZmZXIgYSBmYXN0ZXIgYWx0ZXJuYXRpdmUgdG8gUk5OcyBvbiBzb21lIHByb2JsZW1zLCBpbiBwYXJ0aWN1bGFyIG5hdHVyYWwtbGFudWFnZSBwcm9jZXNzaW5nIHRhc2tzLgoqIFR5cGljYWxseSwgMUQgY29udm5ldHMgYXJlIHN0cnVjdHVyZWQgbXVjaCBsaWtlIHRoZWlyIDJEIGVxdWl2YWxlbnRzIGZyb20gdGhlIHdvcmxkIG9mIGNvbXB1dGVyIHZpc2lvbjp0aGV5IGNvbnNpc3Qgb2Ygc3RhY2tzIG9mIGBsYXllcl9jb252XzFkKClgIGFuZCBgbGF5ZXJfbWF4X3Bvb2xpbmdfMWQoKWAsIGVuZGluZyBpbiBhIGdsb2JhbCBwb29saW5nIG9wZXJhdGlvbiBvciBmbGF0dGVuaW5nIG9wZXJhdGlvbi4KKiBCZWNhdXNlIFJOTnMgYXJlIGV4dHJlbWVseSBleHBlbnNpdmUgZm9yIHByb2Nlc3NpbmcgdmVyeSBsb25nIHNlcXVlbmNlcywgYnV0IDFEIGNvbnZuZXRzIGFyZSBjaGVhcCwgaXQgY2FuIGJlIGEgZ29vZCBpZGVhIHRvIHVzZSBhIDFEIGNvbnZuZXQgYXMgYSBwcmVwcm9jZXNzaW5nIHN0ZXAgYmVmb3JlIGEgUk5OLCBzaG9ydGVuaW5nIHRoZSBzZXF1ZW5jZSBhbmQgZXh0cmFjdGluZyB1c2VmdWwgcmVwcmVzZW50YXRpb25zIGZvciB0aGUgUk5OIHRvIHByb2Nlc3MuCgpPbmUgdXNlZnVsIGFuZCBpbXBvcnRhbnQgY29uY2VwdCB0aGF0IHdlIHdvbid0IGNvdmVyIGluIHRoZXNlIHBhZ2VzIGlzIHRoYXQgb2YgMUQgY29udm9sdXRpb24gd2l0aCBkaWxhdGVkIGtlcm5lbHMuCg==