Dense Neural Network Case Study — Particle Detection

Objective

The goal of this case study was to develop a dense neural network to predict the existence of a new particle from a large dataset provided by the client. The prediction task is binary: 1 for detection and 0 for non-detection. The challenge involved handling over 7 million examples across 28 features, requiring efficient data loading, model architecture design, and accurate performance evaluation through cross-validation.

Data Preparation

  • Input Features: 28 total features, including scientific measurements and a mass variable.
  • Target: Binary class labeled # label (0 = no detection, 1 = detection)
  • Imputation: Not required; dataset was complete with no missing values.
  • Size: 7,000,000 examples, 28 features.
  • Splitting: Replaced all train/test split logic with 5-fold Stratified Cross-Validation using sklearn.model_selection.StratifiedKFold.

Model Configuration

  • Architecture: Dense neural network with the following layers:
    • Input Layer: 28 features
    • Hidden Layers: [500, 400, 300, 200, 100] units, each followed by BatchNormalization and ReLU activation
    • Output Layer: 1 unit with sigmoid activation (float32 to support mixed precision)
  • Loss Function: Binary Crossentropy
  • Optimizer: Adam
  • Precision Policy: Mixed precision (float16) for accelerated performance on A100 GPU
  • Callbacks: EarlyStopping and TensorBoard were configured (though not used in CV)

Hyperparameter Selection (Ablation Study)

  • Tested Batch Sizes: 1000, 2048 (optimal was 1000 for memory balance)
  • Tested Epochs: 1, 3, 5 — Early signs of convergence observed by epoch 3
  • Final Settings:
    • Epochs: 3 per fold
    • Batch size: 1000
    • Activation Function: ReLU
    • Final Output Activation: Sigmoid

Cross-Validation Results (5-Fold Stratified K-Fold)

Fold Accuracy Precision Recall AUC
1 0.8841 0.8737 0.8980 0.8841
2 0.8835 0.8615 0.9140 0.8835
3 0.8836 0.8762 0.8935 0.8836
4 0.8841 0.8698 0.9036 0.8841
5 0.8834 0.8630 0.9116 0.8834

Mean Accuracy: 0.8837
Mean Precision: 0.8688
Mean Recall: 0.9041
Mean AUC: 0.8837

Model Convergence

The model was considered fully trained after 3 epochs per fold, as no significant loss reduction or performance gain was observed beyond that point. This was verified across all folds with stable loss and increasing or plateauing AUC scores.

Figure 1 — Accuracy Over Epochs

This plot shows the training and validation accuracy over 3 epochs. Accuracy increases consistently across both datasets, indicating effective learning and no overfitting.

Figure 2 — Loss Over Epochs

This plot shows the training and validation loss over 3 epochs. Loss decreases steadily, confirming model convergence and generalization.

Results

The model was evaluated using 5-fold stratified cross-validation to ensure generalization without relying on a traditional train/test split.

Fold Performance:

Fold Accuracy Precision Recall AUC
1 0.8841 0.8737 0.8980 0.8841
2 0.8835 0.8615 0.9140 0.8835
3 0.8836 0.8762 0.8935 0.8836
4 0.8841 0.8698 0.9036 0.8841
5 0.8834 0.8630 0.9116 0.8834

Average Metrics: - Accuracy: 0.8837 - Precision: 0.8688 - Recall: 0.9041 - AUC-ROC: 0.8837

The model showed consistently strong performance across all folds, with minimal variance. Validation loss decreased steadily over epochs, and training/validation accuracy tracked closely, indicating no over fitting.

Conclusion

This study implemented a dense neural network to detect the presence of a new particle within a large scientific dataset consisting of over 7 million examples and 28 features. The model was trained using 5-fold stratified cross-validation to ensure generalization and reproducibility.

The final architecture consisted of five hidden layers with Batch Normalization and ReLU activations, culminating in a sigmoid output for binary classification. Mixed precision on an A100 GPU accelerated training while maintaining numerical stability.

Convergence was achieved after only 3 epochs per fold. Across all folds, the model achieved:

  • Accuracy: 0.8837
  • Precision: 0.8688
  • Recall: 0.9041
  • AUC-ROC: 0.8837

Loss declined consistently, and accuracy improved across epochs, confirming model stability. All metrics were reported numerically, and all design choices were justified through iterative tuning and ablation.

This result demonstrates the network’s ability to generalize effectively on large-scale binary classification tasks in the context of particle detection.


APPENDIX:

# -*- coding: utf-8 -*-

    https://colab.research.google.com/drive/12giz1_OhvNpqLowiEf5h6ms9PQszuy4A

initial code
import tensorflow as tf
from tensorflow.keras import datasets, layers, models
import matplotlib.pyplot as plt

from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive
from google.colab import auth
from oauth2client.client import GoogleCredentials

auth.authenticate_user()
gauth = GoogleAuth()
gauth.credentials = GoogleCredentials.get_application_default()
drive = GoogleDrive(gauth)

link = 'https://drive.google.com/file/d/1hJGgFSvtRsNREGPVjkSTLZqOzNc0okgv/view?usp=sharing'
id = link.split('/')[-2]
print(id)

import pandas as pd
from google.colab import drive
drive.mount('/content/drive')

# Enable mixed precision for A100 optimization
from tensorflow.keras import mixed_precision
mixed_precision.set_global_policy('mixed_float16')

path = "/content/drive/MyDrive/all_train.csv"

import tensorflow as tf
import numpy as np

data_file = path
temp_data_set = tf.data.experimental.make_csv_dataset(
    data_file,
    batch_size=1000,
    num_epochs=1,
    label_name='# label',
    ignore_errors=True,
)

def pack(features, label):
    return tf.stack(list(features.values()), axis=-1), tf.cast(label, tf.int32)

packed_dataset = temp_data_set.map(pack)

for features, labels in packed_dataset.take(1):
    print(features.shape)
    print(np.unique(labels.numpy()))
    print(len(features.numpy()))
    print(labels.numpy())

from time import time
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, BatchNormalization
from tensorflow.keras.callbacks import EarlyStopping, TensorBoard

my_model = Sequential()
my_model.add(tf.keras.Input(shape=(28,)))
my_model.add(BatchNormalization())
my_model.add(Dense(500, activation='relu'))
my_model.add(BatchNormalization())
my_model.add(Dense(400, activation='relu'))
my_model.add(BatchNormalization())
my_model.add(Dense(300, activation='relu'))
my_model.add(BatchNormalization())
my_model.add(Dense(200, activation='relu'))
my_model.add(BatchNormalization())
my_model.add(Dense(100, activation='relu'))
my_model.add(BatchNormalization())
my_model.add(Dense(1, activation='sigmoid', dtype='float32'))  # output must be float32

train_size = int(0.8 * 1000)
val_size = int(0.2 * 1000)

full_dataset = packed_dataset.shuffle(buffer_size=1000)
train_dataset = full_dataset.take(train_size)
val_dataset = full_dataset.skip(val_size)

from tensorflow.keras.optimizers import Adam
opt = Adam()

my_model.compile(optimizer=opt, loss=tf.keras.losses.BinaryCrossentropy(), metrics=[
    'accuracy',
    tf.keras.metrics.FalseNegatives(name='false_negatives_1'),
    tf.keras.metrics.FalsePositives(name='false_positives_1'),
    tf.keras.metrics.AUC(name='auc_1'),
    tf.keras.metrics.Precision(name='precision_1'),
    tf.keras.metrics.Recall(name='recall_1')
])

import datetime
log_dir = "logs/fit/" + datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
tb = TensorBoard(log_dir=log_dir, histogram_freq=1)


safety = EarlyStopping(patience=3, monitor='val_loss')

history = my_model.fit(
    packed_dataset.take(train_size),
    validation_data=packed_dataset.skip(train_size),
    epochs=1000,
    callbacks=[safety, tb]
)

print(history.history.keys())

plt.plot(history.history['accuracy'])
plt.plot(history.history['val_accuracy'])
plt.title('model accuracy')
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.legend(['train', 'test'], loc='upper left')
plt.show()

plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('model loss')
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend(['train', 'test'], loc='upper left')
plt.show()

# mount
from google.colab import drive
drive.mount('/content/drive')

# install pydrive2

# Authenticate PyDrive2
from pydrive2.auth import GoogleAuth
from pydrive2.drive import GoogleDrive
from google.colab import auth
from oauth2client.client import GoogleCredentials

auth.authenticate_user()
gauth = GoogleAuth()
gauth.credentials = GoogleCredentials.get_application_default()
drive = GoogleDrive(gauth)

from tensorflow.keras import mixed_precision
mixed_precision.set_global_policy('mixed_float16')

# Enable Mixed Precision (for A100 GPU)
import tensorflow as tf

data_file = "/content/drive/MyDrive/all_train.csv.gz"

temp_data_set = tf.data.experimental.make_csv_dataset(
    data_file,
    compression_type='GZIP',
    batch_size=1000,
    num_epochs=1,
    label_name='# label',
    ignore_errors=True,
)

#load and Stream the GZIP Dataset from Drive
import tensorflow as tf

data_file = "/content/drive/MyDrive/all_train.csv.gz"

temp_data_set = tf.data.experimental.make_csv_dataset(
    data_file,
    compression_type='GZIP',
    batch_size=1000,
    num_epochs=1,
    label_name='# label',
    ignore_errors=True,
)

#  Pack Features into Tensor Format
def pack(features, label):
    return tf.stack(list(features.values()), axis=-1), tf.cast(label, tf.int32)

packed_dataset = temp_data_set.map(pack)

# Preview the Dataset (Optional Sanity Check)
for features, labels in packed_dataset.take(1):
    print(features.shape)  # should be (1000, 28)
    print(tf.reduce_min(labels), tf.reduce_max(labels))  # should be 0 and 1

train_size = 800
val_size = 200

full_dataset = packed_dataset.shuffle(buffer_size=1000)
train_dataset = full_dataset.take(train_size).repeat()
val_dataset = full_dataset.skip(train_size).repeat()

# Build the Neural Network Model
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, BatchNormalization

model = Sequential([
    tf.keras.Input(shape=(28,)),
    BatchNormalization(),
    Dense(500, activation='relu'),
    BatchNormalization(),
    Dense(400, activation='relu'),
    BatchNormalization(),
    Dense(300, activation='relu'),
    BatchNormalization(),
    Dense(200, activation='relu'),
    BatchNormalization(),
    Dense(100, activation='relu'),
    BatchNormalization(),
    Dense(1, activation='sigmoid', dtype='float32')  # force final output to float32
])

# compile
from tensorflow.keras.optimizers import Adam

model.compile(
    optimizer=Adam(),
    loss=tf.keras.losses.BinaryCrossentropy(),
    metrics=[
        'accuracy',
        tf.keras.metrics.FalseNegatives(name='false_negatives_1'),
        tf.keras.metrics.FalsePositives(name='false_positives_1'),
        tf.keras.metrics.AUC(name='auc_1'),
        tf.keras.metrics.Precision(name='precision_1'),
        tf.keras.metrics.Recall(name='recall_1')
    ]
)

# Set Up TensorBoard Logging and Early Stopping
from tensorflow.keras.callbacks import TensorBoard, EarlyStopping
import datetime

log_dir = "logs/fit/" + datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
tb = TensorBoard(log_dir=log_dir, histogram_freq=1)
safety = EarlyStopping(patience=3, monitor='val_loss')

# # train the model
# history = model.fit(
#     train_dataset,
#     validation_data=val_dataset,
#     steps_per_epoch=800,
#     validation_steps=200,
#     epochs=1000,
#     callbacks=[safety, tb]
# )

# # faster - train the model

# history = model.fit(
#     train_dataset,
#     validation_data=val_dataset,
#     steps_per_epoch=800,
#     validation_steps=200,
#     epochs=1000,
#     callbacks=[safety, tb],
#     verbose=0  # fastest screen-wise
# )

# use cv not tts:
import pandas as pd
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import accuracy_score, precision_score, recall_score, roc_auc_score
import numpy as np

# Load full dataset once
df = pd.read_csv("/content/drive/MyDrive/all_train.csv.gz", compression='gzip')
# X = df.drop(columns=['# label', 'mass']).values #(comment out for use w cv - use for tts)

X = df.drop(columns=['# label']).values #(use with CV comment out for tts)

y = df['# label'].values

# K-Fold setup
kf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

all_metrics = []

for fold, (train_idx, val_idx) in enumerate(kf.split(X, y)):
    print(f"\nFOLD {fold + 1}")
    x_train, x_val = X[train_idx], X[val_idx]
    y_train, y_val = y[train_idx], y[val_idx]

    # Build a new model for each fold
    model = Sequential([
        tf.keras.Input(shape=(28,)),
        BatchNormalization(),
        Dense(500, activation='relu'),
        BatchNormalization(),
        Dense(400, activation='relu'),
        BatchNormalization(),
        Dense(300, activation='relu'),
        BatchNormalization(),
        Dense(200, activation='relu'),
        BatchNormalization(),
        Dense(100, activation='relu'),
        BatchNormalization(),
        Dense(1, activation='sigmoid', dtype='float32'),
    ])

    model.compile(
        optimizer=Adam(),
        loss='binary_crossentropy',
        metrics=['accuracy', tf.keras.metrics.AUC(name='auc'), tf.keras.metrics.Precision(), tf.keras.metrics.Recall()]
    )

    model.fit(x_train, y_train, epochs=3, batch_size=1000, verbose=0)

    # Evaluate
    y_pred = model.predict(x_val) > 0.5
    acc = accuracy_score(y_val, y_pred)
    prec = precision_score(y_val, y_pred)
    rec = recall_score(y_val, y_pred)
    auc = roc_auc_score(y_val, y_pred)
    all_metrics.append((acc, prec, rec, auc))
    print(f"ACC: {acc:.4f}  PREC: {prec:.4f}  REC: {rec:.4f}  AUC: {auc:.4f}")

# Fold metrics from output
fold_metrics = [
    (0.8841, 0.8737, 0.8980, 0.8841),
    (0.8835, 0.8615, 0.9140, 0.8835),
    (0.8836, 0.8762, 0.8935, 0.8836),
    (0.8841, 0.8698, 0.9036, 0.8841),
    (0.8834, 0.8630, 0.9116, 0.8834),
]

accs, precs, recs, aucs = zip(*fold_metrics)

print(f"Mean Accuracy:     {np.mean(accs):.4f}")
print(f"Mean Precision:    {np.mean(precs):.4f}")
print(f"Mean Recall:       {np.mean(recs):.4f}")
print(f"Mean AUC:          {np.mean(aucs):.4f}")

# Commented out IPython magic to ensure Python compatibility.
next
#     %load_ext tensorboard
# %tensorboard --logdir logs

#  Plot Training History (Accuracy + Loss)
import matplotlib.pyplot as plt

# Plot accuracy
plt.plot(history.history['accuracy'], label='Train Accuracy')
plt.plot(history.history['val_accuracy'], label='Validation Accuracy')
plt.title('Accuracy over Epochs')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.legend()
plt.grid(True)
plt.show()

# Plot loss
plt.plot(history.history['loss'], label='Train Loss')
plt.plot(history.history['val_loss'], label='Validation Loss')
plt.title('Loss over Epochs')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
plt.grid(True)
plt.show()

history = model.fit(
    train_dataset,
    validation_data=val_dataset,
    steps_per_epoch=800,
    validation_steps=200,
    epochs=3,
    callbacks=[tb]  # Skip early stopping so it finishes all 3 epochs
)

import matplotlib.pyplot as plt

# Accuracy
plt.plot(history.history['accuracy'], label='Train Accuracy')
plt.plot(history.history['val_accuracy'], label='Val Accuracy')
plt.title('Accuracy over Epochs')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.legend()
plt.grid(True)
plt.show()

# Loss
plt.plot(history.history['loss'], label='Train Loss')
plt.plot(history.history['val_loss'], label='Val Loss')
plt.title('Loss over Epochs')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
plt.grid(True)
plt.show()

# evaluate on validaion data:
results = model.evaluate(val_dataset, steps=200)
for name, value in zip(model.metrics_names, results):
    print(f"{name}: {value:.4f}")

# Save the model
model.save('/content/drive/MyDrive/saved_particle_model')

#  Export as a .zip file for download or transfer
import shutil
shutil.make_archive('/content/particle_model_export', 'zip', '/content/drive/MyDrive/saved_particle_model')

#  Generate classification metrics (for the case study)
import numpy as np
from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score

# Get predictions from model on validation data
y_true = []
y_pred = []

for features, labels in val_dataset.take(200):
    preds = model.predict(features)
    y_true.extend(labels.numpy())
    y_pred.extend((preds > 0.5).astype(int).flatten())

# Metrics
print("Confusion Matrix:")
print(confusion_matrix(y_true, y_pred))

print("\nClassification Report:")
print(classification_report(y_true, y_pred, digits=4))

print("\nAUC-ROC:")
print(f"AUC: {roc_auc_score(y_true, y_pred):.4f}")

# Commented out IPython magic to ensure Python compatibility.

import matplotlib.pyplot as plt

# Save accuracy plot
plt.plot(history.history['accuracy'], label='Train Accuracy')
plt.plot(history.history['val_accuracy'], label='Val Accuracy')
plt.title('Accuracy over Epochs')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.legend()
plt.grid(True)
plt.savefig('/content/accuracy_plot.png')
plt.close()

# Save loss plot
plt.plot(history.history['loss'], label='Train Loss')
plt.plot(history.history['val_loss'], label='Val Loss')
plt.title('Loss over Epochs')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
plt.grid(True)
plt.savefig('/content/loss_plot.png')
plt.close()

Appendix: Full Python Code

# mount
from google.colab import drive
drive.mount('/content/drive')
!ls "/content/drive/MyDrive/Colab Notebooks/texaschikkita-particle-detection.ipynb"
!ls "/content/drive/MyDrive/Colab_Notebooks/texaschikkita-particle-detection.ipynb"
# install pydrive2
!pip install -U -q PyDrive2
# Authenticate PyDrive2
from pydrive2.auth import GoogleAuth
from pydrive2.drive import GoogleDrive
from google.colab import auth
from oauth2client.client import GoogleCredentials

auth.authenticate_user()
gauth = GoogleAuth()
gauth.credentials = GoogleCredentials.get_application_default()
drive = GoogleDrive(gauth)
from tensorflow.keras import mixed_precision
mixed_precision.set_global_policy('mixed_float16')
# Enable Mixed Precision (for A100 GPU)
import tensorflow as tf

data_file = "/content/drive/MyDrive/all_train.csv.gz"

temp_data_set = tf.data.experimental.make_csv_dataset(
    data_file,
    compression_type='GZIP',
    batch_size=1000,
    num_epochs=1,
    label_name='# label',
    ignore_errors=True,
)
#load and Stream the GZIP Dataset from Drive
import tensorflow as tf

data_file = "/content/drive/MyDrive/all_train.csv.gz"

temp_data_set = tf.data.experimental.make_csv_dataset(
    data_file,
    compression_type='GZIP',
    batch_size=1000,
    num_epochs=1,
    label_name='# label',
    ignore_errors=True,
)
#  Pack Features into Tensor Format
def pack(features, label):
    return tf.stack(list(features.values()), axis=-1), tf.cast(label, tf.int32)

packed_dataset = temp_data_set.map(pack)
# Preview the Dataset (Optional Sanity Check)
for features, labels in packed_dataset.take(1):
    print(features.shape)  # should be (1000, 28)
    print(tf.reduce_min(labels), tf.reduce_max(labels))  # should be 0 and 1
train_size = 800
val_size = 200

full_dataset = packed_dataset.shuffle(buffer_size=1000)
train_dataset = full_dataset.take(train_size).repeat()
val_dataset = full_dataset.skip(train_size).repeat()
# Build the Neural Network Model
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, BatchNormalization

model = Sequential([
    tf.keras.Input(shape=(28,)),
    BatchNormalization(),
    Dense(500, activation='relu'),
    BatchNormalization(),
    Dense(400, activation='relu'),
    BatchNormalization(),
    Dense(300, activation='relu'),
    BatchNormalization(),
    Dense(200, activation='relu'),
    BatchNormalization(),
    Dense(100, activation='relu'),
    BatchNormalization(),
    Dense(1, activation='sigmoid', dtype='float32')  # force final output to float32
])
# compile
from tensorflow.keras.optimizers import Adam

model.compile(
    optimizer=Adam(),
    loss=tf.keras.losses.BinaryCrossentropy(),
    metrics=[
        'accuracy',
        tf.keras.metrics.FalseNegatives(name='false_negatives_1'),
        tf.keras.metrics.FalsePositives(name='false_positives_1'),
        tf.keras.metrics.AUC(name='auc_1'),
        tf.keras.metrics.Precision(name='precision_1'),
        tf.keras.metrics.Recall(name='recall_1')
    ]
)
# Set Up TensorBoard Logging and Early Stopping
from tensorflow.keras.callbacks import TensorBoard, EarlyStopping
import datetime

log_dir = "logs/fit/" + datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
tb = TensorBoard(log_dir=log_dir, histogram_freq=1)
safety = EarlyStopping(patience=3, monitor='val_loss')
# # train the model
# history = model.fit(
#     train_dataset,
#     validation_data=val_dataset,
#     steps_per_epoch=800,
#     validation_steps=200,
#     epochs=1000,
#     callbacks=[safety, tb]
# )
# # faster - train the model

# history = model.fit(
#     train_dataset,
#     validation_data=val_dataset,
#     steps_per_epoch=800,
#     validation_steps=200,
#     epochs=1000,
#     callbacks=[safety, tb],
#     verbose=0  # fastest screen-wise
# )
# use cv not tts:
import pandas as pd
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import accuracy_score, precision_score, recall_score, roc_auc_score
import numpy as np

# Load full dataset once
df = pd.read_csv("/content/drive/MyDrive/all_train.csv.gz", compression='gzip')
# X = df.drop(columns=['# label', 'mass']).values #(comment out for use w cv - use for tts)

X = df.drop(columns=['# label']).values #(use with CV comment out for tts)

y = df['# label'].values

# K-Fold setup
kf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

all_metrics = []

for fold, (train_idx, val_idx) in enumerate(kf.split(X, y)):
    print(f"\nFOLD {fold + 1}")
    x_train, x_val = X[train_idx], X[val_idx]
    y_train, y_val = y[train_idx], y[val_idx]

    # Build a new model for each fold
    model = Sequential([
        tf.keras.Input(shape=(28,)),
        BatchNormalization(),
        Dense(500, activation='relu'),
        BatchNormalization(),
        Dense(400, activation='relu'),
        BatchNormalization(),
        Dense(300, activation='relu'),
        BatchNormalization(),
        Dense(200, activation='relu'),
        BatchNormalization(),
        Dense(100, activation='relu'),
        BatchNormalization(),
        Dense(1, activation='sigmoid', dtype='float32'),
    ])

    model.compile(
        optimizer=Adam(),
        loss='binary_crossentropy',
        metrics=['accuracy', tf.keras.metrics.AUC(name='auc'), tf.keras.metrics.Precision(), tf.keras.metrics.Recall()]
    )

    model.fit(x_train, y_train, epochs=3, batch_size=1000, verbose=0)

    # Evaluate
    y_pred = model.predict(x_val) > 0.5
    acc = accuracy_score(y_val, y_pred)
    prec = precision_score(y_val, y_pred)
    rec = recall_score(y_val, y_pred)
    auc = roc_auc_score(y_val, y_pred)
    all_metrics.append((acc, prec, rec, auc))
    print(f"ACC: {acc:.4f}  PREC: {prec:.4f}  REC: {rec:.4f}  AUC: {auc:.4f}")
# Fold metrics from output
fold_metrics = [
    (0.8841, 0.8737, 0.8980, 0.8841),
    (0.8835, 0.8615, 0.9140, 0.8835),
    (0.8836, 0.8762, 0.8935, 0.8836),
    (0.8841, 0.8698, 0.9036, 0.8841),
    (0.8834, 0.8630, 0.9116, 0.8834),
]

accs, precs, recs, aucs = zip(*fold_metrics)

print(f"Mean Accuracy:     {np.mean(accs):.4f}")
print(f"Mean Precision:    {np.mean(precs):.4f}")
print(f"Mean Recall:       {np.mean(recs):.4f}")
print(f"Mean AUC:          {np.mean(aucs):.4f}")
next
    %load_ext tensorboard
%tensorboard --logdir logs
#  Plot Training History (Accuracy + Loss)
import matplotlib.pyplot as plt

# Plot accuracy
plt.plot(history.history['accuracy'], label='Train Accuracy')
plt.plot(history.history['val_accuracy'], label='Validation Accuracy')
plt.title('Accuracy over Epochs')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.legend()
plt.grid(True)
plt.show()

# Plot loss
plt.plot(history.history['loss'], label='Train Loss')
plt.plot(history.history['val_loss'], label='Validation Loss')
plt.title('Loss over Epochs')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
plt.grid(True)
plt.show()
history = model.fit(
    train_dataset,
    validation_data=val_dataset,
    steps_per_epoch=800,
    validation_steps=200,
    epochs=3,
    callbacks=[tb]  # Skip early stopping so it finishes all 3 epochs
)
import matplotlib.pyplot as plt

# Accuracy
plt.plot(history.history['accuracy'], label='Train Accuracy')
plt.plot(history.history['val_accuracy'], label='Val Accuracy')
plt.title('Accuracy over Epochs')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.legend()
plt.grid(True)
plt.show()

# Loss
plt.plot(history.history['loss'], label='Train Loss')
plt.plot(history.history['val_loss'], label='Val Loss')
plt.title('Loss over Epochs')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
plt.grid(True)
plt.show()
# evaluate on validaion data:
results = model.evaluate(val_dataset, steps=200)
for name, value in zip(model.metrics_names, results):
    print(f"{name}: {value:.4f}")
# Save the model
model.save('/content/drive/MyDrive/saved_particle_model')
#  Export as a .zip file for download or transfer
import shutil
shutil.make_archive('/content/particle_model_export', 'zip', '/content/drive/MyDrive/saved_particle_model')
#  Generate classification metrics (for the case study)
import numpy as np
from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score

# Get predictions from model on validation data
y_true = []
y_pred = []

for features, labels in val_dataset.take(200):
    preds = model.predict(features)
    y_true.extend(labels.numpy())
    y_pred.extend((preds > 0.5).astype(int).flatten())

# Metrics
print("Confusion Matrix:")
print(confusion_matrix(y_true, y_pred))

print("\nClassification Report:")
print(classification_report(y_true, y_pred, digits=4))

print("\nAUC-ROC:")
print(f"AUC: {roc_auc_score(y_true, y_pred):.4f}")
LS0tDQp0aXRsZTogIlFUVyBDUzYgLSBOTiINCmF1dGhvcjogIkplc3NpY2EgTWNQSGF1bCINCm91dHB1dDogaHRtbF9ub3RlYm9vaw0KICANCi0tLQ0KDQojIERlbnNlIE5ldXJhbCBOZXR3b3JrIENhc2UgU3R1ZHkg4oCUIFBhcnRpY2xlIERldGVjdGlvbg0KDQojIyBPYmplY3RpdmUNCg0KVGhlIGdvYWwgb2YgdGhpcyBjYXNlIHN0dWR5IHdhcyB0byBkZXZlbG9wIGEgZGVuc2UgbmV1cmFsIG5ldHdvcmsgdG8gcHJlZGljdCB0aGUgZXhpc3RlbmNlIG9mIGEgbmV3IHBhcnRpY2xlIGZyb20gYSBsYXJnZSBkYXRhc2V0IHByb3ZpZGVkIGJ5IHRoZSBjbGllbnQuIFRoZSBwcmVkaWN0aW9uIHRhc2sgaXMgYmluYXJ5OiAxIGZvciBkZXRlY3Rpb24gYW5kIDAgZm9yIG5vbi1kZXRlY3Rpb24uIFRoZSBjaGFsbGVuZ2UgaW52b2x2ZWQgaGFuZGxpbmcgb3ZlciA3IG1pbGxpb24gZXhhbXBsZXMgYWNyb3NzIDI4IGZlYXR1cmVzLCByZXF1aXJpbmcgZWZmaWNpZW50IGRhdGEgbG9hZGluZywgbW9kZWwgYXJjaGl0ZWN0dXJlIGRlc2lnbiwgYW5kIGFjY3VyYXRlIHBlcmZvcm1hbmNlIGV2YWx1YXRpb24gdGhyb3VnaCBjcm9zcy12YWxpZGF0aW9uLg0KDQojIyBEYXRhIFByZXBhcmF0aW9uDQoNCi0gICBJbnB1dCBGZWF0dXJlczogMjggdG90YWwgZmVhdHVyZXMsIGluY2x1ZGluZyBzY2llbnRpZmljIG1lYXN1cmVtZW50cyBhbmQgYSBgbWFzc2AgdmFyaWFibGUuDQotICAgVGFyZ2V0OiBCaW5hcnkgY2xhc3MgbGFiZWxlZCBgIyBsYWJlbGAgKDAgPSBubyBkZXRlY3Rpb24sIDEgPSBkZXRlY3Rpb24pDQotICAgSW1wdXRhdGlvbjogTm90IHJlcXVpcmVkOyBkYXRhc2V0IHdhcyBjb21wbGV0ZSB3aXRoIG5vIG1pc3NpbmcgdmFsdWVzLg0KLSAgIFNpemU6IDcsMDAwLDAwMCBleGFtcGxlcywgMjggZmVhdHVyZXMuDQotICAgU3BsaXR0aW5nOiBSZXBsYWNlZCBhbGwgdHJhaW4vdGVzdCBzcGxpdCBsb2dpYyB3aXRoIDUtZm9sZCBTdHJhdGlmaWVkIENyb3NzLVZhbGlkYXRpb24gdXNpbmcgYHNrbGVhcm4ubW9kZWxfc2VsZWN0aW9uLlN0cmF0aWZpZWRLRm9sZGAuDQoNCiMjIE1vZGVsIENvbmZpZ3VyYXRpb24NCg0KLSAgIEFyY2hpdGVjdHVyZTogRGVuc2UgbmV1cmFsIG5ldHdvcmsgd2l0aCB0aGUgZm9sbG93aW5nIGxheWVyczoNCiAgICAtICAgSW5wdXQgTGF5ZXI6IDI4IGZlYXR1cmVzDQogICAgLSAgIEhpZGRlbiBMYXllcnM6IFs1MDAsIDQwMCwgMzAwLCAyMDAsIDEwMF0gdW5pdHMsIGVhY2ggZm9sbG93ZWQgYnkgQmF0Y2hOb3JtYWxpemF0aW9uIGFuZCBSZUxVIGFjdGl2YXRpb24NCiAgICAtICAgT3V0cHV0IExheWVyOiAxIHVuaXQgd2l0aCBzaWdtb2lkIGFjdGl2YXRpb24gKGBmbG9hdDMyYCB0byBzdXBwb3J0IG1peGVkIHByZWNpc2lvbikNCi0gICBMb3NzIEZ1bmN0aW9uOiBCaW5hcnkgQ3Jvc3NlbnRyb3B5DQotICAgT3B0aW1pemVyOiBBZGFtDQotICAgUHJlY2lzaW9uIFBvbGljeTogTWl4ZWQgcHJlY2lzaW9uIChgZmxvYXQxNmApIGZvciBhY2NlbGVyYXRlZCBwZXJmb3JtYW5jZSBvbiBBMTAwIEdQVQ0KLSAgIENhbGxiYWNrczogRWFybHlTdG9wcGluZyBhbmQgVGVuc29yQm9hcmQgd2VyZSBjb25maWd1cmVkICh0aG91Z2ggbm90IHVzZWQgaW4gQ1YpDQoNCiMjIEh5cGVycGFyYW1ldGVyIFNlbGVjdGlvbiAoQWJsYXRpb24gU3R1ZHkpDQoNCi0gICBUZXN0ZWQgQmF0Y2ggU2l6ZXM6IDEwMDAsIDIwNDggKG9wdGltYWwgd2FzIDEwMDAgZm9yIG1lbW9yeSBiYWxhbmNlKQ0KLSAgIFRlc3RlZCBFcG9jaHM6IDEsIDMsIDUg4oCUIEVhcmx5IHNpZ25zIG9mIGNvbnZlcmdlbmNlIG9ic2VydmVkIGJ5IGVwb2NoIDMNCi0gICBGaW5hbCBTZXR0aW5nczoNCiAgICAtICAgRXBvY2hzOiAzIHBlciBmb2xkDQogICAgLSAgIEJhdGNoIHNpemU6IDEwMDANCiAgICAtICAgQWN0aXZhdGlvbiBGdW5jdGlvbjogUmVMVQ0KICAgIC0gICBGaW5hbCBPdXRwdXQgQWN0aXZhdGlvbjogU2lnbW9pZA0KDQojIyBDcm9zcy1WYWxpZGF0aW9uIFJlc3VsdHMgKDUtRm9sZCBTdHJhdGlmaWVkIEstRm9sZCkNCg0KfCBGb2xkIHwgQWNjdXJhY3kgfCBQcmVjaXNpb24gfCBSZWNhbGwgfCBBVUMgICAgfA0KfC0tLS0tLXwtLS0tLS0tLS0tfC0tLS0tLS0tLS0tfC0tLS0tLS0tfC0tLS0tLS0tfA0KfCAxICAgIHwgMC44ODQxICAgfCAwLjg3MzcgICAgfCAwLjg5ODAgfCAwLjg4NDEgfA0KfCAyICAgIHwgMC44ODM1ICAgfCAwLjg2MTUgICAgfCAwLjkxNDAgfCAwLjg4MzUgfA0KfCAzICAgIHwgMC44ODM2ICAgfCAwLjg3NjIgICAgfCAwLjg5MzUgfCAwLjg4MzYgfA0KfCA0ICAgIHwgMC44ODQxICAgfCAwLjg2OTggICAgfCAwLjkwMzYgfCAwLjg4NDEgfA0KfCA1ICAgIHwgMC44ODM0ICAgfCAwLjg2MzAgICAgfCAwLjkxMTYgfCAwLjg4MzQgfA0KDQpNZWFuIEFjY3VyYWN5OiAwLjg4MzdcDQpNZWFuIFByZWNpc2lvbjogMC44Njg4XA0KTWVhbiBSZWNhbGw6IDAuOTA0MVwNCk1lYW4gQVVDOiAwLjg4MzcNCg0KIyMgTW9kZWwgQ29udmVyZ2VuY2UNCg0KVGhlIG1vZGVsIHdhcyBjb25zaWRlcmVkIGZ1bGx5IHRyYWluZWQgYWZ0ZXIgMyBlcG9jaHMgcGVyIGZvbGQsIGFzIG5vIHNpZ25pZmljYW50IGxvc3MgcmVkdWN0aW9uIG9yIHBlcmZvcm1hbmNlIGdhaW4gd2FzIG9ic2VydmVkIGJleW9uZCB0aGF0IHBvaW50LiBUaGlzIHdhcyB2ZXJpZmllZCBhY3Jvc3MgYWxsIGZvbGRzIHdpdGggc3RhYmxlIGxvc3MgYW5kIGluY3JlYXNpbmcgb3IgcGxhdGVhdWluZyBBVUMgc2NvcmVzLg0KDQoNCiMjIyMgKipGaWd1cmUgMSoqIOKAlCBBY2N1cmFjeSBPdmVyIEVwb2Nocw0KDQoqVGhpcyBwbG90IHNob3dzIHRoZSB0cmFpbmluZyBhbmQgdmFsaWRhdGlvbiBhY2N1cmFjeSBvdmVyIDMgZXBvY2hzLiBBY2N1cmFjeSBpbmNyZWFzZXMgY29uc2lzdGVudGx5IGFjcm9zcyBib3RoIGRhdGFzZXRzLCBpbmRpY2F0aW5nIGVmZmVjdGl2ZSBsZWFybmluZyBhbmQgbm8gb3ZlcmZpdHRpbmcuKg0KDQoNCiMjIyMgKipGaWd1cmUgMioqIOKAlCBMb3NzIE92ZXIgRXBvY2hzDQoNCipUaGlzIHBsb3Qgc2hvd3MgdGhlIHRyYWluaW5nIGFuZCB2YWxpZGF0aW9uIGxvc3Mgb3ZlciAzIGVwb2Nocy4gTG9zcyBkZWNyZWFzZXMgc3RlYWRpbHksIGNvbmZpcm1pbmcgbW9kZWwgY29udmVyZ2VuY2UgYW5kIGdlbmVyYWxpemF0aW9uLioNCg0KIyBSZXN1bHRzDQoNClRoZSBtb2RlbCB3YXMgZXZhbHVhdGVkIHVzaW5nIDUtZm9sZCBzdHJhdGlmaWVkIGNyb3NzLXZhbGlkYXRpb24gdG8gZW5zdXJlIGdlbmVyYWxpemF0aW9uIHdpdGhvdXQgcmVseWluZyBvbiBhIHRyYWRpdGlvbmFsIHRyYWluL3Rlc3Qgc3BsaXQuDQoNCiMjIyBGb2xkIFBlcmZvcm1hbmNlOg0KDQp8IEZvbGQgfCBBY2N1cmFjeSB8IFByZWNpc2lvbiB8IFJlY2FsbCB8IEFVQyAgICB8DQp8LS0tLS0tfC0tLS0tLS0tLS18LS0tLS0tLS0tLS18LS0tLS0tLS18LS0tLS0tLS18DQp8IDEgICAgfCAwLjg4NDEgICB8IDAuODczNyAgICB8IDAuODk4MCB8IDAuODg0MSB8DQp8IDIgICAgfCAwLjg4MzUgICB8IDAuODYxNSAgICB8IDAuOTE0MCB8IDAuODgzNSB8DQp8IDMgICAgfCAwLjg4MzYgICB8IDAuODc2MiAgICB8IDAuODkzNSB8IDAuODgzNiB8DQp8IDQgICAgfCAwLjg4NDEgICB8IDAuODY5OCAgICB8IDAuOTAzNiB8IDAuODg0MSB8DQp8IDUgICAgfCAwLjg4MzQgICB8IDAuODYzMCAgICB8IDAuOTExNiB8IDAuODgzNCB8DQoNCioqQXZlcmFnZSBNZXRyaWNzOioqIC0gQWNjdXJhY3k6IGAwLjg4MzdgIC0gUHJlY2lzaW9uOiBgMC44Njg4YCAtIFJlY2FsbDogYDAuOTA0MWAgLSBBVUMtUk9DOiBgMC44ODM3YA0KDQpUaGUgbW9kZWwgc2hvd2VkIGNvbnNpc3RlbnRseSBzdHJvbmcgcGVyZm9ybWFuY2UgYWNyb3NzIGFsbCBmb2xkcywgd2l0aCBtaW5pbWFsIHZhcmlhbmNlLiBWYWxpZGF0aW9uIGxvc3MgZGVjcmVhc2VkIHN0ZWFkaWx5IG92ZXIgZXBvY2hzLCBhbmQgdHJhaW5pbmcvdmFsaWRhdGlvbiBhY2N1cmFjeSB0cmFja2VkIGNsb3NlbHksIGluZGljYXRpbmcgbm8gb3ZlciBmaXR0aW5nLg0KDQojIyAgQ29uY2x1c2lvbg0KDQpUaGlzIHN0dWR5IGltcGxlbWVudGVkIGEgZGVuc2UgbmV1cmFsIG5ldHdvcmsgdG8gZGV0ZWN0IHRoZSBwcmVzZW5jZSBvZiBhIG5ldyBwYXJ0aWNsZSB3aXRoaW4gYSBsYXJnZSBzY2llbnRpZmljIGRhdGFzZXQgY29uc2lzdGluZyBvZiBvdmVyIDcgbWlsbGlvbiBleGFtcGxlcyBhbmQgMjggZmVhdHVyZXMuIFRoZSBtb2RlbCB3YXMgdHJhaW5lZCB1c2luZyA1LWZvbGQgc3RyYXRpZmllZCBjcm9zcy12YWxpZGF0aW9uIHRvIGVuc3VyZSBnZW5lcmFsaXphdGlvbiBhbmQgcmVwcm9kdWNpYmlsaXR5Lg0KDQpUaGUgZmluYWwgYXJjaGl0ZWN0dXJlIGNvbnNpc3RlZCBvZiBmaXZlIGhpZGRlbiBsYXllcnMgd2l0aCBCYXRjaCBOb3JtYWxpemF0aW9uIGFuZCBSZUxVIGFjdGl2YXRpb25zLCBjdWxtaW5hdGluZyBpbiBhIHNpZ21vaWQgb3V0cHV0IGZvciBiaW5hcnkgY2xhc3NpZmljYXRpb24uIE1peGVkIHByZWNpc2lvbiBvbiBhbiBBMTAwIEdQVSBhY2NlbGVyYXRlZCB0cmFpbmluZyB3aGlsZSBtYWludGFpbmluZyBudW1lcmljYWwgc3RhYmlsaXR5Lg0KDQpDb252ZXJnZW5jZSB3YXMgYWNoaWV2ZWQgYWZ0ZXIgb25seSAzIGVwb2NocyBwZXIgZm9sZC4gQWNyb3NzIGFsbCBmb2xkcywgdGhlIG1vZGVsIGFjaGlldmVkOg0KDQotICAgKipBY2N1cmFjeSoqOiBgMC44ODM3YA0KLSAgICoqUHJlY2lzaW9uKio6IGAwLjg2ODhgDQotICAgKipSZWNhbGwqKjogYDAuOTA0MWANCi0gICAqKkFVQy1ST0MqKjogYDAuODgzN2ANCg0KTG9zcyBkZWNsaW5lZCBjb25zaXN0ZW50bHksIGFuZCBhY2N1cmFjeSBpbXByb3ZlZCBhY3Jvc3MgZXBvY2hzLCBjb25maXJtaW5nIG1vZGVsIHN0YWJpbGl0eS4gQWxsIG1ldHJpY3Mgd2VyZSByZXBvcnRlZCBudW1lcmljYWxseSwgYW5kIGFsbCBkZXNpZ24gY2hvaWNlcyB3ZXJlIGp1c3RpZmllZCB0aHJvdWdoIGl0ZXJhdGl2ZSB0dW5pbmcgYW5kIGFibGF0aW9uLg0KDQpUaGlzIHJlc3VsdCBkZW1vbnN0cmF0ZXMgdGhlIG5ldHdvcmvigJlzIGFiaWxpdHkgdG8gZ2VuZXJhbGl6ZSBlZmZlY3RpdmVseSBvbiBsYXJnZS1zY2FsZSBiaW5hcnkgY2xhc3NpZmljYXRpb24gdGFza3MgaW4gdGhlIGNvbnRleHQgb2YgcGFydGljbGUgZGV0ZWN0aW9uLg0KDQotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0NCg0KKipBUFBFTkRJWDoqKg0KDQpgYGB7cHl0aG9ufQ0KIyAtKi0gY29kaW5nOiB1dGYtOCAtKi0NCg0KDQogICAgaHR0cHM6Ly9jb2xhYi5yZXNlYXJjaC5nb29nbGUuY29tL2RyaXZlLzEyZ2l6MV9PaHZOcHFMb3dpRWY1aDZtczlQUXN6dXk0QQ0KDQppbml0aWFsIGNvZGUNCmltcG9ydCB0ZW5zb3JmbG93IGFzIHRmDQpmcm9tIHRlbnNvcmZsb3cua2VyYXMgaW1wb3J0IGRhdGFzZXRzLCBsYXllcnMsIG1vZGVscw0KaW1wb3J0IG1hdHBsb3RsaWIucHlwbG90IGFzIHBsdA0KDQpmcm9tIHB5ZHJpdmUuYXV0aCBpbXBvcnQgR29vZ2xlQXV0aA0KZnJvbSBweWRyaXZlLmRyaXZlIGltcG9ydCBHb29nbGVEcml2ZQ0KZnJvbSBnb29nbGUuY29sYWIgaW1wb3J0IGF1dGgNCmZyb20gb2F1dGgyY2xpZW50LmNsaWVudCBpbXBvcnQgR29vZ2xlQ3JlZGVudGlhbHMNCg0KYXV0aC5hdXRoZW50aWNhdGVfdXNlcigpDQpnYXV0aCA9IEdvb2dsZUF1dGgoKQ0KZ2F1dGguY3JlZGVudGlhbHMgPSBHb29nbGVDcmVkZW50aWFscy5nZXRfYXBwbGljYXRpb25fZGVmYXVsdCgpDQpkcml2ZSA9IEdvb2dsZURyaXZlKGdhdXRoKQ0KDQpsaW5rID0gJ2h0dHBzOi8vZHJpdmUuZ29vZ2xlLmNvbS9maWxlL2QvMWhKR2dGU3Z0UnNOUkVHUFZqa1NUTFpxT3pOYzBva2d2L3ZpZXc/dXNwPXNoYXJpbmcnDQppZCA9IGxpbmsuc3BsaXQoJy8nKVstMl0NCnByaW50KGlkKQ0KDQppbXBvcnQgcGFuZGFzIGFzIHBkDQpmcm9tIGdvb2dsZS5jb2xhYiBpbXBvcnQgZHJpdmUNCmRyaXZlLm1vdW50KCcvY29udGVudC9kcml2ZScpDQoNCiMgRW5hYmxlIG1peGVkIHByZWNpc2lvbiBmb3IgQTEwMCBvcHRpbWl6YXRpb24NCmZyb20gdGVuc29yZmxvdy5rZXJhcyBpbXBvcnQgbWl4ZWRfcHJlY2lzaW9uDQptaXhlZF9wcmVjaXNpb24uc2V0X2dsb2JhbF9wb2xpY3koJ21peGVkX2Zsb2F0MTYnKQ0KDQpwYXRoID0gIi9jb250ZW50L2RyaXZlL015RHJpdmUvYWxsX3RyYWluLmNzdiINCg0KaW1wb3J0IHRlbnNvcmZsb3cgYXMgdGYNCmltcG9ydCBudW1weSBhcyBucA0KDQpkYXRhX2ZpbGUgPSBwYXRoDQp0ZW1wX2RhdGFfc2V0ID0gdGYuZGF0YS5leHBlcmltZW50YWwubWFrZV9jc3ZfZGF0YXNldCgNCiAgICBkYXRhX2ZpbGUsDQogICAgYmF0Y2hfc2l6ZT0xMDAwLA0KICAgIG51bV9lcG9jaHM9MSwNCiAgICBsYWJlbF9uYW1lPScjIGxhYmVsJywNCiAgICBpZ25vcmVfZXJyb3JzPVRydWUsDQopDQoNCmRlZiBwYWNrKGZlYXR1cmVzLCBsYWJlbCk6DQogICAgcmV0dXJuIHRmLnN0YWNrKGxpc3QoZmVhdHVyZXMudmFsdWVzKCkpLCBheGlzPS0xKSwgdGYuY2FzdChsYWJlbCwgdGYuaW50MzIpDQoNCnBhY2tlZF9kYXRhc2V0ID0gdGVtcF9kYXRhX3NldC5tYXAocGFjaykNCg0KZm9yIGZlYXR1cmVzLCBsYWJlbHMgaW4gcGFja2VkX2RhdGFzZXQudGFrZSgxKToNCiAgICBwcmludChmZWF0dXJlcy5zaGFwZSkNCiAgICBwcmludChucC51bmlxdWUobGFiZWxzLm51bXB5KCkpKQ0KICAgIHByaW50KGxlbihmZWF0dXJlcy5udW1weSgpKSkNCiAgICBwcmludChsYWJlbHMubnVtcHkoKSkNCg0KZnJvbSB0aW1lIGltcG9ydCB0aW1lDQpmcm9tIHRlbnNvcmZsb3cua2VyYXMubW9kZWxzIGltcG9ydCBTZXF1ZW50aWFsDQpmcm9tIHRlbnNvcmZsb3cua2VyYXMubGF5ZXJzIGltcG9ydCBEZW5zZSwgQmF0Y2hOb3JtYWxpemF0aW9uDQpmcm9tIHRlbnNvcmZsb3cua2VyYXMuY2FsbGJhY2tzIGltcG9ydCBFYXJseVN0b3BwaW5nLCBUZW5zb3JCb2FyZA0KDQpteV9tb2RlbCA9IFNlcXVlbnRpYWwoKQ0KbXlfbW9kZWwuYWRkKHRmLmtlcmFzLklucHV0KHNoYXBlPSgyOCwpKSkNCm15X21vZGVsLmFkZChCYXRjaE5vcm1hbGl6YXRpb24oKSkNCm15X21vZGVsLmFkZChEZW5zZSg1MDAsIGFjdGl2YXRpb249J3JlbHUnKSkNCm15X21vZGVsLmFkZChCYXRjaE5vcm1hbGl6YXRpb24oKSkNCm15X21vZGVsLmFkZChEZW5zZSg0MDAsIGFjdGl2YXRpb249J3JlbHUnKSkNCm15X21vZGVsLmFkZChCYXRjaE5vcm1hbGl6YXRpb24oKSkNCm15X21vZGVsLmFkZChEZW5zZSgzMDAsIGFjdGl2YXRpb249J3JlbHUnKSkNCm15X21vZGVsLmFkZChCYXRjaE5vcm1hbGl6YXRpb24oKSkNCm15X21vZGVsLmFkZChEZW5zZSgyMDAsIGFjdGl2YXRpb249J3JlbHUnKSkNCm15X21vZGVsLmFkZChCYXRjaE5vcm1hbGl6YXRpb24oKSkNCm15X21vZGVsLmFkZChEZW5zZSgxMDAsIGFjdGl2YXRpb249J3JlbHUnKSkNCm15X21vZGVsLmFkZChCYXRjaE5vcm1hbGl6YXRpb24oKSkNCm15X21vZGVsLmFkZChEZW5zZSgxLCBhY3RpdmF0aW9uPSdzaWdtb2lkJywgZHR5cGU9J2Zsb2F0MzInKSkgICMgb3V0cHV0IG11c3QgYmUgZmxvYXQzMg0KDQp0cmFpbl9zaXplID0gaW50KDAuOCAqIDEwMDApDQp2YWxfc2l6ZSA9IGludCgwLjIgKiAxMDAwKQ0KDQpmdWxsX2RhdGFzZXQgPSBwYWNrZWRfZGF0YXNldC5zaHVmZmxlKGJ1ZmZlcl9zaXplPTEwMDApDQp0cmFpbl9kYXRhc2V0ID0gZnVsbF9kYXRhc2V0LnRha2UodHJhaW5fc2l6ZSkNCnZhbF9kYXRhc2V0ID0gZnVsbF9kYXRhc2V0LnNraXAodmFsX3NpemUpDQoNCmZyb20gdGVuc29yZmxvdy5rZXJhcy5vcHRpbWl6ZXJzIGltcG9ydCBBZGFtDQpvcHQgPSBBZGFtKCkNCg0KbXlfbW9kZWwuY29tcGlsZShvcHRpbWl6ZXI9b3B0LCBsb3NzPXRmLmtlcmFzLmxvc3Nlcy5CaW5hcnlDcm9zc2VudHJvcHkoKSwgbWV0cmljcz1bDQogICAgJ2FjY3VyYWN5JywNCiAgICB0Zi5rZXJhcy5tZXRyaWNzLkZhbHNlTmVnYXRpdmVzKG5hbWU9J2ZhbHNlX25lZ2F0aXZlc18xJyksDQogICAgdGYua2VyYXMubWV0cmljcy5GYWxzZVBvc2l0aXZlcyhuYW1lPSdmYWxzZV9wb3NpdGl2ZXNfMScpLA0KICAgIHRmLmtlcmFzLm1ldHJpY3MuQVVDKG5hbWU9J2F1Y18xJyksDQogICAgdGYua2VyYXMubWV0cmljcy5QcmVjaXNpb24obmFtZT0ncHJlY2lzaW9uXzEnKSwNCiAgICB0Zi5rZXJhcy5tZXRyaWNzLlJlY2FsbChuYW1lPSdyZWNhbGxfMScpDQpdKQ0KDQppbXBvcnQgZGF0ZXRpbWUNCmxvZ19kaXIgPSAibG9ncy9maXQvIiArIGRhdGV0aW1lLmRhdGV0aW1lLm5vdygpLnN0cmZ0aW1lKCIlWSVtJWQtJUglTSVTIikNCnRiID0gVGVuc29yQm9hcmQobG9nX2Rpcj1sb2dfZGlyLCBoaXN0b2dyYW1fZnJlcT0xKQ0KDQoNCnNhZmV0eSA9IEVhcmx5U3RvcHBpbmcocGF0aWVuY2U9MywgbW9uaXRvcj0ndmFsX2xvc3MnKQ0KDQpoaXN0b3J5ID0gbXlfbW9kZWwuZml0KA0KICAgIHBhY2tlZF9kYXRhc2V0LnRha2UodHJhaW5fc2l6ZSksDQogICAgdmFsaWRhdGlvbl9kYXRhPXBhY2tlZF9kYXRhc2V0LnNraXAodHJhaW5fc2l6ZSksDQogICAgZXBvY2hzPTEwMDAsDQogICAgY2FsbGJhY2tzPVtzYWZldHksIHRiXQ0KKQ0KDQpwcmludChoaXN0b3J5Lmhpc3Rvcnkua2V5cygpKQ0KDQpwbHQucGxvdChoaXN0b3J5Lmhpc3RvcnlbJ2FjY3VyYWN5J10pDQpwbHQucGxvdChoaXN0b3J5Lmhpc3RvcnlbJ3ZhbF9hY2N1cmFjeSddKQ0KcGx0LnRpdGxlKCdtb2RlbCBhY2N1cmFjeScpDQpwbHQueWxhYmVsKCdhY2N1cmFjeScpDQpwbHQueGxhYmVsKCdlcG9jaCcpDQpwbHQubGVnZW5kKFsndHJhaW4nLCAndGVzdCddLCBsb2M9J3VwcGVyIGxlZnQnKQ0KcGx0LnNob3coKQ0KDQpwbHQucGxvdChoaXN0b3J5Lmhpc3RvcnlbJ2xvc3MnXSkNCnBsdC5wbG90KGhpc3RvcnkuaGlzdG9yeVsndmFsX2xvc3MnXSkNCnBsdC50aXRsZSgnbW9kZWwgbG9zcycpDQpwbHQueWxhYmVsKCdsb3NzJykNCnBsdC54bGFiZWwoJ2Vwb2NoJykNCnBsdC5sZWdlbmQoWyd0cmFpbicsICd0ZXN0J10sIGxvYz0ndXBwZXIgbGVmdCcpDQpwbHQuc2hvdygpDQoNCiMgbW91bnQNCmZyb20gZ29vZ2xlLmNvbGFiIGltcG9ydCBkcml2ZQ0KZHJpdmUubW91bnQoJy9jb250ZW50L2RyaXZlJykNCg0KIyBpbnN0YWxsIHB5ZHJpdmUyDQoNCiMgQXV0aGVudGljYXRlIFB5RHJpdmUyDQpmcm9tIHB5ZHJpdmUyLmF1dGggaW1wb3J0IEdvb2dsZUF1dGgNCmZyb20gcHlkcml2ZTIuZHJpdmUgaW1wb3J0IEdvb2dsZURyaXZlDQpmcm9tIGdvb2dsZS5jb2xhYiBpbXBvcnQgYXV0aA0KZnJvbSBvYXV0aDJjbGllbnQuY2xpZW50IGltcG9ydCBHb29nbGVDcmVkZW50aWFscw0KDQphdXRoLmF1dGhlbnRpY2F0ZV91c2VyKCkNCmdhdXRoID0gR29vZ2xlQXV0aCgpDQpnYXV0aC5jcmVkZW50aWFscyA9IEdvb2dsZUNyZWRlbnRpYWxzLmdldF9hcHBsaWNhdGlvbl9kZWZhdWx0KCkNCmRyaXZlID0gR29vZ2xlRHJpdmUoZ2F1dGgpDQoNCmZyb20gdGVuc29yZmxvdy5rZXJhcyBpbXBvcnQgbWl4ZWRfcHJlY2lzaW9uDQptaXhlZF9wcmVjaXNpb24uc2V0X2dsb2JhbF9wb2xpY3koJ21peGVkX2Zsb2F0MTYnKQ0KDQojIEVuYWJsZSBNaXhlZCBQcmVjaXNpb24gKGZvciBBMTAwIEdQVSkNCmltcG9ydCB0ZW5zb3JmbG93IGFzIHRmDQoNCmRhdGFfZmlsZSA9ICIvY29udGVudC9kcml2ZS9NeURyaXZlL2FsbF90cmFpbi5jc3YuZ3oiDQoNCnRlbXBfZGF0YV9zZXQgPSB0Zi5kYXRhLmV4cGVyaW1lbnRhbC5tYWtlX2Nzdl9kYXRhc2V0KA0KICAgIGRhdGFfZmlsZSwNCiAgICBjb21wcmVzc2lvbl90eXBlPSdHWklQJywNCiAgICBiYXRjaF9zaXplPTEwMDAsDQogICAgbnVtX2Vwb2Nocz0xLA0KICAgIGxhYmVsX25hbWU9JyMgbGFiZWwnLA0KICAgIGlnbm9yZV9lcnJvcnM9VHJ1ZSwNCikNCg0KI2xvYWQgYW5kIFN0cmVhbSB0aGUgR1pJUCBEYXRhc2V0IGZyb20gRHJpdmUNCmltcG9ydCB0ZW5zb3JmbG93IGFzIHRmDQoNCmRhdGFfZmlsZSA9ICIvY29udGVudC9kcml2ZS9NeURyaXZlL2FsbF90cmFpbi5jc3YuZ3oiDQoNCnRlbXBfZGF0YV9zZXQgPSB0Zi5kYXRhLmV4cGVyaW1lbnRhbC5tYWtlX2Nzdl9kYXRhc2V0KA0KICAgIGRhdGFfZmlsZSwNCiAgICBjb21wcmVzc2lvbl90eXBlPSdHWklQJywNCiAgICBiYXRjaF9zaXplPTEwMDAsDQogICAgbnVtX2Vwb2Nocz0xLA0KICAgIGxhYmVsX25hbWU9JyMgbGFiZWwnLA0KICAgIGlnbm9yZV9lcnJvcnM9VHJ1ZSwNCikNCg0KIyAgUGFjayBGZWF0dXJlcyBpbnRvIFRlbnNvciBGb3JtYXQNCmRlZiBwYWNrKGZlYXR1cmVzLCBsYWJlbCk6DQogICAgcmV0dXJuIHRmLnN0YWNrKGxpc3QoZmVhdHVyZXMudmFsdWVzKCkpLCBheGlzPS0xKSwgdGYuY2FzdChsYWJlbCwgdGYuaW50MzIpDQoNCnBhY2tlZF9kYXRhc2V0ID0gdGVtcF9kYXRhX3NldC5tYXAocGFjaykNCg0KIyBQcmV2aWV3IHRoZSBEYXRhc2V0IChPcHRpb25hbCBTYW5pdHkgQ2hlY2spDQpmb3IgZmVhdHVyZXMsIGxhYmVscyBpbiBwYWNrZWRfZGF0YXNldC50YWtlKDEpOg0KICAgIHByaW50KGZlYXR1cmVzLnNoYXBlKSAgIyBzaG91bGQgYmUgKDEwMDAsIDI4KQ0KICAgIHByaW50KHRmLnJlZHVjZV9taW4obGFiZWxzKSwgdGYucmVkdWNlX21heChsYWJlbHMpKSAgIyBzaG91bGQgYmUgMCBhbmQgMQ0KDQp0cmFpbl9zaXplID0gODAwDQp2YWxfc2l6ZSA9IDIwMA0KDQpmdWxsX2RhdGFzZXQgPSBwYWNrZWRfZGF0YXNldC5zaHVmZmxlKGJ1ZmZlcl9zaXplPTEwMDApDQp0cmFpbl9kYXRhc2V0ID0gZnVsbF9kYXRhc2V0LnRha2UodHJhaW5fc2l6ZSkucmVwZWF0KCkNCnZhbF9kYXRhc2V0ID0gZnVsbF9kYXRhc2V0LnNraXAodHJhaW5fc2l6ZSkucmVwZWF0KCkNCg0KIyBCdWlsZCB0aGUgTmV1cmFsIE5ldHdvcmsgTW9kZWwNCmZyb20gdGVuc29yZmxvdy5rZXJhcy5tb2RlbHMgaW1wb3J0IFNlcXVlbnRpYWwNCmZyb20gdGVuc29yZmxvdy5rZXJhcy5sYXllcnMgaW1wb3J0IERlbnNlLCBCYXRjaE5vcm1hbGl6YXRpb24NCg0KbW9kZWwgPSBTZXF1ZW50aWFsKFsNCiAgICB0Zi5rZXJhcy5JbnB1dChzaGFwZT0oMjgsKSksDQogICAgQmF0Y2hOb3JtYWxpemF0aW9uKCksDQogICAgRGVuc2UoNTAwLCBhY3RpdmF0aW9uPSdyZWx1JyksDQogICAgQmF0Y2hOb3JtYWxpemF0aW9uKCksDQogICAgRGVuc2UoNDAwLCBhY3RpdmF0aW9uPSdyZWx1JyksDQogICAgQmF0Y2hOb3JtYWxpemF0aW9uKCksDQogICAgRGVuc2UoMzAwLCBhY3RpdmF0aW9uPSdyZWx1JyksDQogICAgQmF0Y2hOb3JtYWxpemF0aW9uKCksDQogICAgRGVuc2UoMjAwLCBhY3RpdmF0aW9uPSdyZWx1JyksDQogICAgQmF0Y2hOb3JtYWxpemF0aW9uKCksDQogICAgRGVuc2UoMTAwLCBhY3RpdmF0aW9uPSdyZWx1JyksDQogICAgQmF0Y2hOb3JtYWxpemF0aW9uKCksDQogICAgRGVuc2UoMSwgYWN0aXZhdGlvbj0nc2lnbW9pZCcsIGR0eXBlPSdmbG9hdDMyJykgICMgZm9yY2UgZmluYWwgb3V0cHV0IHRvIGZsb2F0MzINCl0pDQoNCiMgY29tcGlsZQ0KZnJvbSB0ZW5zb3JmbG93LmtlcmFzLm9wdGltaXplcnMgaW1wb3J0IEFkYW0NCg0KbW9kZWwuY29tcGlsZSgNCiAgICBvcHRpbWl6ZXI9QWRhbSgpLA0KICAgIGxvc3M9dGYua2VyYXMubG9zc2VzLkJpbmFyeUNyb3NzZW50cm9weSgpLA0KICAgIG1ldHJpY3M9Ww0KICAgICAgICAnYWNjdXJhY3knLA0KICAgICAgICB0Zi5rZXJhcy5tZXRyaWNzLkZhbHNlTmVnYXRpdmVzKG5hbWU9J2ZhbHNlX25lZ2F0aXZlc18xJyksDQogICAgICAgIHRmLmtlcmFzLm1ldHJpY3MuRmFsc2VQb3NpdGl2ZXMobmFtZT0nZmFsc2VfcG9zaXRpdmVzXzEnKSwNCiAgICAgICAgdGYua2VyYXMubWV0cmljcy5BVUMobmFtZT0nYXVjXzEnKSwNCiAgICAgICAgdGYua2VyYXMubWV0cmljcy5QcmVjaXNpb24obmFtZT0ncHJlY2lzaW9uXzEnKSwNCiAgICAgICAgdGYua2VyYXMubWV0cmljcy5SZWNhbGwobmFtZT0ncmVjYWxsXzEnKQ0KICAgIF0NCikNCg0KIyBTZXQgVXAgVGVuc29yQm9hcmQgTG9nZ2luZyBhbmQgRWFybHkgU3RvcHBpbmcNCmZyb20gdGVuc29yZmxvdy5rZXJhcy5jYWxsYmFja3MgaW1wb3J0IFRlbnNvckJvYXJkLCBFYXJseVN0b3BwaW5nDQppbXBvcnQgZGF0ZXRpbWUNCg0KbG9nX2RpciA9ICJsb2dzL2ZpdC8iICsgZGF0ZXRpbWUuZGF0ZXRpbWUubm93KCkuc3RyZnRpbWUoIiVZJW0lZC0lSCVNJVMiKQ0KdGIgPSBUZW5zb3JCb2FyZChsb2dfZGlyPWxvZ19kaXIsIGhpc3RvZ3JhbV9mcmVxPTEpDQpzYWZldHkgPSBFYXJseVN0b3BwaW5nKHBhdGllbmNlPTMsIG1vbml0b3I9J3ZhbF9sb3NzJykNCg0KIyAjIHRyYWluIHRoZSBtb2RlbA0KIyBoaXN0b3J5ID0gbW9kZWwuZml0KA0KIyAgICAgdHJhaW5fZGF0YXNldCwNCiMgICAgIHZhbGlkYXRpb25fZGF0YT12YWxfZGF0YXNldCwNCiMgICAgIHN0ZXBzX3Blcl9lcG9jaD04MDAsDQojICAgICB2YWxpZGF0aW9uX3N0ZXBzPTIwMCwNCiMgICAgIGVwb2Nocz0xMDAwLA0KIyAgICAgY2FsbGJhY2tzPVtzYWZldHksIHRiXQ0KIyApDQoNCiMgIyBmYXN0ZXIgLSB0cmFpbiB0aGUgbW9kZWwNCg0KIyBoaXN0b3J5ID0gbW9kZWwuZml0KA0KIyAgICAgdHJhaW5fZGF0YXNldCwNCiMgICAgIHZhbGlkYXRpb25fZGF0YT12YWxfZGF0YXNldCwNCiMgICAgIHN0ZXBzX3Blcl9lcG9jaD04MDAsDQojICAgICB2YWxpZGF0aW9uX3N0ZXBzPTIwMCwNCiMgICAgIGVwb2Nocz0xMDAwLA0KIyAgICAgY2FsbGJhY2tzPVtzYWZldHksIHRiXSwNCiMgICAgIHZlcmJvc2U9MCAgIyBmYXN0ZXN0IHNjcmVlbi13aXNlDQojICkNCg0KIyB1c2UgY3Ygbm90IHR0czoNCmltcG9ydCBwYW5kYXMgYXMgcGQNCmZyb20gc2tsZWFybi5tb2RlbF9zZWxlY3Rpb24gaW1wb3J0IFN0cmF0aWZpZWRLRm9sZA0KZnJvbSBza2xlYXJuLm1ldHJpY3MgaW1wb3J0IGFjY3VyYWN5X3Njb3JlLCBwcmVjaXNpb25fc2NvcmUsIHJlY2FsbF9zY29yZSwgcm9jX2F1Y19zY29yZQ0KaW1wb3J0IG51bXB5IGFzIG5wDQoNCiMgTG9hZCBmdWxsIGRhdGFzZXQgb25jZQ0KZGYgPSBwZC5yZWFkX2NzdigiL2NvbnRlbnQvZHJpdmUvTXlEcml2ZS9hbGxfdHJhaW4uY3N2Lmd6IiwgY29tcHJlc3Npb249J2d6aXAnKQ0KIyBYID0gZGYuZHJvcChjb2x1bW5zPVsnIyBsYWJlbCcsICdtYXNzJ10pLnZhbHVlcyAjKGNvbW1lbnQgb3V0IGZvciB1c2UgdyBjdiAtIHVzZSBmb3IgdHRzKQ0KDQpYID0gZGYuZHJvcChjb2x1bW5zPVsnIyBsYWJlbCddKS52YWx1ZXMgIyh1c2Ugd2l0aCBDViBjb21tZW50IG91dCBmb3IgdHRzKQ0KDQp5ID0gZGZbJyMgbGFiZWwnXS52YWx1ZXMNCg0KIyBLLUZvbGQgc2V0dXANCmtmID0gU3RyYXRpZmllZEtGb2xkKG5fc3BsaXRzPTUsIHNodWZmbGU9VHJ1ZSwgcmFuZG9tX3N0YXRlPTQyKQ0KDQphbGxfbWV0cmljcyA9IFtdDQoNCmZvciBmb2xkLCAodHJhaW5faWR4LCB2YWxfaWR4KSBpbiBlbnVtZXJhdGUoa2Yuc3BsaXQoWCwgeSkpOg0KICAgIHByaW50KGYiXG5GT0xEIHtmb2xkICsgMX0iKQ0KICAgIHhfdHJhaW4sIHhfdmFsID0gWFt0cmFpbl9pZHhdLCBYW3ZhbF9pZHhdDQogICAgeV90cmFpbiwgeV92YWwgPSB5W3RyYWluX2lkeF0sIHlbdmFsX2lkeF0NCg0KICAgICMgQnVpbGQgYSBuZXcgbW9kZWwgZm9yIGVhY2ggZm9sZA0KICAgIG1vZGVsID0gU2VxdWVudGlhbChbDQogICAgICAgIHRmLmtlcmFzLklucHV0KHNoYXBlPSgyOCwpKSwNCiAgICAgICAgQmF0Y2hOb3JtYWxpemF0aW9uKCksDQogICAgICAgIERlbnNlKDUwMCwgYWN0aXZhdGlvbj0ncmVsdScpLA0KICAgICAgICBCYXRjaE5vcm1hbGl6YXRpb24oKSwNCiAgICAgICAgRGVuc2UoNDAwLCBhY3RpdmF0aW9uPSdyZWx1JyksDQogICAgICAgIEJhdGNoTm9ybWFsaXphdGlvbigpLA0KICAgICAgICBEZW5zZSgzMDAsIGFjdGl2YXRpb249J3JlbHUnKSwNCiAgICAgICAgQmF0Y2hOb3JtYWxpemF0aW9uKCksDQogICAgICAgIERlbnNlKDIwMCwgYWN0aXZhdGlvbj0ncmVsdScpLA0KICAgICAgICBCYXRjaE5vcm1hbGl6YXRpb24oKSwNCiAgICAgICAgRGVuc2UoMTAwLCBhY3RpdmF0aW9uPSdyZWx1JyksDQogICAgICAgIEJhdGNoTm9ybWFsaXphdGlvbigpLA0KICAgICAgICBEZW5zZSgxLCBhY3RpdmF0aW9uPSdzaWdtb2lkJywgZHR5cGU9J2Zsb2F0MzInKSwNCiAgICBdKQ0KDQogICAgbW9kZWwuY29tcGlsZSgNCiAgICAgICAgb3B0aW1pemVyPUFkYW0oKSwNCiAgICAgICAgbG9zcz0nYmluYXJ5X2Nyb3NzZW50cm9weScsDQogICAgICAgIG1ldHJpY3M9WydhY2N1cmFjeScsIHRmLmtlcmFzLm1ldHJpY3MuQVVDKG5hbWU9J2F1YycpLCB0Zi5rZXJhcy5tZXRyaWNzLlByZWNpc2lvbigpLCB0Zi5rZXJhcy5tZXRyaWNzLlJlY2FsbCgpXQ0KICAgICkNCg0KICAgIG1vZGVsLmZpdCh4X3RyYWluLCB5X3RyYWluLCBlcG9jaHM9MywgYmF0Y2hfc2l6ZT0xMDAwLCB2ZXJib3NlPTApDQoNCiAgICAjIEV2YWx1YXRlDQogICAgeV9wcmVkID0gbW9kZWwucHJlZGljdCh4X3ZhbCkgPiAwLjUNCiAgICBhY2MgPSBhY2N1cmFjeV9zY29yZSh5X3ZhbCwgeV9wcmVkKQ0KICAgIHByZWMgPSBwcmVjaXNpb25fc2NvcmUoeV92YWwsIHlfcHJlZCkNCiAgICByZWMgPSByZWNhbGxfc2NvcmUoeV92YWwsIHlfcHJlZCkNCiAgICBhdWMgPSByb2NfYXVjX3Njb3JlKHlfdmFsLCB5X3ByZWQpDQogICAgYWxsX21ldHJpY3MuYXBwZW5kKChhY2MsIHByZWMsIHJlYywgYXVjKSkNCiAgICBwcmludChmIkFDQzoge2FjYzouNGZ9ICBQUkVDOiB7cHJlYzouNGZ9ICBSRUM6IHtyZWM6LjRmfSAgQVVDOiB7YXVjOi40Zn0iKQ0KDQojIEZvbGQgbWV0cmljcyBmcm9tIG91dHB1dA0KZm9sZF9tZXRyaWNzID0gWw0KICAgICgwLjg4NDEsIDAuODczNywgMC44OTgwLCAwLjg4NDEpLA0KICAgICgwLjg4MzUsIDAuODYxNSwgMC45MTQwLCAwLjg4MzUpLA0KICAgICgwLjg4MzYsIDAuODc2MiwgMC44OTM1LCAwLjg4MzYpLA0KICAgICgwLjg4NDEsIDAuODY5OCwgMC45MDM2LCAwLjg4NDEpLA0KICAgICgwLjg4MzQsIDAuODYzMCwgMC45MTE2LCAwLjg4MzQpLA0KXQ0KDQphY2NzLCBwcmVjcywgcmVjcywgYXVjcyA9IHppcCgqZm9sZF9tZXRyaWNzKQ0KDQpwcmludChmIk1lYW4gQWNjdXJhY3k6ICAgICB7bnAubWVhbihhY2NzKTouNGZ9IikNCnByaW50KGYiTWVhbiBQcmVjaXNpb246ICAgIHtucC5tZWFuKHByZWNzKTouNGZ9IikNCnByaW50KGYiTWVhbiBSZWNhbGw6ICAgICAgIHtucC5tZWFuKHJlY3MpOi40Zn0iKQ0KcHJpbnQoZiJNZWFuIEFVQzogICAgICAgICAge25wLm1lYW4oYXVjcyk6LjRmfSIpDQoNCiMgQ29tbWVudGVkIG91dCBJUHl0aG9uIG1hZ2ljIHRvIGVuc3VyZSBQeXRob24gY29tcGF0aWJpbGl0eS4NCm5leHQNCiMgICAgICVsb2FkX2V4dCB0ZW5zb3Jib2FyZA0KIyAldGVuc29yYm9hcmQgLS1sb2dkaXIgbG9ncw0KDQojICBQbG90IFRyYWluaW5nIEhpc3RvcnkgKEFjY3VyYWN5ICsgTG9zcykNCmltcG9ydCBtYXRwbG90bGliLnB5cGxvdCBhcyBwbHQNCg0KIyBQbG90IGFjY3VyYWN5DQpwbHQucGxvdChoaXN0b3J5Lmhpc3RvcnlbJ2FjY3VyYWN5J10sIGxhYmVsPSdUcmFpbiBBY2N1cmFjeScpDQpwbHQucGxvdChoaXN0b3J5Lmhpc3RvcnlbJ3ZhbF9hY2N1cmFjeSddLCBsYWJlbD0nVmFsaWRhdGlvbiBBY2N1cmFjeScpDQpwbHQudGl0bGUoJ0FjY3VyYWN5IG92ZXIgRXBvY2hzJykNCnBsdC54bGFiZWwoJ0Vwb2NoJykNCnBsdC55bGFiZWwoJ0FjY3VyYWN5JykNCnBsdC5sZWdlbmQoKQ0KcGx0LmdyaWQoVHJ1ZSkNCnBsdC5zaG93KCkNCg0KIyBQbG90IGxvc3MNCnBsdC5wbG90KGhpc3RvcnkuaGlzdG9yeVsnbG9zcyddLCBsYWJlbD0nVHJhaW4gTG9zcycpDQpwbHQucGxvdChoaXN0b3J5Lmhpc3RvcnlbJ3ZhbF9sb3NzJ10sIGxhYmVsPSdWYWxpZGF0aW9uIExvc3MnKQ0KcGx0LnRpdGxlKCdMb3NzIG92ZXIgRXBvY2hzJykNCnBsdC54bGFiZWwoJ0Vwb2NoJykNCnBsdC55bGFiZWwoJ0xvc3MnKQ0KcGx0LmxlZ2VuZCgpDQpwbHQuZ3JpZChUcnVlKQ0KcGx0LnNob3coKQ0KDQpoaXN0b3J5ID0gbW9kZWwuZml0KA0KICAgIHRyYWluX2RhdGFzZXQsDQogICAgdmFsaWRhdGlvbl9kYXRhPXZhbF9kYXRhc2V0LA0KICAgIHN0ZXBzX3Blcl9lcG9jaD04MDAsDQogICAgdmFsaWRhdGlvbl9zdGVwcz0yMDAsDQogICAgZXBvY2hzPTMsDQogICAgY2FsbGJhY2tzPVt0Yl0gICMgU2tpcCBlYXJseSBzdG9wcGluZyBzbyBpdCBmaW5pc2hlcyBhbGwgMyBlcG9jaHMNCikNCg0KaW1wb3J0IG1hdHBsb3RsaWIucHlwbG90IGFzIHBsdA0KDQojIEFjY3VyYWN5DQpwbHQucGxvdChoaXN0b3J5Lmhpc3RvcnlbJ2FjY3VyYWN5J10sIGxhYmVsPSdUcmFpbiBBY2N1cmFjeScpDQpwbHQucGxvdChoaXN0b3J5Lmhpc3RvcnlbJ3ZhbF9hY2N1cmFjeSddLCBsYWJlbD0nVmFsIEFjY3VyYWN5JykNCnBsdC50aXRsZSgnQWNjdXJhY3kgb3ZlciBFcG9jaHMnKQ0KcGx0LnhsYWJlbCgnRXBvY2gnKQ0KcGx0LnlsYWJlbCgnQWNjdXJhY3knKQ0KcGx0LmxlZ2VuZCgpDQpwbHQuZ3JpZChUcnVlKQ0KcGx0LnNob3coKQ0KDQojIExvc3MNCnBsdC5wbG90KGhpc3RvcnkuaGlzdG9yeVsnbG9zcyddLCBsYWJlbD0nVHJhaW4gTG9zcycpDQpwbHQucGxvdChoaXN0b3J5Lmhpc3RvcnlbJ3ZhbF9sb3NzJ10sIGxhYmVsPSdWYWwgTG9zcycpDQpwbHQudGl0bGUoJ0xvc3Mgb3ZlciBFcG9jaHMnKQ0KcGx0LnhsYWJlbCgnRXBvY2gnKQ0KcGx0LnlsYWJlbCgnTG9zcycpDQpwbHQubGVnZW5kKCkNCnBsdC5ncmlkKFRydWUpDQpwbHQuc2hvdygpDQoNCiMgZXZhbHVhdGUgb24gdmFsaWRhaW9uIGRhdGE6DQpyZXN1bHRzID0gbW9kZWwuZXZhbHVhdGUodmFsX2RhdGFzZXQsIHN0ZXBzPTIwMCkNCmZvciBuYW1lLCB2YWx1ZSBpbiB6aXAobW9kZWwubWV0cmljc19uYW1lcywgcmVzdWx0cyk6DQogICAgcHJpbnQoZiJ7bmFtZX06IHt2YWx1ZTouNGZ9IikNCg0KIyBTYXZlIHRoZSBtb2RlbA0KbW9kZWwuc2F2ZSgnL2NvbnRlbnQvZHJpdmUvTXlEcml2ZS9zYXZlZF9wYXJ0aWNsZV9tb2RlbCcpDQoNCiMgIEV4cG9ydCBhcyBhIC56aXAgZmlsZSBmb3IgZG93bmxvYWQgb3IgdHJhbnNmZXINCmltcG9ydCBzaHV0aWwNCnNodXRpbC5tYWtlX2FyY2hpdmUoJy9jb250ZW50L3BhcnRpY2xlX21vZGVsX2V4cG9ydCcsICd6aXAnLCAnL2NvbnRlbnQvZHJpdmUvTXlEcml2ZS9zYXZlZF9wYXJ0aWNsZV9tb2RlbCcpDQoNCiMgIEdlbmVyYXRlIGNsYXNzaWZpY2F0aW9uIG1ldHJpY3MgKGZvciB0aGUgY2FzZSBzdHVkeSkNCmltcG9ydCBudW1weSBhcyBucA0KZnJvbSBza2xlYXJuLm1ldHJpY3MgaW1wb3J0IGNsYXNzaWZpY2F0aW9uX3JlcG9ydCwgY29uZnVzaW9uX21hdHJpeCwgcm9jX2F1Y19zY29yZQ0KDQojIEdldCBwcmVkaWN0aW9ucyBmcm9tIG1vZGVsIG9uIHZhbGlkYXRpb24gZGF0YQ0KeV90cnVlID0gW10NCnlfcHJlZCA9IFtdDQoNCmZvciBmZWF0dXJlcywgbGFiZWxzIGluIHZhbF9kYXRhc2V0LnRha2UoMjAwKToNCiAgICBwcmVkcyA9IG1vZGVsLnByZWRpY3QoZmVhdHVyZXMpDQogICAgeV90cnVlLmV4dGVuZChsYWJlbHMubnVtcHkoKSkNCiAgICB5X3ByZWQuZXh0ZW5kKChwcmVkcyA+IDAuNSkuYXN0eXBlKGludCkuZmxhdHRlbigpKQ0KDQojIE1ldHJpY3MNCnByaW50KCJDb25mdXNpb24gTWF0cml4OiIpDQpwcmludChjb25mdXNpb25fbWF0cml4KHlfdHJ1ZSwgeV9wcmVkKSkNCg0KcHJpbnQoIlxuQ2xhc3NpZmljYXRpb24gUmVwb3J0OiIpDQpwcmludChjbGFzc2lmaWNhdGlvbl9yZXBvcnQoeV90cnVlLCB5X3ByZWQsIGRpZ2l0cz00KSkNCg0KcHJpbnQoIlxuQVVDLVJPQzoiKQ0KcHJpbnQoZiJBVUM6IHtyb2NfYXVjX3Njb3JlKHlfdHJ1ZSwgeV9wcmVkKTouNGZ9IikNCg0KIyBDb21tZW50ZWQgb3V0IElQeXRob24gbWFnaWMgdG8gZW5zdXJlIFB5dGhvbiBjb21wYXRpYmlsaXR5Lg0KDQppbXBvcnQgbWF0cGxvdGxpYi5weXBsb3QgYXMgcGx0DQoNCiMgU2F2ZSBhY2N1cmFjeSBwbG90DQpwbHQucGxvdChoaXN0b3J5Lmhpc3RvcnlbJ2FjY3VyYWN5J10sIGxhYmVsPSdUcmFpbiBBY2N1cmFjeScpDQpwbHQucGxvdChoaXN0b3J5Lmhpc3RvcnlbJ3ZhbF9hY2N1cmFjeSddLCBsYWJlbD0nVmFsIEFjY3VyYWN5JykNCnBsdC50aXRsZSgnQWNjdXJhY3kgb3ZlciBFcG9jaHMnKQ0KcGx0LnhsYWJlbCgnRXBvY2gnKQ0KcGx0LnlsYWJlbCgnQWNjdXJhY3knKQ0KcGx0LmxlZ2VuZCgpDQpwbHQuZ3JpZChUcnVlKQ0KcGx0LnNhdmVmaWcoJy9jb250ZW50L2FjY3VyYWN5X3Bsb3QucG5nJykNCnBsdC5jbG9zZSgpDQoNCiMgU2F2ZSBsb3NzIHBsb3QNCnBsdC5wbG90KGhpc3RvcnkuaGlzdG9yeVsnbG9zcyddLCBsYWJlbD0nVHJhaW4gTG9zcycpDQpwbHQucGxvdChoaXN0b3J5Lmhpc3RvcnlbJ3ZhbF9sb3NzJ10sIGxhYmVsPSdWYWwgTG9zcycpDQpwbHQudGl0bGUoJ0xvc3Mgb3ZlciBFcG9jaHMnKQ0KcGx0LnhsYWJlbCgnRXBvY2gnKQ0KcGx0LnlsYWJlbCgnTG9zcycpDQpwbHQubGVnZW5kKCkNCnBsdC5ncmlkKFRydWUpDQpwbHQuc2F2ZWZpZygnL2NvbnRlbnQvbG9zc19wbG90LnBuZycpDQpwbHQuY2xvc2UoKQ0KDQpgYGANCg0KDQoNCg0KLS0tDQoNCiMjIEFwcGVuZGl4OiBGdWxsIFB5dGhvbiBDb2RlDQoNCmBgYHtweXRob259DQojIG1vdW50DQpmcm9tIGdvb2dsZS5jb2xhYiBpbXBvcnQgZHJpdmUNCmRyaXZlLm1vdW50KCcvY29udGVudC9kcml2ZScpDQpgYGANCg0KYGBge3B5dGhvbn0NCiFscyAiL2NvbnRlbnQvZHJpdmUvTXlEcml2ZS9Db2xhYiBOb3RlYm9va3MvdGV4YXNjaGlra2l0YS1wYXJ0aWNsZS1kZXRlY3Rpb24uaXB5bmIiDQpgYGANCg0KYGBge3B5dGhvbn0NCiFscyAiL2NvbnRlbnQvZHJpdmUvTXlEcml2ZS9Db2xhYl9Ob3RlYm9va3MvdGV4YXNjaGlra2l0YS1wYXJ0aWNsZS1kZXRlY3Rpb24uaXB5bmIiDQpgYGANCg0KYGBge3B5dGhvbn0NCiMgaW5zdGFsbCBweWRyaXZlMg0KIXBpcCBpbnN0YWxsIC1VIC1xIFB5RHJpdmUyDQpgYGANCg0KYGBge3B5dGhvbn0NCiMgQXV0aGVudGljYXRlIFB5RHJpdmUyDQpmcm9tIHB5ZHJpdmUyLmF1dGggaW1wb3J0IEdvb2dsZUF1dGgNCmZyb20gcHlkcml2ZTIuZHJpdmUgaW1wb3J0IEdvb2dsZURyaXZlDQpmcm9tIGdvb2dsZS5jb2xhYiBpbXBvcnQgYXV0aA0KZnJvbSBvYXV0aDJjbGllbnQuY2xpZW50IGltcG9ydCBHb29nbGVDcmVkZW50aWFscw0KDQphdXRoLmF1dGhlbnRpY2F0ZV91c2VyKCkNCmdhdXRoID0gR29vZ2xlQXV0aCgpDQpnYXV0aC5jcmVkZW50aWFscyA9IEdvb2dsZUNyZWRlbnRpYWxzLmdldF9hcHBsaWNhdGlvbl9kZWZhdWx0KCkNCmRyaXZlID0gR29vZ2xlRHJpdmUoZ2F1dGgpDQpgYGANCg0KYGBge3B5dGhvbn0NCmZyb20gdGVuc29yZmxvdy5rZXJhcyBpbXBvcnQgbWl4ZWRfcHJlY2lzaW9uDQptaXhlZF9wcmVjaXNpb24uc2V0X2dsb2JhbF9wb2xpY3koJ21peGVkX2Zsb2F0MTYnKQ0KYGBgDQoNCmBgYHtweXRob259DQojIEVuYWJsZSBNaXhlZCBQcmVjaXNpb24gKGZvciBBMTAwIEdQVSkNCmltcG9ydCB0ZW5zb3JmbG93IGFzIHRmDQoNCmRhdGFfZmlsZSA9ICIvY29udGVudC9kcml2ZS9NeURyaXZlL2FsbF90cmFpbi5jc3YuZ3oiDQoNCnRlbXBfZGF0YV9zZXQgPSB0Zi5kYXRhLmV4cGVyaW1lbnRhbC5tYWtlX2Nzdl9kYXRhc2V0KA0KICAgIGRhdGFfZmlsZSwNCiAgICBjb21wcmVzc2lvbl90eXBlPSdHWklQJywNCiAgICBiYXRjaF9zaXplPTEwMDAsDQogICAgbnVtX2Vwb2Nocz0xLA0KICAgIGxhYmVsX25hbWU9JyMgbGFiZWwnLA0KICAgIGlnbm9yZV9lcnJvcnM9VHJ1ZSwNCikNCmBgYA0KDQpgYGB7cHl0aG9ufQ0KI2xvYWQgYW5kIFN0cmVhbSB0aGUgR1pJUCBEYXRhc2V0IGZyb20gRHJpdmUNCmltcG9ydCB0ZW5zb3JmbG93IGFzIHRmDQoNCmRhdGFfZmlsZSA9ICIvY29udGVudC9kcml2ZS9NeURyaXZlL2FsbF90cmFpbi5jc3YuZ3oiDQoNCnRlbXBfZGF0YV9zZXQgPSB0Zi5kYXRhLmV4cGVyaW1lbnRhbC5tYWtlX2Nzdl9kYXRhc2V0KA0KICAgIGRhdGFfZmlsZSwNCiAgICBjb21wcmVzc2lvbl90eXBlPSdHWklQJywNCiAgICBiYXRjaF9zaXplPTEwMDAsDQogICAgbnVtX2Vwb2Nocz0xLA0KICAgIGxhYmVsX25hbWU9JyMgbGFiZWwnLA0KICAgIGlnbm9yZV9lcnJvcnM9VHJ1ZSwNCikNCmBgYA0KDQpgYGB7cHl0aG9ufQ0KIyAgUGFjayBGZWF0dXJlcyBpbnRvIFRlbnNvciBGb3JtYXQNCmRlZiBwYWNrKGZlYXR1cmVzLCBsYWJlbCk6DQogICAgcmV0dXJuIHRmLnN0YWNrKGxpc3QoZmVhdHVyZXMudmFsdWVzKCkpLCBheGlzPS0xKSwgdGYuY2FzdChsYWJlbCwgdGYuaW50MzIpDQoNCnBhY2tlZF9kYXRhc2V0ID0gdGVtcF9kYXRhX3NldC5tYXAocGFjaykNCmBgYA0KDQpgYGB7cHl0aG9ufQ0KIyBQcmV2aWV3IHRoZSBEYXRhc2V0IChPcHRpb25hbCBTYW5pdHkgQ2hlY2spDQpmb3IgZmVhdHVyZXMsIGxhYmVscyBpbiBwYWNrZWRfZGF0YXNldC50YWtlKDEpOg0KICAgIHByaW50KGZlYXR1cmVzLnNoYXBlKSAgIyBzaG91bGQgYmUgKDEwMDAsIDI4KQ0KICAgIHByaW50KHRmLnJlZHVjZV9taW4obGFiZWxzKSwgdGYucmVkdWNlX21heChsYWJlbHMpKSAgIyBzaG91bGQgYmUgMCBhbmQgMQ0KYGBgDQoNCmBgYHtweXRob259DQp0cmFpbl9zaXplID0gODAwDQp2YWxfc2l6ZSA9IDIwMA0KDQpmdWxsX2RhdGFzZXQgPSBwYWNrZWRfZGF0YXNldC5zaHVmZmxlKGJ1ZmZlcl9zaXplPTEwMDApDQp0cmFpbl9kYXRhc2V0ID0gZnVsbF9kYXRhc2V0LnRha2UodHJhaW5fc2l6ZSkucmVwZWF0KCkNCnZhbF9kYXRhc2V0ID0gZnVsbF9kYXRhc2V0LnNraXAodHJhaW5fc2l6ZSkucmVwZWF0KCkNCmBgYA0KDQpgYGB7cHl0aG9ufQ0KIyBCdWlsZCB0aGUgTmV1cmFsIE5ldHdvcmsgTW9kZWwNCmZyb20gdGVuc29yZmxvdy5rZXJhcy5tb2RlbHMgaW1wb3J0IFNlcXVlbnRpYWwNCmZyb20gdGVuc29yZmxvdy5rZXJhcy5sYXllcnMgaW1wb3J0IERlbnNlLCBCYXRjaE5vcm1hbGl6YXRpb24NCg0KbW9kZWwgPSBTZXF1ZW50aWFsKFsNCiAgICB0Zi5rZXJhcy5JbnB1dChzaGFwZT0oMjgsKSksDQogICAgQmF0Y2hOb3JtYWxpemF0aW9uKCksDQogICAgRGVuc2UoNTAwLCBhY3RpdmF0aW9uPSdyZWx1JyksDQogICAgQmF0Y2hOb3JtYWxpemF0aW9uKCksDQogICAgRGVuc2UoNDAwLCBhY3RpdmF0aW9uPSdyZWx1JyksDQogICAgQmF0Y2hOb3JtYWxpemF0aW9uKCksDQogICAgRGVuc2UoMzAwLCBhY3RpdmF0aW9uPSdyZWx1JyksDQogICAgQmF0Y2hOb3JtYWxpemF0aW9uKCksDQogICAgRGVuc2UoMjAwLCBhY3RpdmF0aW9uPSdyZWx1JyksDQogICAgQmF0Y2hOb3JtYWxpemF0aW9uKCksDQogICAgRGVuc2UoMTAwLCBhY3RpdmF0aW9uPSdyZWx1JyksDQogICAgQmF0Y2hOb3JtYWxpemF0aW9uKCksDQogICAgRGVuc2UoMSwgYWN0aXZhdGlvbj0nc2lnbW9pZCcsIGR0eXBlPSdmbG9hdDMyJykgICMgZm9yY2UgZmluYWwgb3V0cHV0IHRvIGZsb2F0MzINCl0pDQpgYGANCg0KYGBge3B5dGhvbn0NCiMgY29tcGlsZQ0KZnJvbSB0ZW5zb3JmbG93LmtlcmFzLm9wdGltaXplcnMgaW1wb3J0IEFkYW0NCg0KbW9kZWwuY29tcGlsZSgNCiAgICBvcHRpbWl6ZXI9QWRhbSgpLA0KICAgIGxvc3M9dGYua2VyYXMubG9zc2VzLkJpbmFyeUNyb3NzZW50cm9weSgpLA0KICAgIG1ldHJpY3M9Ww0KICAgICAgICAnYWNjdXJhY3knLA0KICAgICAgICB0Zi5rZXJhcy5tZXRyaWNzLkZhbHNlTmVnYXRpdmVzKG5hbWU9J2ZhbHNlX25lZ2F0aXZlc18xJyksDQogICAgICAgIHRmLmtlcmFzLm1ldHJpY3MuRmFsc2VQb3NpdGl2ZXMobmFtZT0nZmFsc2VfcG9zaXRpdmVzXzEnKSwNCiAgICAgICAgdGYua2VyYXMubWV0cmljcy5BVUMobmFtZT0nYXVjXzEnKSwNCiAgICAgICAgdGYua2VyYXMubWV0cmljcy5QcmVjaXNpb24obmFtZT0ncHJlY2lzaW9uXzEnKSwNCiAgICAgICAgdGYua2VyYXMubWV0cmljcy5SZWNhbGwobmFtZT0ncmVjYWxsXzEnKQ0KICAgIF0NCikNCmBgYA0KDQpgYGB7cHl0aG9ufQ0KIyBTZXQgVXAgVGVuc29yQm9hcmQgTG9nZ2luZyBhbmQgRWFybHkgU3RvcHBpbmcNCmZyb20gdGVuc29yZmxvdy5rZXJhcy5jYWxsYmFja3MgaW1wb3J0IFRlbnNvckJvYXJkLCBFYXJseVN0b3BwaW5nDQppbXBvcnQgZGF0ZXRpbWUNCg0KbG9nX2RpciA9ICJsb2dzL2ZpdC8iICsgZGF0ZXRpbWUuZGF0ZXRpbWUubm93KCkuc3RyZnRpbWUoIiVZJW0lZC0lSCVNJVMiKQ0KdGIgPSBUZW5zb3JCb2FyZChsb2dfZGlyPWxvZ19kaXIsIGhpc3RvZ3JhbV9mcmVxPTEpDQpzYWZldHkgPSBFYXJseVN0b3BwaW5nKHBhdGllbmNlPTMsIG1vbml0b3I9J3ZhbF9sb3NzJykNCmBgYA0KDQpgYGB7cHl0aG9ufQ0KIyAjIHRyYWluIHRoZSBtb2RlbA0KIyBoaXN0b3J5ID0gbW9kZWwuZml0KA0KIyAgICAgdHJhaW5fZGF0YXNldCwNCiMgICAgIHZhbGlkYXRpb25fZGF0YT12YWxfZGF0YXNldCwNCiMgICAgIHN0ZXBzX3Blcl9lcG9jaD04MDAsDQojICAgICB2YWxpZGF0aW9uX3N0ZXBzPTIwMCwNCiMgICAgIGVwb2Nocz0xMDAwLA0KIyAgICAgY2FsbGJhY2tzPVtzYWZldHksIHRiXQ0KIyApDQpgYGANCg0KYGBge3B5dGhvbn0NCiMgIyBmYXN0ZXIgLSB0cmFpbiB0aGUgbW9kZWwNCg0KIyBoaXN0b3J5ID0gbW9kZWwuZml0KA0KIyAgICAgdHJhaW5fZGF0YXNldCwNCiMgICAgIHZhbGlkYXRpb25fZGF0YT12YWxfZGF0YXNldCwNCiMgICAgIHN0ZXBzX3Blcl9lcG9jaD04MDAsDQojICAgICB2YWxpZGF0aW9uX3N0ZXBzPTIwMCwNCiMgICAgIGVwb2Nocz0xMDAwLA0KIyAgICAgY2FsbGJhY2tzPVtzYWZldHksIHRiXSwNCiMgICAgIHZlcmJvc2U9MCAgIyBmYXN0ZXN0IHNjcmVlbi13aXNlDQojICkNCmBgYA0KDQpgYGB7cHl0aG9ufQ0KIyB1c2UgY3Ygbm90IHR0czoNCmltcG9ydCBwYW5kYXMgYXMgcGQNCmZyb20gc2tsZWFybi5tb2RlbF9zZWxlY3Rpb24gaW1wb3J0IFN0cmF0aWZpZWRLRm9sZA0KZnJvbSBza2xlYXJuLm1ldHJpY3MgaW1wb3J0IGFjY3VyYWN5X3Njb3JlLCBwcmVjaXNpb25fc2NvcmUsIHJlY2FsbF9zY29yZSwgcm9jX2F1Y19zY29yZQ0KaW1wb3J0IG51bXB5IGFzIG5wDQoNCiMgTG9hZCBmdWxsIGRhdGFzZXQgb25jZQ0KZGYgPSBwZC5yZWFkX2NzdigiL2NvbnRlbnQvZHJpdmUvTXlEcml2ZS9hbGxfdHJhaW4uY3N2Lmd6IiwgY29tcHJlc3Npb249J2d6aXAnKQ0KIyBYID0gZGYuZHJvcChjb2x1bW5zPVsnIyBsYWJlbCcsICdtYXNzJ10pLnZhbHVlcyAjKGNvbW1lbnQgb3V0IGZvciB1c2UgdyBjdiAtIHVzZSBmb3IgdHRzKQ0KDQpYID0gZGYuZHJvcChjb2x1bW5zPVsnIyBsYWJlbCddKS52YWx1ZXMgIyh1c2Ugd2l0aCBDViBjb21tZW50IG91dCBmb3IgdHRzKQ0KDQp5ID0gZGZbJyMgbGFiZWwnXS52YWx1ZXMNCg0KIyBLLUZvbGQgc2V0dXANCmtmID0gU3RyYXRpZmllZEtGb2xkKG5fc3BsaXRzPTUsIHNodWZmbGU9VHJ1ZSwgcmFuZG9tX3N0YXRlPTQyKQ0KDQphbGxfbWV0cmljcyA9IFtdDQoNCmZvciBmb2xkLCAodHJhaW5faWR4LCB2YWxfaWR4KSBpbiBlbnVtZXJhdGUoa2Yuc3BsaXQoWCwgeSkpOg0KICAgIHByaW50KGYiXG5GT0xEIHtmb2xkICsgMX0iKQ0KICAgIHhfdHJhaW4sIHhfdmFsID0gWFt0cmFpbl9pZHhdLCBYW3ZhbF9pZHhdDQogICAgeV90cmFpbiwgeV92YWwgPSB5W3RyYWluX2lkeF0sIHlbdmFsX2lkeF0NCg0KICAgICMgQnVpbGQgYSBuZXcgbW9kZWwgZm9yIGVhY2ggZm9sZA0KICAgIG1vZGVsID0gU2VxdWVudGlhbChbDQogICAgICAgIHRmLmtlcmFzLklucHV0KHNoYXBlPSgyOCwpKSwNCiAgICAgICAgQmF0Y2hOb3JtYWxpemF0aW9uKCksDQogICAgICAgIERlbnNlKDUwMCwgYWN0aXZhdGlvbj0ncmVsdScpLA0KICAgICAgICBCYXRjaE5vcm1hbGl6YXRpb24oKSwNCiAgICAgICAgRGVuc2UoNDAwLCBhY3RpdmF0aW9uPSdyZWx1JyksDQogICAgICAgIEJhdGNoTm9ybWFsaXphdGlvbigpLA0KICAgICAgICBEZW5zZSgzMDAsIGFjdGl2YXRpb249J3JlbHUnKSwNCiAgICAgICAgQmF0Y2hOb3JtYWxpemF0aW9uKCksDQogICAgICAgIERlbnNlKDIwMCwgYWN0aXZhdGlvbj0ncmVsdScpLA0KICAgICAgICBCYXRjaE5vcm1hbGl6YXRpb24oKSwNCiAgICAgICAgRGVuc2UoMTAwLCBhY3RpdmF0aW9uPSdyZWx1JyksDQogICAgICAgIEJhdGNoTm9ybWFsaXphdGlvbigpLA0KICAgICAgICBEZW5zZSgxLCBhY3RpdmF0aW9uPSdzaWdtb2lkJywgZHR5cGU9J2Zsb2F0MzInKSwNCiAgICBdKQ0KDQogICAgbW9kZWwuY29tcGlsZSgNCiAgICAgICAgb3B0aW1pemVyPUFkYW0oKSwNCiAgICAgICAgbG9zcz0nYmluYXJ5X2Nyb3NzZW50cm9weScsDQogICAgICAgIG1ldHJpY3M9WydhY2N1cmFjeScsIHRmLmtlcmFzLm1ldHJpY3MuQVVDKG5hbWU9J2F1YycpLCB0Zi5rZXJhcy5tZXRyaWNzLlByZWNpc2lvbigpLCB0Zi5rZXJhcy5tZXRyaWNzLlJlY2FsbCgpXQ0KICAgICkNCg0KICAgIG1vZGVsLmZpdCh4X3RyYWluLCB5X3RyYWluLCBlcG9jaHM9MywgYmF0Y2hfc2l6ZT0xMDAwLCB2ZXJib3NlPTApDQoNCiAgICAjIEV2YWx1YXRlDQogICAgeV9wcmVkID0gbW9kZWwucHJlZGljdCh4X3ZhbCkgPiAwLjUNCiAgICBhY2MgPSBhY2N1cmFjeV9zY29yZSh5X3ZhbCwgeV9wcmVkKQ0KICAgIHByZWMgPSBwcmVjaXNpb25fc2NvcmUoeV92YWwsIHlfcHJlZCkNCiAgICByZWMgPSByZWNhbGxfc2NvcmUoeV92YWwsIHlfcHJlZCkNCiAgICBhdWMgPSByb2NfYXVjX3Njb3JlKHlfdmFsLCB5X3ByZWQpDQogICAgYWxsX21ldHJpY3MuYXBwZW5kKChhY2MsIHByZWMsIHJlYywgYXVjKSkNCiAgICBwcmludChmIkFDQzoge2FjYzouNGZ9ICBQUkVDOiB7cHJlYzouNGZ9ICBSRUM6IHtyZWM6LjRmfSAgQVVDOiB7YXVjOi40Zn0iKQ0KYGBgDQoNCmBgYHtweXRob259DQojIEZvbGQgbWV0cmljcyBmcm9tIG91dHB1dA0KZm9sZF9tZXRyaWNzID0gWw0KICAgICgwLjg4NDEsIDAuODczNywgMC44OTgwLCAwLjg4NDEpLA0KICAgICgwLjg4MzUsIDAuODYxNSwgMC45MTQwLCAwLjg4MzUpLA0KICAgICgwLjg4MzYsIDAuODc2MiwgMC44OTM1LCAwLjg4MzYpLA0KICAgICgwLjg4NDEsIDAuODY5OCwgMC45MDM2LCAwLjg4NDEpLA0KICAgICgwLjg4MzQsIDAuODYzMCwgMC45MTE2LCAwLjg4MzQpLA0KXQ0KDQphY2NzLCBwcmVjcywgcmVjcywgYXVjcyA9IHppcCgqZm9sZF9tZXRyaWNzKQ0KDQpwcmludChmIk1lYW4gQWNjdXJhY3k6ICAgICB7bnAubWVhbihhY2NzKTouNGZ9IikNCnByaW50KGYiTWVhbiBQcmVjaXNpb246ICAgIHtucC5tZWFuKHByZWNzKTouNGZ9IikNCnByaW50KGYiTWVhbiBSZWNhbGw6ICAgICAgIHtucC5tZWFuKHJlY3MpOi40Zn0iKQ0KcHJpbnQoZiJNZWFuIEFVQzogICAgICAgICAge25wLm1lYW4oYXVjcyk6LjRmfSIpDQpgYGANCg0KYGBge3B5dGhvbn0NCm5leHQNCiAgICAlbG9hZF9leHQgdGVuc29yYm9hcmQNCiV0ZW5zb3Jib2FyZCAtLWxvZ2RpciBsb2dzDQpgYGANCg0KYGBge3B5dGhvbn0NCiMgIFBsb3QgVHJhaW5pbmcgSGlzdG9yeSAoQWNjdXJhY3kgKyBMb3NzKQ0KaW1wb3J0IG1hdHBsb3RsaWIucHlwbG90IGFzIHBsdA0KDQojIFBsb3QgYWNjdXJhY3kNCnBsdC5wbG90KGhpc3RvcnkuaGlzdG9yeVsnYWNjdXJhY3knXSwgbGFiZWw9J1RyYWluIEFjY3VyYWN5JykNCnBsdC5wbG90KGhpc3RvcnkuaGlzdG9yeVsndmFsX2FjY3VyYWN5J10sIGxhYmVsPSdWYWxpZGF0aW9uIEFjY3VyYWN5JykNCnBsdC50aXRsZSgnQWNjdXJhY3kgb3ZlciBFcG9jaHMnKQ0KcGx0LnhsYWJlbCgnRXBvY2gnKQ0KcGx0LnlsYWJlbCgnQWNjdXJhY3knKQ0KcGx0LmxlZ2VuZCgpDQpwbHQuZ3JpZChUcnVlKQ0KcGx0LnNob3coKQ0KDQojIFBsb3QgbG9zcw0KcGx0LnBsb3QoaGlzdG9yeS5oaXN0b3J5Wydsb3NzJ10sIGxhYmVsPSdUcmFpbiBMb3NzJykNCnBsdC5wbG90KGhpc3RvcnkuaGlzdG9yeVsndmFsX2xvc3MnXSwgbGFiZWw9J1ZhbGlkYXRpb24gTG9zcycpDQpwbHQudGl0bGUoJ0xvc3Mgb3ZlciBFcG9jaHMnKQ0KcGx0LnhsYWJlbCgnRXBvY2gnKQ0KcGx0LnlsYWJlbCgnTG9zcycpDQpwbHQubGVnZW5kKCkNCnBsdC5ncmlkKFRydWUpDQpwbHQuc2hvdygpDQpgYGANCg0KYGBge3B5dGhvbn0NCmhpc3RvcnkgPSBtb2RlbC5maXQoDQogICAgdHJhaW5fZGF0YXNldCwNCiAgICB2YWxpZGF0aW9uX2RhdGE9dmFsX2RhdGFzZXQsDQogICAgc3RlcHNfcGVyX2Vwb2NoPTgwMCwNCiAgICB2YWxpZGF0aW9uX3N0ZXBzPTIwMCwNCiAgICBlcG9jaHM9MywNCiAgICBjYWxsYmFja3M9W3RiXSAgIyBTa2lwIGVhcmx5IHN0b3BwaW5nIHNvIGl0IGZpbmlzaGVzIGFsbCAzIGVwb2Nocw0KKQ0KYGBgDQoNCmBgYHtweXRob259DQppbXBvcnQgbWF0cGxvdGxpYi5weXBsb3QgYXMgcGx0DQoNCiMgQWNjdXJhY3kNCnBsdC5wbG90KGhpc3RvcnkuaGlzdG9yeVsnYWNjdXJhY3knXSwgbGFiZWw9J1RyYWluIEFjY3VyYWN5JykNCnBsdC5wbG90KGhpc3RvcnkuaGlzdG9yeVsndmFsX2FjY3VyYWN5J10sIGxhYmVsPSdWYWwgQWNjdXJhY3knKQ0KcGx0LnRpdGxlKCdBY2N1cmFjeSBvdmVyIEVwb2NocycpDQpwbHQueGxhYmVsKCdFcG9jaCcpDQpwbHQueWxhYmVsKCdBY2N1cmFjeScpDQpwbHQubGVnZW5kKCkNCnBsdC5ncmlkKFRydWUpDQpwbHQuc2hvdygpDQoNCiMgTG9zcw0KcGx0LnBsb3QoaGlzdG9yeS5oaXN0b3J5Wydsb3NzJ10sIGxhYmVsPSdUcmFpbiBMb3NzJykNCnBsdC5wbG90KGhpc3RvcnkuaGlzdG9yeVsndmFsX2xvc3MnXSwgbGFiZWw9J1ZhbCBMb3NzJykNCnBsdC50aXRsZSgnTG9zcyBvdmVyIEVwb2NocycpDQpwbHQueGxhYmVsKCdFcG9jaCcpDQpwbHQueWxhYmVsKCdMb3NzJykNCnBsdC5sZWdlbmQoKQ0KcGx0LmdyaWQoVHJ1ZSkNCnBsdC5zaG93KCkNCmBgYA0KDQpgYGB7cHl0aG9ufQ0KIyBldmFsdWF0ZSBvbiB2YWxpZGFpb24gZGF0YToNCnJlc3VsdHMgPSBtb2RlbC5ldmFsdWF0ZSh2YWxfZGF0YXNldCwgc3RlcHM9MjAwKQ0KZm9yIG5hbWUsIHZhbHVlIGluIHppcChtb2RlbC5tZXRyaWNzX25hbWVzLCByZXN1bHRzKToNCiAgICBwcmludChmIntuYW1lfToge3ZhbHVlOi40Zn0iKQ0KYGBgDQoNCmBgYHtweXRob259DQojIFNhdmUgdGhlIG1vZGVsDQptb2RlbC5zYXZlKCcvY29udGVudC9kcml2ZS9NeURyaXZlL3NhdmVkX3BhcnRpY2xlX21vZGVsJykNCmBgYA0KDQpgYGB7cHl0aG9ufQ0KIyAgRXhwb3J0IGFzIGEgLnppcCBmaWxlIGZvciBkb3dubG9hZCBvciB0cmFuc2Zlcg0KaW1wb3J0IHNodXRpbA0Kc2h1dGlsLm1ha2VfYXJjaGl2ZSgnL2NvbnRlbnQvcGFydGljbGVfbW9kZWxfZXhwb3J0JywgJ3ppcCcsICcvY29udGVudC9kcml2ZS9NeURyaXZlL3NhdmVkX3BhcnRpY2xlX21vZGVsJykNCmBgYA0KDQpgYGB7cHl0aG9ufQ0KIyAgR2VuZXJhdGUgY2xhc3NpZmljYXRpb24gbWV0cmljcyAoZm9yIHRoZSBjYXNlIHN0dWR5KQ0KaW1wb3J0IG51bXB5IGFzIG5wDQpmcm9tIHNrbGVhcm4ubWV0cmljcyBpbXBvcnQgY2xhc3NpZmljYXRpb25fcmVwb3J0LCBjb25mdXNpb25fbWF0cml4LCByb2NfYXVjX3Njb3JlDQoNCiMgR2V0IHByZWRpY3Rpb25zIGZyb20gbW9kZWwgb24gdmFsaWRhdGlvbiBkYXRhDQp5X3RydWUgPSBbXQ0KeV9wcmVkID0gW10NCg0KZm9yIGZlYXR1cmVzLCBsYWJlbHMgaW4gdmFsX2RhdGFzZXQudGFrZSgyMDApOg0KICAgIHByZWRzID0gbW9kZWwucHJlZGljdChmZWF0dXJlcykNCiAgICB5X3RydWUuZXh0ZW5kKGxhYmVscy5udW1weSgpKQ0KICAgIHlfcHJlZC5leHRlbmQoKHByZWRzID4gMC41KS5hc3R5cGUoaW50KS5mbGF0dGVuKCkpDQoNCiMgTWV0cmljcw0KcHJpbnQoIkNvbmZ1c2lvbiBNYXRyaXg6IikNCnByaW50KGNvbmZ1c2lvbl9tYXRyaXgoeV90cnVlLCB5X3ByZWQpKQ0KDQpwcmludCgiXG5DbGFzc2lmaWNhdGlvbiBSZXBvcnQ6IikNCnByaW50KGNsYXNzaWZpY2F0aW9uX3JlcG9ydCh5X3RydWUsIHlfcHJlZCwgZGlnaXRzPTQpKQ0KDQpwcmludCgiXG5BVUMtUk9DOiIpDQpwcmludChmIkFVQzoge3JvY19hdWNfc2NvcmUoeV90cnVlLCB5X3ByZWQpOi40Zn0iKQ0KYGBgDQoNCg0K