La Regresión Logística
La Regresión Logística es un método estadístico para predecir clases
binarias. El resultado o variable objetivo es de naturaleza dicotómica.
Dicotómica significa que solo hay dos clases posibles. Por ejemplo, se
puede utilizar para problemas de detección de cáncer o calcular la
probabilidad de que ocurra un evento.
La Regresión Logística es uno de los algoritmos de Machine Learning
más simples y más utilizados para la clasificación de dos clases. Es
fácil de implementar y se puede usar como línea de base para cualquier
problema de clasificación binaria. La Regresión Logística describe y
estima la relación entre una variable binaria dependiente y las
variables independientes.
En general, este algoritmo se puede utilizar para varios problemas de
clasificación, como la detección de spam, predicción de la diabetes, si
un cliente determinado comprará un producto en particular o si se irá
con la competencia, hay muchos más ejemplos en donde se puede aplicar
este algoritmo.
Por su parte la Regresión Logística lleva el nombre de la función
utilizada en el núcleo del método, la función logística es también
llamada función Sigmoide. Esta función es una curva en forma de S que
puede tomar cualquier número de valor real y asignar a un valor entre 0
y 1.
Si la curva va a infinito positivo la predicción se convertirá en 1,
y si la curva pasa el infinito negativo, la predicción se convertirá en
0. Si la salida de la función Sigmoide es mayor que 0.5, podemos
clasificar el resultado como 1 o SI, y si es menor que 0.5 podemos
clasificarlo como 0 o NO. Por su parte si el resultado es 0.75, podemos
decir en términos de probabilidad como, hay un 75% de probabilidades de
que el paciente sufra cáncer.
Pero veamos este algoritmo de manera matemática, la ecuación de
Regresión Lineal es está:
Donde “y” es la variable dependiente y “x1, x2, …” son variables
independientes o explicativas.
Por su parte, la ecuación de la función Sigmoide es la siguiente:
Entonces si aplicamos la función Sigmoide en la Regresión Lineal nos
quedaría algo como esto:
\[
p=\frac{1}{1+e^{-\left(a_{1} x_{1}+a_{2} x_{2}+a_{n} x_{n}+b\right)}}
\]
Ejemplo de Regresion Logistica usando Python - Sklearn
En esta seccion explicaremos la parte práctica del algoritmo de
Regresión Logística, en donde desarrollaremos un modelo para predecir el
numero en una imagen de 8x8.
Para este análisis vamos a utilizar uno de los dataset que se
encuentra disponible en la librería scikit-learn y es el correspondiente
a digits
Si no lo sabias dentro de la librería de Python scikit-learn dispones
de varios dataset, con los que puedes practicar tus conocimientos de
Machine Learning. Puedes encontrar tanto para problemas de regresión,
como para problemas de clasificación.
A continuacion comentamos cada seccion del codigo de python
Primero cargamos los modulos y dataset que usara para el
analisis.
import matplotlib.pyplot as plt
from sklearn.datasets import load_digits
digits = load_digits()
Se imprime un descriptivo con mas detalle acerca del dataset
digits
print(digits.DESCR)
.. _digits_dataset:
Optical recognition of handwritten digits dataset
--------------------------------------------------
**Data Set Characteristics:**
:Number of Instances: 1797
:Number of Attributes: 64
:Attribute Information: 8x8 image of integer pixels in the range 0..16.
:Missing Attribute Values: None
:Creator: E. Alpaydin (alpaydin '@' boun.edu.tr)
:Date: July; 1998
This is a copy of the test set of the UCI ML hand-written digits datasets
https://archive.ics.uci.edu/ml/datasets/Optical+Recognition+of+Handwritten+Digits
The data set contains images of hand-written digits: 10 classes where
each class refers to a digit.
Preprocessing programs made available by NIST were used to extract
normalized bitmaps of handwritten digits from a preprinted form. From a
total of 43 people, 30 contributed to the training set and different 13
to the test set. 32x32 bitmaps are divided into nonoverlapping blocks of
4x4 and the number of on pixels are counted in each block. This generates
an input matrix of 8x8 where each element is an integer in the range
0..16. This reduces dimensionality and gives invariance to small
distortions.
For info on NIST preprocessing routines, see M. D. Garris, J. L. Blue, G.
T. Candela, D. L. Dimmick, J. Geist, P. J. Grother, S. A. Janet, and C.
L. Wilson, NIST Form-Based Handprint Recognition System, NISTIR 5469,
1994.
.. topic:: References
- C. Kaynak (1995) Methods of Combining Multiple Classifiers and Their
Applications to Handwritten Digit Recognition, MSc Thesis, Institute of
Graduate Studies in Science and Engineering, Bogazici University.
- E. Alpaydin, C. Kaynak (1998) Cascading Classifiers, Kybernetika.
- Ken Tang and Ponnuthurai N. Suganthan and Xi Yao and A. Kai Qin.
Linear dimensionalityreduction using relevance weighted LDA. School of
Electrical and Electronic Engineering Nanyang Technological University.
2005.
- Claudio Gentile. A New Approximate Maximal Margin Classification
Algorithm. NIPS. 2000.
Visualicemos algunas componentes de nuestra data. Veamos las primeras
10 componentes de digits.images y digits.target.
plt.figure(figsize = (20,4))
for i in range(0,5):
#print(i)
plt.subplot(1,5, i+1)
plt.imshow(digits.images[i], cmap = plt.cm.gray)
plt.title("data : %i \n" %digits.target[i], fontsize = 20)
k=5
plt.figure(figsize = (20,4))
for i in range(0,5):
#print(i)
j=int(i+k)
plt.subplot(1,5, i+1)
plt.imshow(digits.images[j], cmap = plt.cm.gray)
plt.title("data : %i \n" %digits.target[j], fontsize = 20)
Ahora vamos a proceder a definir las variables de “x” y “y” que vamos
emplear en nuestro modelo.
Para “x” vamos a utilizar todas las variables que se
encuentran dentro de digits.data, Por su
parte, “y” será igual a los datos correspondientes a
digits.target.
Definido “x” y “y” ya podemos
realizar la separación correspondiente a los datos de prueba y
entrenamiento para ello importamos la respectiva librería y procedemos a
utilizar train_test_split para separar los datos.
Para la separación de los datos, vamos a tomar un 25% de los mismos
para utilizarlos como prueba una vez que hayamos obtenido el modelo.
# datos numericos de la imagen, en un formato lineal,como 64 variables independientes
digits["data"][0]
# resultado , variable objetivo , label , este seria mi variable dependiente
digits.target[0]
# Construimos el dataset train y test
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(digits.data, digits.target,
test_size=0.25,
random_state= 666)
Ya en este momento tenemos nuestros datos listos para empezar a
construir el modelo.
Lo primero que debemos hacer es importar LogisticRegression que se
encuentra dentro de la librería linear_model, y a su vez definimos el
algoritmo “logistic1” .
#Defino el algoritmo "logistic1" a utilizar
from sklearn.linear_model import LogisticRegression
logistic1 = LogisticRegression(max_iter = 2000,
verbose = 1,
tol = 1e-6)
Seguidamente entrenamos el modelo utilizando la instrucción fit y los
datos tanto de “X” como de “y” de
entrenamiento.
#Entreno el modelo
logistic1.fit(x_train, y_train)
Y finalmente realizamos una predicción, utilizando la instrucción
predict y los datos de prueba.
# Hagamos la prediccion para todos los datos de prueba
predictions = logistic1.predict(x_test)
Calculamos la precisión del algoritmo
“logistic1”
# score
score = logistic1.score(x_test, y_test)
score
score
Out[13]: 0.9688888888888889
Ademas calculamos y visualizamos la Matriz de Confusion.
# Matriz de confusion
from sklearn import metrics
cm = metrics.confusion_matrix(y_test, predictions)
cm
# Visualicemos la matriz de confusion
import seaborn as sns
plt.figure(figsize = (9,9))
sns.heatmap(cm, annot = True, fmt = ".3f",
linewidths=0.5, square = True,
cmap = "Blues_r")
plt.ylabel("Datos Reales (y_test)")
plt.xlabel("Predicciones del modelo ")
plt.title("Score {0}".format(score))
El resultado del score y Matriz de confusion confirman que el modelo
obtenido es bueno.
Finalmente adjutamos el codigo completo de python:
# %% Regresion logistica con digits
# Carguemos modulos y librerias
import matplotlib.pyplot as plt
from sklearn.datasets import load_digits
# Cargamos los datos
digits = load_digits()
# Descripcion del dataset digits
print(digits.DESCR)
# %% Visualicemos algunas componentes de nuestra data
# Veamos las primeras 10 componentes de digits.images y digits.target
plt.figure(figsize = (20,4))
for i in range(0,5):
#print(i)
plt.subplot(1,5, i+1)
plt.imshow(digits.images[i], cmap = plt.cm.gray)
plt.title("data : %i \n" %digits.target[i], fontsize = 20)
k=5
plt.figure(figsize = (20,4))
for i in range(0,5):
#print(i)
j=int(i+k)
plt.subplot(1,5, i+1)
plt.imshow(digits.images[j], cmap = plt.cm.gray)
plt.title("data : %i \n" %digits.target[j], fontsize = 20)
# %% Construccion del modelo
# datos numericos de la imagen, en un formato lineal,como 64 variables independientes
digits["data"][0]
# resultado , variable objetivo , label , este seria mi variable dependiente
digits.target[0]
# Construimos el dataset train y test
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(digits.data, digits.target,
test_size=0.25,
random_state= 666)
#Defino el algoritmo "logistic1" a utilizar
from sklearn.linear_model import LogisticRegression
logistic1 = LogisticRegression(max_iter = 2000,
verbose = 1,
tol = 1e-6)
#Entreno el modelo
logistic1.fit(x_train, y_train)
# %%
# Hagamos la prediccion para todos los datos de prueba
predictions = logistic1.predict(x_test)
# %% Medir el rendimiento/performance del modelo : logistic1
# score
score = logistic1.score(x_test, y_test)
score
# Matriz de confusion
from sklearn import metrics
cm = metrics.confusion_matrix(y_test, predictions)
cm
# Visualicemos la matriz de confusion
import seaborn as sns
plt.figure(figsize = (9,9))
sns.heatmap(cm, annot = True, fmt = ".3f",
linewidths=0.5, square = True,
cmap = "Blues_r")
plt.ylabel("Datos Reales (y_test)")
plt.xlabel("Predicciones del modelo ")
plt.title("Score {0}".format(score))
LS0tDQp0aXRsZTogIlJlZ3Jlc2lvbiBMb2dpc3RpY2EgY29uIFB5dGhvbiAtIFNrbGVhcm4iDQphdXRob3I6ICJXaWxsaWFtIEFzdG9jb25kb3IgRmVsaXgiDQpkYXRlOiAiMjAyMi8wOC8xMCINCnN1YnRpdGxlOiBNYXJrZG93biAtIFJNYXJrZG93bg0Kb3V0cHV0Og0KICBodG1sX2RvY3VtZW50Og0KICAgIGNvZGVfZG93bmxvYWQ6IFRSVUUNCi0tLQ0KDQoNCg0KDQo8IS0tIGNvbWVudGFyaW9zICAtLT4NCjwhLS0gaHR0cHM6Ly9hZ2VuY2lhYjEyLmNvbS9ub3RpY2lhL3F1ZS1zb24tcmVncmVzaW9uLWNsYXNpZmljYWNpb24tbWFjaGluZS1sZWFybmluZyAtLT4NCg0KIyBSZWdyZXNpw7NuIHkgY2xhc2lmaWNhY2nDs24gZW4gTWFjaGluZSBMZWFybmluZw0KDQoqKk1hY2hpbmUgTGVhcm5pbmcqKiBlcyB1biBzdWJjYW1wbyBkZSBsYXMgY2llbmNpYXMgZGUgbGEgY29tcHV0YWNpw7NuIHkgbGEgKippbnRlbGlnZW5jaWEgYXJ0aWZpY2lhbCoqLCBxdWUgc2Ugc2lydmUgZGUgKiphbGdvcml0bW9zKiogcXVlIHBlcm1pdGVuIGEgbGFzIG3DoXF1aW5hcyBhcHJlbmRlciBpbWl0YW5kbyBsYSBmb3JtYSBlbiBsYSBxdWUgbG9zIHNlcmVzIGh1bWFub3MgZGVzYXJyb2xsYW4gYWNjaW9uZXMuDQoNCkxvcyBtw6l0b2RvcyBkZSAqKmNsYXNpZmljYWNpw7NuIHkgcmVncmVzacOzbioqIHNlIGVuY3VlbnRyYW4gZGVudHJvIGRlIHVuYSByYW1hIGRlbCAqKk1hY2hpbmUgTGVhcm5pbmcqKiBjb25vY2lkYSBjb21vIGFwcmVuZGl6YWplIGF1dG9tw6F0aWNvIHN1cGVydmlzYWRvLiBUZSBleHBsaWNhbW9zIHF1w6kgc29uIHkgZW4gcXXDqSBzZSBkaWZlcmVuY2lhbi4NCg0KPCEtLSBodHRwczovL3d3dy50ZWNodGFyZ2V0LmNvbS9zZWFyY2hlbnRlcnByaXNlYWkvZGVmaW5pdGlvbi9tYWNoaW5lLWxlYXJuaW5nLU1MIzp+OnRleHQ9TWFjaGluZSUyMGxlYXJuaW5nJTIwKE1MKSUyMGlzJTIwYSx0byUyMHByZWRpY3QlMjBuZXclMjBvdXRwdXQlMjB2YWx1ZXMuIC0tPg0KDQoNCiMgR3J1cG9zIGRlIGFsZ29yaXRtb3MgZW4gTWFjaGluZSBMZWFybmluZw0KDQpFbCBNYWNoaW5lIExlYXJuaW5nIHNlIGZ1bmRhbWVudGEgZW4gY3VhdHJvIGdyYW5kZXMgZ3J1cG9zIGRlIGFsZ29yaXRtb3M6DQoNCi0gTWFjaGluZSBMZWFybmluZyBzdXBlcnZpc2Fkby4NCi0gTWFjaGluZSBMZWFybmluZyBubyBzdXBlcnZpc2Fkby4NCi0gTWFjaGluZSBMZWFybmluZyBzZW1pc3VwZXJ2aXNhZG8uDQotIE1hY2hpbmUgTGVhcm5pbmcgcG9yIHJlZnVlcnpvLg0KDQoNCiMjIyBNYWNoaW5lIExlYXJuaW5nIHN1cGVydmlzYWRvDQoNCkVsIE1hY2hpbmUgTGVhcm5pbmcvYXByZW5kaXphamUgc3VwZXJ2aXNhZG8gdHJhYmFqYSBjb24gZGF0b3MgZXRpcXVldGFkb3MsIGVzIGRlY2lyLCBkYXRvcyBwYXJhIGxvcyBxdWUgeWEgY29ub2NlIGxhIHJlc3B1ZXN0YSBkZSBkZXN0aW5vLiBFbiBiYXNlIGEgdW4gaGlzdMOzcmljbywgdHJhdGEgZGUgYnVzY2FyIHBhdHJvbmVzIHJlbGFjaW9uw6FuZG9sb3MgY29uIHVuIGNhbXBvIGVzcGVjaWFsLCBsbGFtYWRvIG9iamV0aXZvLCBkYWRhcyB1bmFzIHZhcmlhYmxlcyBkZSBlbnRyYWRhLg0KDQpBIHRyYXbDqXMgZGUgZGljaG8gaGlzdMOzcmljbyBkZSBkYXRvcywgZWwgYWxnb3JpdG1vIHB1ZWRlIGFwcmVuZGVyIGEgYXNpZ25hciB1bmEgZXRpcXVldGEgZGUgc2FsaWRhIG8gZnVuY2nDs24gcXVlIGxlIHBlcm1pdGEgcHJlZGVjaXIgZWwgYXRyaWJ1dG8gb2JqZXRpdm8gcGFyYSB1bmEgbnVldmEgYWNjacOzbi4NCg0KRWwgYXByZW5kaXphamUgc3VwZXJ2aXNhZG8gc2UgdXRpbGl6YSBlbiBkb3MgdGlwb3MgZGUgcHJvYmxlbWFzOiBjbGFzaWZpY2FjacOzbiAocG9yIGVqZW1wbG8sIGRldGVjY2nDs24gZGUgZnJhdWRlKSB5IHJlZ3Jlc2nDs24gKHBvciBlamVtcGxvLCBwcmVkaWNjaW9uZXMgbWV0ZW9yb2zDs2dpY2FzKS4NCg0KDQojIyMgTWFjaGluZSBMZWFybmluZyBubyBzdXBlcnZpc2Fkbw0KDQpFbCBNYWNoaW5lIExlYXJuaW5nL2FwcmVuZGl6YWplIG5vIHN1cGVydmlzYWRvIGVzdMOhIGZvcm1hZG8gcG9yIGRhdG9zIGRlIGVudHJhZGEsIHBlcm8gbm8gc2UgY29ub2NlbiBsb3MgZGF0b3MgZGUgc2FsaWRhLCBlcyBkZWNpciwgbm8gY3VlbnRhIGNvbiB1biBjb25qdW50byBkZSBkYXRvcyBldGlxdWV0YWRvcyBwYXJhIGxhcyBwcnVlYmFzIGRlIGVudHJlbmFtaWVudG8uIEVzdGUgbW9kZWxvIHN1ZWxlIHV0aWxpemFyc2UgY29tbyBtw6l0b2RvIGV4cGxvcmF0b3JpbyBvIGRlIGFuw6FsaXNpcy4NCg0KTG9zIHVzb3MgbcOhcyBjb211bmVzIGRlIGVzdGUgdGlwbyBkZSBhcHJlbmRpemFqZSBzb246IHByb2JsZW1hcyBkZSBjbHVzdGVyaW5nLCBhZ3J1cGFtaWVudG9zIGRlIGNvLW9jdXJyZW5jaWEgbyBwcm9maWxpbmcgZGUgZGF0b3MuDQoNCg0KIyMjIE1hY2hpbmUgTGVhcm5pbmcgc2VtaXN1cGVydmlzYWRvDQoNCkVsIE1hY2hpbmUgTGVhcm5pbmcvYXByZW5kaXphamUgc2VtaXN1cGVydmlzYWRvIGVzIHVuYSB0w6ljbmljYSBkZSBhcHJlbmRpemFqZSBhdXRvbcOhdGljbyBxdWUgdXRpbGl6YSBkYXRvcyBkZSBlbnRyZW5hbWllbnRvIHRhbnRvIGV0aXF1ZXRhZG9zIGNvbW8gbm8gZXRpcXVldGFkb3MuDQoNCiMjIyBNYWNoaW5lIExlYXJuaW5nIHBvciByZWZ1ZXJ6bw0KDQpQb3Igw7psdGltbywgZWwgTWFjaGluZSBMZWFybmluZy9hcHJlbmRpemFqZSBwb3IgcmVmdWVyem8gc2UgYmFzYSBlbiB1biBzaXN0ZW1hIGRlIHBydWViYSB5IGVycm9yLiBTdSBvYmpldGl2byBwcmluY2lwYWwgZXMgZ2VuZXJhciB1biBhcHJlbmRpemFqZSBxdWUgcGVybWl0YSBvYnRlbmVyIHVuYSByZWNvbXBlbnNhIGVzcGVjw61maWNhIGEgbWVkaW8tbGFyZ28gcGxhem8uDQoNCg0KDQojIFTDqWNuaWNhcyBkZSBjbGFzaWZpY2FjacOzbiANCg0KTGFzIHTDqWNuaWNhcyBkZSBjbGFzaWZpY2FjacOzbiBzb24gdW5hIHBhcnRlIGVzZW5jaWFsIGRlIE1hY2hpbmUgTGVhcm5pbmcsIHlhIHF1ZSBhcHJveGltYWRhbWVudGUgZWwgNzAlIGRlIGxvcyBwcm9ibGVtYXMgc29uIGRlIGNsYXNpZmljYWNpw7NuLiBIYXkgbXVjaG9zIGFsZ29yaXRtb3MgZGUgY2xhc2lmaWNhY2nDs24sIHBlcm8gbGEgUmVncmVzacOzbiBMb2fDrXN0aWNhIGVzIGNvbcO6biB5IGVzIHVuIG3DqXRvZG8gZGUgcmVncmVzacOzbiDDunRpbCBwYXJhIHJlc29sdmVyIHByb2JsZW1hcyBkZSBjbGFzaWZpY2FjacOzbiBiaW5hcmlhLg0KDQo8Y2VudGVyPiAgICAgIDwhLS1IVE1MIFRhZ3MgIyMgZXRpcXVldGFzIGh0bWwgLS0+IA0KIVtdKFRlY25pY2FDbGFzaWZpY2FjaW9uLmpwZyl7d2lkdGg9NjAwfQ0KPC9jZW50ZXI+DQoNCiMgTGEgUmVncmVzacOzbiBMb2fDrXN0aWNhIA0KDQpMYSBSZWdyZXNpw7NuIExvZ8Otc3RpY2EgZXMgdW4gbcOpdG9kbyBlc3RhZMOtc3RpY28gcGFyYSBwcmVkZWNpciBjbGFzZXMgYmluYXJpYXMuIEVsIHJlc3VsdGFkbyBvIHZhcmlhYmxlIG9iamV0aXZvIGVzIGRlIG5hdHVyYWxlemEgZGljb3TDs21pY2EuIERpY290w7NtaWNhIHNpZ25pZmljYSBxdWUgc29sbyBoYXkgZG9zIGNsYXNlcyBwb3NpYmxlcy4gUG9yIGVqZW1wbG8sIHNlIHB1ZWRlIHV0aWxpemFyIHBhcmEgcHJvYmxlbWFzIGRlIGRldGVjY2nDs24gZGUgY8OhbmNlciBvIGNhbGN1bGFyIGxhIHByb2JhYmlsaWRhZCBkZSBxdWUgb2N1cnJhIHVuIGV2ZW50by4NCg0KTGEgUmVncmVzacOzbiBMb2fDrXN0aWNhIGVzIHVubyBkZSBsb3MgYWxnb3JpdG1vcyBkZSBNYWNoaW5lIExlYXJuaW5nIG3DoXMgc2ltcGxlcyB5IG3DoXMgdXRpbGl6YWRvcyBwYXJhIGxhIGNsYXNpZmljYWNpw7NuIGRlIGRvcyBjbGFzZXMuIEVzIGbDoWNpbCBkZSBpbXBsZW1lbnRhciB5IHNlIHB1ZWRlIHVzYXIgY29tbyBsw61uZWEgZGUgYmFzZSBwYXJhIGN1YWxxdWllciBwcm9ibGVtYSBkZSBjbGFzaWZpY2FjacOzbiBiaW5hcmlhLiBMYSBSZWdyZXNpw7NuIExvZ8Otc3RpY2EgZGVzY3JpYmUgeSBlc3RpbWEgbGEgcmVsYWNpw7NuIGVudHJlIHVuYSB2YXJpYWJsZSBiaW5hcmlhIGRlcGVuZGllbnRlIHkgbGFzIHZhcmlhYmxlcyBpbmRlcGVuZGllbnRlcy4NCg0KPGNlbnRlcj4gICAgICA8IS0tSFRNTCBUYWdzICMjIGV0aXF1ZXRhcyBodG1sIC0tPiANCiFbXShSZWdyZXNpb25Mb2dpc3RpY2FfMS5qcGcpe3dpZHRoPTYwMH0NCjwvY2VudGVyPg0KDQoNCkVuIGdlbmVyYWwsIGVzdGUgYWxnb3JpdG1vIHNlIHB1ZWRlIHV0aWxpemFyIHBhcmEgdmFyaW9zIHByb2JsZW1hcyBkZSBjbGFzaWZpY2FjacOzbiwgY29tbyBsYSBkZXRlY2Npw7NuIGRlIHNwYW0sIHByZWRpY2Npw7NuIGRlIGxhIGRpYWJldGVzLCBzaSB1biBjbGllbnRlIGRldGVybWluYWRvIGNvbXByYXLDoSB1biBwcm9kdWN0byBlbiBwYXJ0aWN1bGFyIG8gc2kgc2UgaXLDoSBjb24gbGEgY29tcGV0ZW5jaWEsIGhheSBtdWNob3MgbcOhcyBlamVtcGxvcyBlbiBkb25kZSBzZSBwdWVkZSBhcGxpY2FyIGVzdGUgYWxnb3JpdG1vLg0KDQpQb3Igc3UgcGFydGUgbGEgUmVncmVzacOzbiBMb2fDrXN0aWNhIGxsZXZhIGVsIG5vbWJyZSBkZSBsYSBmdW5jacOzbiB1dGlsaXphZGEgZW4gZWwgbsO6Y2xlbyBkZWwgbcOpdG9kbywgbGEgZnVuY2nDs24gbG9nw61zdGljYSBlcyB0YW1iacOpbiBsbGFtYWRhIGZ1bmNpw7NuIFNpZ21vaWRlLiBFc3RhIGZ1bmNpw7NuIGVzIHVuYSBjdXJ2YSBlbiBmb3JtYSBkZSBTIHF1ZSBwdWVkZSB0b21hciBjdWFscXVpZXIgbsO6bWVybyBkZSB2YWxvciByZWFsIHkgYXNpZ25hciBhIHVuIHZhbG9yIGVudHJlIDAgeSAxLg0KDQoNCjxjZW50ZXI+ICAgICAgPCEtLUhUTUwgVGFncyAjIyBldGlxdWV0YXMgaHRtbCAtLT4gDQohW10oUmVncmVzaW9uTG9naXN0aWNhXzIuanBnKXt3aWR0aD02MDB9DQo8L2NlbnRlcj4NCg0KDQpTaSBsYSBjdXJ2YSB2YSBhIGluZmluaXRvIHBvc2l0aXZvIGxhIHByZWRpY2Npw7NuIHNlIGNvbnZlcnRpcsOhIGVuIDEsIHkgc2kgbGEgY3VydmEgcGFzYSBlbCBpbmZpbml0byBuZWdhdGl2bywgbGEgcHJlZGljY2nDs24gc2UgY29udmVydGlyw6EgZW4gMC4gU2kgbGEgc2FsaWRhIGRlIGxhIGZ1bmNpw7NuIFNpZ21vaWRlIGVzIG1heW9yIHF1ZSAwLjUsIHBvZGVtb3MgY2xhc2lmaWNhciBlbCByZXN1bHRhZG8gY29tbyAxIG8gU0ksIHkgc2kgZXMgbWVub3IgcXVlIDAuNSBwb2RlbW9zIGNsYXNpZmljYXJsbyBjb21vIDAgbyBOTy4gUG9yIHN1IHBhcnRlIHNpIGVsIHJlc3VsdGFkbyBlcyAwLjc1LCBwb2RlbW9zIGRlY2lyIGVuIHTDqXJtaW5vcyBkZSBwcm9iYWJpbGlkYWQgY29tbywgaGF5IHVuIDc1JSBkZSBwcm9iYWJpbGlkYWRlcyBkZSBxdWUgZWwgcGFjaWVudGUgc3VmcmEgY8OhbmNlci4NCg0KUGVybyB2ZWFtb3MgZXN0ZSBhbGdvcml0bW8gZGUgbWFuZXJhIG1hdGVtw6F0aWNhLCBsYSBlY3VhY2nDs24gZGUgUmVncmVzacOzbiBMaW5lYWwgZXMgZXN0w6E6DQoNCjxjZW50ZXI+ICAgICAgPCEtLUhUTUwgVGFncyAjIyBldGlxdWV0YXMgaHRtbCAtLT4gDQohW10oUmVncmVzaW9uTG9naXN0aWNhXzMuanBnKXt3aWR0aD02MDB9DQo8L2NlbnRlcj4NCg0KRG9uZGUg4oCceeKAnSBlcyBsYSB2YXJpYWJsZSBkZXBlbmRpZW50ZSB5IOKAnHgxLCB4Miwg4oCm4oCdIHNvbiB2YXJpYWJsZXMgaW5kZXBlbmRpZW50ZXMgbyBleHBsaWNhdGl2YXMuDQoNClBvciBzdSBwYXJ0ZSwgbGEgZWN1YWNpw7NuIGRlIGxhIGZ1bmNpw7NuIFNpZ21vaWRlIGVzIGxhIHNpZ3VpZW50ZToNCg0KPGNlbnRlcj4gICAgICA8IS0tSFRNTCBUYWdzICMjIGV0aXF1ZXRhcyBodG1sIC0tPiANCiFbXShSZWdyZXNpb25Mb2dpc3RpY2FfNC5qcGcpe3dpZHRoPTYwMH0NCjwvY2VudGVyPg0KDQpFbnRvbmNlcyBzaSBhcGxpY2Ftb3MgbGEgZnVuY2nDs24gU2lnbW9pZGUgZW4gbGEgUmVncmVzacOzbiBMaW5lYWwgbm9zIHF1ZWRhcsOtYSBhbGdvIGNvbW8gZXN0bzoNCg0KJCQNCnA9XGZyYWN7MX17MStlXnstXGxlZnQoYV97MX0geF97MX0rYV97Mn0geF97Mn0rYV97bn0geF97bn0rYlxyaWdodCl9fQ0KJCQNCg0KDQoNCiMgRGlmZXJlbmNpYXMgZW50cmUgUmVncmVzacOzbiBMaW5lYWwgeSBSZWdyZXNpw7NuIExvZ8Otc3RpY2ENCg0KTGEgUmVncmVzacOzbiBMaW5lYWwgcHJvcG9yY2lvbmEgdW5hIHNhbGlkYSBjb250aW51YSwgcGVybyBsYSBSZWdyZXNpw7NuIExvZ8Otc3RpY2EgcHJvcG9yY2lvbmEgdW5hIHNhbGlkYSBkaXNjcmV0YS4gVW4gZWplbXBsbyBkZSB1bmEgc2FsaWRhIGNvbnRpbnVhIGVzIGNvbm9jZXIgZWwgcG9yY2VudGFqZSBkZSBwcm9iYWJpbGlkYWQgZGUgbGx1dmlhIG8gZWwgcHJlY2lvIGRlIHVuYSBhY2Npw7NuLiBVbiBlamVtcGxvIGRlIHVuYSBzYWxpZGEgZGlzY3JldGEsIHBvciBzdSBwYXJ0ZSwgZXMgY29ub2NlciBzaSB2YSBhIGxsb3ZlciBvIG5vLCBvIHNpIGVsIHByZWNpbyBkZSB1bmEgYWNjacOzbiBzdWJpcsOhIG8gbm8uDQoNCg0KIyBUaXBvcyBkZSBSZWdyZXNpw7NuIExvZ8Otc3RpY2ENCg0KLSBSZWdyZXNpw7NuIExvZ8Otc3RpY2EgQmluYXJpYTogbGEgdmFyaWFibGUgb2JqZXRpdm8gdGllbmUgc29sbyBkb3MgcmVzdWx0YWRvcyBwb3NpYmxlLCBMbHVldmUgbyBOTyBMbHVldmUsIFN1YmUgbyBCYWphLg0KLSBSZWdyZXNpw7NuIExvZ8Otc3RpY2EgTXVsdGlub21pYWw6IGxhIHZhcmlhYmxlIG9iamV0aXZvIHRpZW5lIHRyZXMgbyBtw6FzIGNhdGVnb3LDrWFzIG5vbWluYWxlcywgY29tbyBwcmVkZWNpciBlbCB0aXBvIGRlIHZpbm8uDQotIFJlZ3Jlc2nDs24gTG9nw61zdGljYSBPcmRpbmFsOiBsYSB2YXJpYWJsZSBvYmpldGl2byB0aWVuZSB0cmVzIG8gbcOhcyBjYXRlZ29yw61hcyBvcmRpbmFsZXMsIGNvbW8gY2xhc2lmaWNhciB1biByZXN0YXVyYW50ZSBvIHVuIHByb2R1Y3RvIGRlbCAxIGFsIDUuDQoNCg0KRW4gcmVzdW1lbiBsYSBSZWdyZXNpw7NuIExvZ8Otc3RpY2EgZXMgZWwgYWxnb3JpdG1vIGRlIE1hY2hpbmUgTGVhcm5pbmcgbcOhcyBmYW1vc28gZGVzcHXDqXMgZGUgbGEgUmVncmVzacOzbiBMaW5lYWwsIGVzIHVuIGFsZ29yaXRtbyBzaW1wbGUgcXVlIHNlIHB1ZWRlIHV0aWxpemFyIHBhcmEgdGFyZWFzIGRlIGNsYXNpZmljYWNpw7NuIGJpbmFyaWFzIHkgbXVsdGl2YXJpYWRhcy4NCg0KPGNlbnRlcj4NCjxpZnJhbWUgd2lkdGg9IjU2MCIgaGVpZ2h0PSIzMTUiIHNyYz0iaHR0cHM6Ly93d3cueW91dHViZS5jb20vZW1iZWQvWi1iRnN5aVF4YjAiIHRpdGxlPSJZb3VUdWJlIHZpZGVvIHBsYXllciIgZnJhbWVib3JkZXI9IjAiIGFsbG93PSJhY2NlbGVyb21ldGVyOyBhdXRvcGxheTsgY2xpcGJvYXJkLXdyaXRlOyBlbmNyeXB0ZWQtbWVkaWE7IGd5cm9zY29wZTsgcGljdHVyZS1pbi1waWN0dXJlIiBhbGxvd2Z1bGxzY3JlZW4gZGF0YS1leHRlcm5hbD0xPjwvaWZyYW1lPg0KDQo8L2NlbnRlcj4NCg0KDQoNCiMgRWplbXBsbyBkZSBSZWdyZXNpb24gTG9naXN0aWNhIHVzYW5kbyBQeXRob24gLSBTa2xlYXJuDQoNCkVuIGVzdGEgc2VjY2lvbiBleHBsaWNhcmVtb3MgbGEgcGFydGUgcHLDoWN0aWNhIGRlbCBhbGdvcml0bW8gZGUgUmVncmVzacOzbiBMb2fDrXN0aWNhLCBlbiBkb25kZSBkZXNhcnJvbGxhcmVtb3MgdW4gbW9kZWxvIHBhcmEgcHJlZGVjaXIgZWwgbnVtZXJvIGVuIHVuYSBpbWFnZW4gZGUgOHg4Lg0KDQpQYXJhIGVzdGUgYW7DoWxpc2lzIHZhbW9zIGEgdXRpbGl6YXIgdW5vIGRlIGxvcyBkYXRhc2V0IHF1ZSBzZSBlbmN1ZW50cmEgZGlzcG9uaWJsZSBlbiBsYSBsaWJyZXLDrWEgc2Npa2l0LWxlYXJuIHkgZXMgZWwgY29ycmVzcG9uZGllbnRlIGEgKioqZGlnaXRzICoqKg0KDQpTaSBubyBsbyBzYWJpYXMgZGVudHJvIGRlIGxhIGxpYnJlcsOtYSBkZSBQeXRob24gc2Npa2l0LWxlYXJuIGRpc3BvbmVzIGRlIHZhcmlvcyBkYXRhc2V0LCBjb24gbG9zIHF1ZSBwdWVkZXMgcHJhY3RpY2FyIHR1cyBjb25vY2ltaWVudG9zIGRlIE1hY2hpbmUgTGVhcm5pbmcuIFB1ZWRlcyBlbmNvbnRyYXIgdGFudG8gcGFyYSBwcm9ibGVtYXMgZGUgcmVncmVzacOzbiwgY29tbyBwYXJhIHByb2JsZW1hcyBkZSBjbGFzaWZpY2FjacOzbi4NCg0KQSBjb250aW51YWNpb24gY29tZW50YW1vcyBjYWRhIHNlY2Npb24gZGVsIGNvZGlnbyBkZSBweXRob24NCg0KUHJpbWVybyBjYXJnYW1vcyBsb3MgbW9kdWxvcyB5IGRhdGFzZXQgcXVlIHVzYXJhIHBhcmEgZWwgYW5hbGlzaXMuDQpgYGBQeXRob24NCmltcG9ydCBtYXRwbG90bGliLnB5cGxvdCBhcyBwbHQNCmZyb20gc2tsZWFybi5kYXRhc2V0cyBpbXBvcnQgbG9hZF9kaWdpdHMNCg0KZGlnaXRzID0gbG9hZF9kaWdpdHMoKQ0KYGBgDQoNClNlIGltcHJpbWUgdW4gZGVzY3JpcHRpdm8gY29uIG1hcyBkZXRhbGxlIGFjZXJjYSBkZWwgZGF0YXNldCAqKipkaWdpdHMqKioNCmBgYFB5dGhvbg0KcHJpbnQoZGlnaXRzLkRFU0NSKQ0KDQpgYGANCg0KYGBgDQouLiBfZGlnaXRzX2RhdGFzZXQ6DQoNCk9wdGljYWwgcmVjb2duaXRpb24gb2YgaGFuZHdyaXR0ZW4gZGlnaXRzIGRhdGFzZXQNCi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tDQoNCioqRGF0YSBTZXQgQ2hhcmFjdGVyaXN0aWNzOioqDQoNCiAgICA6TnVtYmVyIG9mIEluc3RhbmNlczogMTc5Nw0KICAgIDpOdW1iZXIgb2YgQXR0cmlidXRlczogNjQNCiAgICA6QXR0cmlidXRlIEluZm9ybWF0aW9uOiA4eDggaW1hZ2Ugb2YgaW50ZWdlciBwaXhlbHMgaW4gdGhlIHJhbmdlIDAuLjE2Lg0KICAgIDpNaXNzaW5nIEF0dHJpYnV0ZSBWYWx1ZXM6IE5vbmUNCiAgICA6Q3JlYXRvcjogRS4gQWxwYXlkaW4gKGFscGF5ZGluICdAJyBib3VuLmVkdS50cikNCiAgICA6RGF0ZTogSnVseTsgMTk5OA0KDQpUaGlzIGlzIGEgY29weSBvZiB0aGUgdGVzdCBzZXQgb2YgdGhlIFVDSSBNTCBoYW5kLXdyaXR0ZW4gZGlnaXRzIGRhdGFzZXRzDQpodHRwczovL2FyY2hpdmUuaWNzLnVjaS5lZHUvbWwvZGF0YXNldHMvT3B0aWNhbCtSZWNvZ25pdGlvbitvZitIYW5kd3JpdHRlbitEaWdpdHMNCg0KVGhlIGRhdGEgc2V0IGNvbnRhaW5zIGltYWdlcyBvZiBoYW5kLXdyaXR0ZW4gZGlnaXRzOiAxMCBjbGFzc2VzIHdoZXJlDQplYWNoIGNsYXNzIHJlZmVycyB0byBhIGRpZ2l0Lg0KDQpQcmVwcm9jZXNzaW5nIHByb2dyYW1zIG1hZGUgYXZhaWxhYmxlIGJ5IE5JU1Qgd2VyZSB1c2VkIHRvIGV4dHJhY3QNCm5vcm1hbGl6ZWQgYml0bWFwcyBvZiBoYW5kd3JpdHRlbiBkaWdpdHMgZnJvbSBhIHByZXByaW50ZWQgZm9ybS4gRnJvbSBhDQp0b3RhbCBvZiA0MyBwZW9wbGUsIDMwIGNvbnRyaWJ1dGVkIHRvIHRoZSB0cmFpbmluZyBzZXQgYW5kIGRpZmZlcmVudCAxMw0KdG8gdGhlIHRlc3Qgc2V0LiAzMngzMiBiaXRtYXBzIGFyZSBkaXZpZGVkIGludG8gbm9ub3ZlcmxhcHBpbmcgYmxvY2tzIG9mDQo0eDQgYW5kIHRoZSBudW1iZXIgb2Ygb24gcGl4ZWxzIGFyZSBjb3VudGVkIGluIGVhY2ggYmxvY2suIFRoaXMgZ2VuZXJhdGVzDQphbiBpbnB1dCBtYXRyaXggb2YgOHg4IHdoZXJlIGVhY2ggZWxlbWVudCBpcyBhbiBpbnRlZ2VyIGluIHRoZSByYW5nZQ0KMC4uMTYuIFRoaXMgcmVkdWNlcyBkaW1lbnNpb25hbGl0eSBhbmQgZ2l2ZXMgaW52YXJpYW5jZSB0byBzbWFsbA0KZGlzdG9ydGlvbnMuDQoNCkZvciBpbmZvIG9uIE5JU1QgcHJlcHJvY2Vzc2luZyByb3V0aW5lcywgc2VlIE0uIEQuIEdhcnJpcywgSi4gTC4gQmx1ZSwgRy4NClQuIENhbmRlbGEsIEQuIEwuIERpbW1pY2ssIEouIEdlaXN0LCBQLiBKLiBHcm90aGVyLCBTLiBBLiBKYW5ldCwgYW5kIEMuDQpMLiBXaWxzb24sIE5JU1QgRm9ybS1CYXNlZCBIYW5kcHJpbnQgUmVjb2duaXRpb24gU3lzdGVtLCBOSVNUSVIgNTQ2OSwNCjE5OTQuDQoNCi4uIHRvcGljOjogUmVmZXJlbmNlcw0KDQogIC0gQy4gS2F5bmFrICgxOTk1KSBNZXRob2RzIG9mIENvbWJpbmluZyBNdWx0aXBsZSBDbGFzc2lmaWVycyBhbmQgVGhlaXINCiAgICBBcHBsaWNhdGlvbnMgdG8gSGFuZHdyaXR0ZW4gRGlnaXQgUmVjb2duaXRpb24sIE1TYyBUaGVzaXMsIEluc3RpdHV0ZSBvZg0KICAgIEdyYWR1YXRlIFN0dWRpZXMgaW4gU2NpZW5jZSBhbmQgRW5naW5lZXJpbmcsIEJvZ2F6aWNpIFVuaXZlcnNpdHkuDQogIC0gRS4gQWxwYXlkaW4sIEMuIEtheW5hayAoMTk5OCkgQ2FzY2FkaW5nIENsYXNzaWZpZXJzLCBLeWJlcm5ldGlrYS4NCiAgLSBLZW4gVGFuZyBhbmQgUG9ubnV0aHVyYWkgTi4gU3VnYW50aGFuIGFuZCBYaSBZYW8gYW5kIEEuIEthaSBRaW4uDQogICAgTGluZWFyIGRpbWVuc2lvbmFsaXR5cmVkdWN0aW9uIHVzaW5nIHJlbGV2YW5jZSB3ZWlnaHRlZCBMREEuIFNjaG9vbCBvZg0KICAgIEVsZWN0cmljYWwgYW5kIEVsZWN0cm9uaWMgRW5naW5lZXJpbmcgTmFueWFuZyBUZWNobm9sb2dpY2FsIFVuaXZlcnNpdHkuDQogICAgMjAwNS4NCiAgLSBDbGF1ZGlvIEdlbnRpbGUuIEEgTmV3IEFwcHJveGltYXRlIE1heGltYWwgTWFyZ2luIENsYXNzaWZpY2F0aW9uDQogICAgQWxnb3JpdGhtLiBOSVBTLiAyMDAwLg0KYGBgDQoNClZpc3VhbGljZW1vcyBhbGd1bmFzIGNvbXBvbmVudGVzIGRlIG51ZXN0cmEgZGF0YS4NClZlYW1vcyBsYXMgcHJpbWVyYXMgMTAgY29tcG9uZW50ZXMgZGUgZGlnaXRzLmltYWdlcyB5IGRpZ2l0cy50YXJnZXQuDQoNCmBgYFB5dGhvbg0KcGx0LmZpZ3VyZShmaWdzaXplID0gKDIwLDQpKQ0KZm9yIGkgaW4gcmFuZ2UoMCw1KToNCiAgICAjcHJpbnQoaSkNCiAgICBwbHQuc3VicGxvdCgxLDUsIGkrMSkNCiAgICBwbHQuaW1zaG93KGRpZ2l0cy5pbWFnZXNbaV0sIGNtYXAgPSBwbHQuY20uZ3JheSkNCiAgICBwbHQudGl0bGUoImRhdGEgOiAlaSBcbiIgJWRpZ2l0cy50YXJnZXRbaV0sIGZvbnRzaXplID0gMjApDQogICAgDQoNCms9NQ0KcGx0LmZpZ3VyZShmaWdzaXplID0gKDIwLDQpKQ0KZm9yIGkgaW4gcmFuZ2UoMCw1KToNCiAgICAjcHJpbnQoaSkNCiAgICBqPWludChpK2spDQogICAgcGx0LnN1YnBsb3QoMSw1LCBpKzEpDQogICAgcGx0Lmltc2hvdyhkaWdpdHMuaW1hZ2VzW2pdLCBjbWFwID0gcGx0LmNtLmdyYXkpDQogICAgcGx0LnRpdGxlKCJkYXRhIDogJWkgXG4iICVkaWdpdHMudGFyZ2V0W2pdLCBmb250c2l6ZSA9IDIwKQ0KDQpgYGANCjxjZW50ZXI+ICAgICAgPCEtLUhUTUwgVGFncyAjIyBldGlxdWV0YXMgaHRtbCAtLT4gDQohW10oRmlndXJlXzEucG5nKQ0KPC9jZW50ZXI+DQoNCjxjZW50ZXI+ICAgICAgPCEtLUhUTUwgVGFncyAjIyBldGlxdWV0YXMgaHRtbCAtLT4gDQohW10oRmlndXJlXzIucG5nKQ0KPC9jZW50ZXI+DQoNCg0KQWhvcmEgdmFtb3MgYSBwcm9jZWRlciBhIGRlZmluaXIgbGFzIHZhcmlhYmxlcyBkZSDigJx44oCdIHkg4oCceeKAnSBxdWUgdmFtb3MgZW1wbGVhciBlbiBudWVzdHJvIG1vZGVsby4NCg0KUGFyYSAqKuKAnHjigJ0qKiB2YW1vcyBhIHV0aWxpemFyIHRvZGFzIGxhcyB2YXJpYWJsZXMgcXVlIHNlIGVuY3VlbnRyYW4gZGVudHJvIGRlICoqKmRpZ2l0cy5kYXRhKioqLCBQb3Igc3UgcGFydGUsICoq4oCceeKAnSoqIHNlcsOhIGlndWFsIGEgbG9zIGRhdG9zIGNvcnJlc3BvbmRpZW50ZXMgYSAqKipkaWdpdHMudGFyZ2V0KioqLg0KDQpEZWZpbmlkbyAqKuKAnHjigJ0qKiB5ICoq4oCceeKAnSoqIHlhIHBvZGVtb3MgcmVhbGl6YXIgbGEgc2VwYXJhY2nDs24gY29ycmVzcG9uZGllbnRlIGEgbG9zIGRhdG9zIGRlIHBydWViYSB5IGVudHJlbmFtaWVudG8gcGFyYSBlbGxvIGltcG9ydGFtb3MgbGEgcmVzcGVjdGl2YSBsaWJyZXLDrWEgeSBwcm9jZWRlbW9zIGEgdXRpbGl6YXIgdHJhaW5fdGVzdF9zcGxpdCBwYXJhIHNlcGFyYXIgbG9zIGRhdG9zLg0KDQpQYXJhIGxhIHNlcGFyYWNpw7NuIGRlIGxvcyBkYXRvcywgdmFtb3MgYSB0b21hciB1biAyNSUgZGUgbG9zIG1pc21vcyBwYXJhIHV0aWxpemFybG9zIGNvbW8gcHJ1ZWJhIHVuYSB2ZXogcXVlIGhheWFtb3Mgb2J0ZW5pZG8gZWwgbW9kZWxvLg0KDQpgYGBQeXRob24NCiMgZGF0b3MgbnVtZXJpY29zIGRlIGxhIGltYWdlbiwgZW4gdW4gZm9ybWF0byBsaW5lYWwsY29tbyA2NCB2YXJpYWJsZXMgaW5kZXBlbmRpZW50ZXMNCmRpZ2l0c1siZGF0YSJdWzBdDQojIHJlc3VsdGFkbyAsIHZhcmlhYmxlIG9iamV0aXZvICwgbGFiZWwgLCBlc3RlIHNlcmlhIG1pIHZhcmlhYmxlIGRlcGVuZGllbnRlDQpkaWdpdHMudGFyZ2V0WzBdDQoNCiMgQ29uc3RydWltb3MgZWwgZGF0YXNldCB0cmFpbiB5IHRlc3QNCmZyb20gc2tsZWFybi5tb2RlbF9zZWxlY3Rpb24gaW1wb3J0IHRyYWluX3Rlc3Rfc3BsaXQNCnhfdHJhaW4sIHhfdGVzdCwgeV90cmFpbiwgeV90ZXN0ID0gdHJhaW5fdGVzdF9zcGxpdChkaWdpdHMuZGF0YSwgZGlnaXRzLnRhcmdldCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0ZXN0X3NpemU9MC4yNSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICByYW5kb21fc3RhdGU9IDY2NikNCg0KYGBgDQoNCllhIGVuIGVzdGUgbW9tZW50byB0ZW5lbW9zIG51ZXN0cm9zIGRhdG9zIGxpc3RvcyBwYXJhIGVtcGV6YXIgYSBjb25zdHJ1aXIgZWwgbW9kZWxvLg0KDQpMbyBwcmltZXJvIHF1ZSBkZWJlbW9zIGhhY2VyIGVzIGltcG9ydGFyIExvZ2lzdGljUmVncmVzc2lvbiBxdWUgc2UgZW5jdWVudHJhIGRlbnRybyBkZSBsYSBsaWJyZXLDrWEgbGluZWFyX21vZGVsLCB5IGEgc3UgdmV6IGRlZmluaW1vcyBlbCBhbGdvcml0bW8gKioibG9naXN0aWMxIioqIC4NCg0KYGBgUHl0aG9uDQojRGVmaW5vIGVsIGFsZ29yaXRtbyAibG9naXN0aWMxIiBhIHV0aWxpemFyDQpmcm9tIHNrbGVhcm4ubGluZWFyX21vZGVsIGltcG9ydCBMb2dpc3RpY1JlZ3Jlc3Npb24NCmxvZ2lzdGljMSA9IExvZ2lzdGljUmVncmVzc2lvbihtYXhfaXRlciA9IDIwMDAsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHZlcmJvc2UgPSAxLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0b2wgPSAxZS02KQ0KDQpgYGANCg0KDQpTZWd1aWRhbWVudGUgZW50cmVuYW1vcyBlbCBtb2RlbG8gdXRpbGl6YW5kbyBsYSBpbnN0cnVjY2nDs24gZml0IHkgbG9zIGRhdG9zIHRhbnRvIGRlICoq4oCcWOKAnSoqIGNvbW8gZGUgKirigJx54oCdKiogZGUgZW50cmVuYW1pZW50by4NCg0KYGBgUHl0aG9uDQojRW50cmVubyBlbCBtb2RlbG8NCmxvZ2lzdGljMS5maXQoeF90cmFpbiwgeV90cmFpbikNCg0KYGBgDQoNCg0KWSBmaW5hbG1lbnRlIHJlYWxpemFtb3MgdW5hIHByZWRpY2Npw7NuLCB1dGlsaXphbmRvIGxhIGluc3RydWNjacOzbiBwcmVkaWN0IHkgbG9zIGRhdG9zIGRlIHBydWViYS4NCg0KDQpgYGBQeXRob24NCiMgSGFnYW1vcyBsYSBwcmVkaWNjaW9uIHBhcmEgdG9kb3MgbG9zIGRhdG9zIGRlIHBydWViYSANCnByZWRpY3Rpb25zID0gbG9naXN0aWMxLnByZWRpY3QoeF90ZXN0KQ0KDQpgYGANCg0KQ2FsY3VsYW1vcyBsYSBwcmVjaXNpw7NuIGRlbCBhbGdvcml0bW8gKioibG9naXN0aWMxIioqDQoNCmBgYFB5dGhvbg0KIyBzY29yZQ0Kc2NvcmUgPSBsb2dpc3RpYzEuc2NvcmUoeF90ZXN0LCB5X3Rlc3QpDQpzY29yZQ0KDQpgYGANCmBgYA0Kc2NvcmUNCk91dFsxM106IDAuOTY4ODg4ODg4ODg4ODg4OQ0KDQpgYGANCg0KDQpBZGVtYXMgY2FsY3VsYW1vcyB5IHZpc3VhbGl6YW1vcyBsYSBNYXRyaXogZGUgQ29uZnVzaW9uLg0KDQoNCmBgYFB5dGhvbg0KIyBNYXRyaXogZGUgY29uZnVzaW9uIA0KZnJvbSBza2xlYXJuIGltcG9ydCBtZXRyaWNzDQpjbSA9IG1ldHJpY3MuY29uZnVzaW9uX21hdHJpeCh5X3Rlc3QsIHByZWRpY3Rpb25zKQ0KY20NCg0KIyBWaXN1YWxpY2Vtb3MgbGEgbWF0cml6IGRlIGNvbmZ1c2lvbg0KaW1wb3J0IHNlYWJvcm4gYXMgc25zDQpwbHQuZmlndXJlKGZpZ3NpemUgPSAoOSw5KSkNCnNucy5oZWF0bWFwKGNtLCBhbm5vdCA9IFRydWUsIGZtdCA9ICIuM2YiLA0KICAgICAgICAgICAgbGluZXdpZHRocz0wLjUsIHNxdWFyZSA9IFRydWUsDQogICAgICAgICAgICBjbWFwID0gIkJsdWVzX3IiKQ0KcGx0LnlsYWJlbCgiRGF0b3MgUmVhbGVzICh5X3Rlc3QpIikNCnBsdC54bGFiZWwoIlByZWRpY2Npb25lcyBkZWwgbW9kZWxvICIpDQpwbHQudGl0bGUoIlNjb3JlIHswfSIuZm9ybWF0KHNjb3JlKSkNCg0KYGBgDQoNCjxjZW50ZXI+ICAgICAgPCEtLUhUTUwgVGFncyAjIyBldGlxdWV0YXMgaHRtbCAtLT4gDQohW10oRmlndXJlXzQucG5nKQ0KPC9jZW50ZXI+DQoNCg0KRWwgcmVzdWx0YWRvIGRlbCBzY29yZSB5IE1hdHJpeiBkZSBjb25mdXNpb24gY29uZmlybWFuIHF1ZSBlbCBtb2RlbG8gb2J0ZW5pZG8gZXMgYnVlbm8uDQoNCkZpbmFsbWVudGUgYWRqdXRhbW9zIGVsIGNvZGlnbyBjb21wbGV0byBkZSBweXRob246DQoNCg0KYGBgUHl0aG9uDQojICUlIFJlZ3Jlc2lvbiBsb2dpc3RpY2EgY29uIGRpZ2l0cw0KDQojIENhcmd1ZW1vcyBtb2R1bG9zIHkgbGlicmVyaWFzDQppbXBvcnQgbWF0cGxvdGxpYi5weXBsb3QgYXMgcGx0DQpmcm9tIHNrbGVhcm4uZGF0YXNldHMgaW1wb3J0IGxvYWRfZGlnaXRzDQoNCiMgQ2FyZ2Ftb3MgbG9zIGRhdG9zDQpkaWdpdHMgPSBsb2FkX2RpZ2l0cygpDQoNCiMgRGVzY3JpcGNpb24gZGVsIGRhdGFzZXQgZGlnaXRzDQpwcmludChkaWdpdHMuREVTQ1IpDQoNCg0KDQojICUlIFZpc3VhbGljZW1vcyBhbGd1bmFzIGNvbXBvbmVudGVzIGRlIG51ZXN0cmEgZGF0YSANCiMgVmVhbW9zIGxhcyBwcmltZXJhcyAxMCBjb21wb25lbnRlcyBkZSBkaWdpdHMuaW1hZ2VzIHkgZGlnaXRzLnRhcmdldA0KDQpwbHQuZmlndXJlKGZpZ3NpemUgPSAoMjAsNCkpDQpmb3IgaSBpbiByYW5nZSgwLDUpOg0KICAgICNwcmludChpKQ0KICAgIHBsdC5zdWJwbG90KDEsNSwgaSsxKQ0KICAgIHBsdC5pbXNob3coZGlnaXRzLmltYWdlc1tpXSwgY21hcCA9IHBsdC5jbS5ncmF5KQ0KICAgIHBsdC50aXRsZSgiZGF0YSA6ICVpIFxuIiAlZGlnaXRzLnRhcmdldFtpXSwgZm9udHNpemUgPSAyMCkNCiAgICANCg0Kaz01DQpwbHQuZmlndXJlKGZpZ3NpemUgPSAoMjAsNCkpDQpmb3IgaSBpbiByYW5nZSgwLDUpOg0KICAgICNwcmludChpKQ0KICAgIGo9aW50KGkraykNCiAgICBwbHQuc3VicGxvdCgxLDUsIGkrMSkNCiAgICBwbHQuaW1zaG93KGRpZ2l0cy5pbWFnZXNbal0sIGNtYXAgPSBwbHQuY20uZ3JheSkNCiAgICBwbHQudGl0bGUoImRhdGEgOiAlaSBcbiIgJWRpZ2l0cy50YXJnZXRbal0sIGZvbnRzaXplID0gMjApDQoNCg0KICAgDQoNCg0KIyAlJSBDb25zdHJ1Y2Npb24gZGVsIG1vZGVsbyANCiMgZGF0b3MgbnVtZXJpY29zIGRlIGxhIGltYWdlbiwgZW4gdW4gZm9ybWF0byBsaW5lYWwsY29tbyA2NCB2YXJpYWJsZXMgaW5kZXBlbmRpZW50ZXMNCmRpZ2l0c1siZGF0YSJdWzBdDQojIHJlc3VsdGFkbyAsIHZhcmlhYmxlIG9iamV0aXZvICwgbGFiZWwgLCBlc3RlIHNlcmlhIG1pIHZhcmlhYmxlIGRlcGVuZGllbnRlDQpkaWdpdHMudGFyZ2V0WzBdDQoNCg0KIyBDb25zdHJ1aW1vcyBlbCBkYXRhc2V0IHRyYWluIHkgdGVzdA0KZnJvbSBza2xlYXJuLm1vZGVsX3NlbGVjdGlvbiBpbXBvcnQgdHJhaW5fdGVzdF9zcGxpdA0KeF90cmFpbiwgeF90ZXN0LCB5X3RyYWluLCB5X3Rlc3QgPSB0cmFpbl90ZXN0X3NwbGl0KGRpZ2l0cy5kYXRhLCBkaWdpdHMudGFyZ2V0LA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRlc3Rfc2l6ZT0wLjI1LA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJhbmRvbV9zdGF0ZT0gNjY2KQ0KDQojRGVmaW5vIGVsIGFsZ29yaXRtbyAibG9naXN0aWMxIiBhIHV0aWxpemFyDQpmcm9tIHNrbGVhcm4ubGluZWFyX21vZGVsIGltcG9ydCBMb2dpc3RpY1JlZ3Jlc3Npb24NCmxvZ2lzdGljMSA9IExvZ2lzdGljUmVncmVzc2lvbihtYXhfaXRlciA9IDIwMDAsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHZlcmJvc2UgPSAxLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0b2wgPSAxZS02KQ0KI0VudHJlbm8gZWwgbW9kZWxvDQpsb2dpc3RpYzEuZml0KHhfdHJhaW4sIHlfdHJhaW4pDQoNCg0KDQojICUlIA0KDQojIEhhZ2Ftb3MgbGEgcHJlZGljY2lvbiBwYXJhIHRvZG9zIGxvcyBkYXRvcyBkZSBwcnVlYmEgDQpwcmVkaWN0aW9ucyA9IGxvZ2lzdGljMS5wcmVkaWN0KHhfdGVzdCkNCg0KIyAlJSBNZWRpciBlbCByZW5kaW1pZW50by9wZXJmb3JtYW5jZSBkZWwgbW9kZWxvIDogbG9naXN0aWMxDQoNCiMgc2NvcmUNCnNjb3JlID0gbG9naXN0aWMxLnNjb3JlKHhfdGVzdCwgeV90ZXN0KQ0Kc2NvcmUNCg0KIyBNYXRyaXogZGUgY29uZnVzaW9uIA0KZnJvbSBza2xlYXJuIGltcG9ydCBtZXRyaWNzDQpjbSA9IG1ldHJpY3MuY29uZnVzaW9uX21hdHJpeCh5X3Rlc3QsIHByZWRpY3Rpb25zKQ0KY20NCg0KIyBWaXN1YWxpY2Vtb3MgbGEgbWF0cml6IGRlIGNvbmZ1c2lvbg0KaW1wb3J0IHNlYWJvcm4gYXMgc25zDQpwbHQuZmlndXJlKGZpZ3NpemUgPSAoOSw5KSkNCnNucy5oZWF0bWFwKGNtLCBhbm5vdCA9IFRydWUsIGZtdCA9ICIuM2YiLA0KICAgICAgICAgICAgbGluZXdpZHRocz0wLjUsIHNxdWFyZSA9IFRydWUsDQogICAgICAgICAgICBjbWFwID0gIkJsdWVzX3IiKQ0KcGx0LnlsYWJlbCgiRGF0b3MgUmVhbGVzICh5X3Rlc3QpIikNCnBsdC54bGFiZWwoIlByZWRpY2Npb25lcyBkZWwgbW9kZWxvICIpDQpwbHQudGl0bGUoIlNjb3JlIHswfSIuZm9ybWF0KHNjb3JlKSkNCg0KDQoNCmBgYA0K