Optimasi

~ Ujian Akhir Semester ~


Kontak : \(\downarrow\)
Email
Instagram yyosia
RPubs https://rpubs.com/yosia/

Kasus Optimisasi Program Linear dalam Industri atau bisnis

PT. HARAPAN TEKSTIL memiliki sebuah pabrik yang akan memproduksi 2 jenis produk, yaitu kain sutera dan kain wol. Untuk memproduksi kedua produk diperlukan bahan baku benang sutera, bahan baku benang wol dan tenaga kerja. Maksimum penyediaan benang sutera adalah 60 kg per hari, benang wol 30 kg per hari dan tenaga kerja 40 jam per hari. Kebutuhan setiap unit produk akan bahan baku dan jam tenaga kerja dapat dilihat dalam tabel berikut :


Kedua jenis produk memberikan keuntungan sebesar Rp.40.000.000,- untuk kain sutera dan Rp.30.000.000,- untuk kain wol. Masalahnya adalah bagaimana menentukan jumlah unit setiap jenis produk yang akan diproduksi setiap hari agar keuntungan yang diperoleh bisa maksimal?

  1. Menentukan Variable

\[x_1= kain \space sutera\] \[x_2 = kain \space wol\]

  1. Fungsi Tujuan

\[Z_{max} = 40x_1 + 30x_2\]

  1. Kendala

\[\begin{align*} 2x_1 + 3x_2 & \leq 60 \space (benang sutera)\\ 2x_2 & \leq 30 \space (benang wol) \\ 2x_1 + x_2 & \leq 40 \space (tenaga kerja) \\ \end{align*}\]

R

library (lpSolve)
# Memasukkan koefisien dari fungsi tujuan pada f.obj
f.obj <- c(40, 30)

# Memasukkan koefiesian fungsi kendala dalam bentuk matriks 
# Dengan nrow menunjukkan banyaknya kendala yaitu 3 dan angka yang 
# diinput disusun perbaris sehingga byrow = TRUE
f.con <- matrix(c(2, 3,
                  0, 2,
                  2, 1), nrow = 3, byrow = TRUE)

# Memasukkan tanda pertidaksamaan pada setiap kendala
f.dir <- c("<=",
           "<=",
           "<=")

# Memasukkan koefisien ruas kanan
f.rhs <- c(60,
           30,
           40)
# Keuntungan Maksimum

lp("max", f.obj, f.con, f.dir, f.rhs)
## Success: the objective function is 900
# Nilai Variabel agar mencapai keuntungan maksimum
lp("max", f.obj, f.con, f.dir, f.rhs)$solution
## [1] 15 10

Nilai z maksimum yang dapat diperoleh saat memenuhi kendala yang diberikan adalah 900

(keuntungan sebesar Rp 900 juta)

di mana

\[x_1 = 15\]

\[x_2=10\]

Python

from pulp import LpMaximize, LpProblem, LpStatus, lpSum, LpVariable, LpMinimize

my_lp_problem = LpProblem("My_LP_Problem", LpMaximize)

x1 = LpVariable('x1', lowBound=0, cat='Continuous')
x2 = LpVariable('x2', lowBound=0, cat='Continuous')

# Objective function
my_lp_problem += 40*x1+30*x2, "Z"

# Constraints
my_lp_problem += 2*x1+3*x2 <= 60
my_lp_problem += 2*x2      <= 100
my_lp_problem += 2*x1+x2   <= 40
my_lp_problem
## My_LP_Problem:
## MAXIMIZE
## 40*x1 + 30*x2 + 0
## SUBJECT TO
## _C1: 2 x1 + 3 x2 <= 60
## 
## _C2: 2 x2 <= 100
## 
## _C3: 2 x1 + x2 <= 40
## 
## VARIABLES
## x1 Continuous
## x2 Continuous
my_lp_problem.solve()
## 1
LpStatus[my_lp_problem.status]
## 'Optimal'
print("Variable x1 = {}".format(x1.varValue))
## Variable x1 = 15.0
print("Variable x2 = {}".format(x2.varValue))
## Variable x2 = 10.0
import pulp
print(pulp.value(my_lp_problem.objective))
## 900.0

Dengan hasil yang sama mendapatkan nilai Maximum dengan Program R

Python juga mendapatkan nilai

\[x_1 = 15\]

\[x_2=10\]

dan \(Z_{max} = 900\) dalam juta

Kasus Optimisasi Program NonLinear dalam Industri atau bisnis!

R

\[Min_z = x_1^2+x_2^2\]

\[\begin{align*} 1-x_1+x_2 & \leq 0\\ 1-x_1^2+x_2^2 & \leq 0\\ 9x_1^2+x_2^2 & \geq 9\\ x_1^2-x_2 & \geq 0\\ x_2^2-x_1 & \geq 0\\ \\ -50\leq \space x_1&,x_2 \space \geq 50 \end{align*}\]

eval_f <- function(x)
{
return ( x[1]^2 + x[2]^2 )
}
eval_g_ineq <- function (x) {
constr <- c(1 - x[1] - x[2],
1 - x[1]^2 - x[2]^2,
9 - 9*x[1]^2 - x[2]^2,
x[2] - x[1]^2,
x[1] - x[2]^2)
return (constr)
}
lb <- c(-50, -50)
ub <- c(50, 50)
x0 <- c(3, 1)
opts <- list( "algorithm" = "NLOPT_GN_ISRES",
"xtol_rel" = 1.0e-15,
"maxeval"= 160000,
"tol_constraints_ineq" = rep( 1.0e-10, 5 ))
library(nloptr)
res <- nloptr(
  x0 = x0,
  eval_f = eval_f,
  lb = lb,
  ub = ub,
  eval_g_ineq = eval_g_ineq,
  opts = opts )
print(res)
## 
## Call:
## 
## nloptr(x0 = x0, eval_f = eval_f, lb = lb, ub = ub, eval_g_ineq = eval_g_ineq, 
##     opts = opts)
## 
## 
## Minimization using NLopt version 2.7.1 
## 
## NLopt solver status: 5 ( NLOPT_MAXEVAL_REACHED: Optimization stopped because 
## maxeval (above) was reached. )
## 
## Number of Iterations....: 160000 
## Termination conditions:  xtol_rel: 1e-15 maxeval: 160000 
## Number of inequality constraints:  5 
## Number of equality constraints:    0 
## Current value of objective function:  1.99999999964032 
## Current value of controls: 1 1

Maka optimal solusi dengan nilai 1.9999 dan \[x_1=1\] \[x_2=1\]

Python

Pemilik restoran membuka kembali bisnis mereka selama pandemi COVID-19, pemerintah membuat indeks risiko kapasitas umum (didefinisikan di bawah), atau singkatnya CRI(capacity risk index) untuk membantu pemilik memahami betapa berisiko membuka area tempat duduk dalam dan luar ruangan mereka.

Mary, seorang pemilik restoran setempat ingin membuka kembali restorannya dengan risiko seminimal mungkin. Namun dia mengerti bahwa untuk memberikan jam kerja yang berharga kepada stafnya, dia harus membuka setidaknya 10% dari kapasitas tempat duduk di dalam ruangan dan 25% dari kapasitas tempat duduk di luar ruangan.

Apa cara optimal (menurut CRI) bagi Mary untuk membuka kembali bisnisnya?”


Kendala

\[Min \space z = 4{x_1}^2 - 4{x_1}^4 + {x_1}^{6/3} + x_1x_2 - 4{x_2}^2 + 4{x_2}^4\]

Subject to : \[\begin{align*} x_1 \leq & \space 0.1\\ x_2 += & \space 0.25 \\ x_1,x_2 \geq & \space 1 \\ \end{align*}\]

Plot

import matplotlib.pyplot as plt
import numpy as np

objective_function = lambda x: (4*x[0]**2) - (4*x[0]**4) + (x[0]**(6/3) + (x[0]*x[1]) - (4*x[1]**2) + (4*x[1]**4))

X_1 = np.linspace(0,1, 100)
X_2 = np.linspace(0,1, 100)
X_1, X_2 = np.meshgrid(X_1, X_2)
Z = objective_function((X_1, X_2))

#Contour plot
fig, ax = plt.subplots()
fig.set_size_inches(14.7, 8.27)

cs = ax.contour(X_1, X_2, Z, 50, cmap='jet')
plt.clabel(cs, inline=1, fontsize=10) # plot objective function

plt.axvline(0.1, color='g', label=r'$x_1 \geq 0.1$') # constraint 1
plt.axhline(0.25, color='r', label=r'$x_2 \geq 0.25$') # constraint 2
plt.legend(bbox_to_anchor=(1, 1), loc=2, borderaxespad=0.1)

# tidy up
plt.xlabel(r'$x_1$', fontsize=16)
plt.ylabel(r'$x_2$', fontsize=16)
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
plt.show()

import random

def geneate_starting_points(number_of_points):
    '''
    number_of_points: how many points we want to generate
    
    returns a list of starting points
    '''
    starting_points = []
    
    for point in range(number_of_points):
        
        starting_points.append((random.random(), random.random()))
        
    return starting_points
  
# define our objective function
objective_function = lambda x: (4*x[0]**2) - (4*x[0]**4) + (x[0]**(6/3) + (x[0]*x[1]) - (4*x[1]**2) + (4*x[1]**4))

# define our constraints
# note that since we have >= constraints we use the 'ineq' constaint type
cons = [
    {'type': 'ineq', 'fun': lambda x: x[0] - 0.1}, # indoor seating >= 0.1
    {'type': 'ineq', 'fun': lambda x: x[1] - 0.25} # outdoor seating >= 0.25
]

# define boundaries
boundaries = [(0,1), (0,1)]

from scipy.optimize import minimize

# generate a list of N potential starting points
starting_points = geneate_starting_points(50)

first_iteration = True
for point in starting_points:
    # for each point run the algorithim
    res = minimize(
        objective_function,
        [point[0], point[1]],
        method='SLSQP',
        bounds=boundaries,
        constraints=cons
    )
    # first iteration always gonna be the best so far
    if first_iteration:
        better_solution_found = False
        best = res
    else:
        # if we find a better solution, lets use it
        if res.success and res.fun < best.fun:
            better_solution_found = True
            best = res
            
# print results if algorithim was successful
if best.success:
    print(f"""Optimal solution found:
      -  Proportion of indoor seating to make available: {round(best.x[0], 3)}
      -  Proportion of outdoor seating to make available: {round(best.x[1], 3)}
      -  Risk index score: {round(best.fun, 3)}""")
else: 
    print("No solution found to problem")
## Optimal solution found:
##       -  Proportion of indoor seating to make available: 0.1
##       -  Proportion of outdoor seating to make available: 0.701
##       -  Risk index score: -0.88

Dalam contoh keluaran di atas, kita dapat melihat bahwa pengoptimal multi-start kita berhasil menemukan minimum global (ketika kita membandingkan hasil yang diharapkan berdasarkan plot kami). Dalam kasus ini kita melihat bahwa Mary dapat meminimalkan CRI-nya menjadi -0,88 dengan membuka 10% tempat duduk di dalam ruangan dan 70% tempat duduk di luar ruangan.

Kasus Optimisasi Portofolio dengan memilih 5 saham terbaik di Indonesia dengan cara melakukan analisis data keuangan terlebih dahulu pada saham LQ45

BBCA.JK = PT Bank Central Asia Tbk

BBNI.JK = PT Bank Negara Indonesia (Persero) Tbk

JSMR.JK = PT Jasa Marga (Persero) Tbk

BBRI.JK = PT Bank Rakyat Indonesia (Persero) Tbk

BSDE.JK = PT Bumi Serpong Damai Tbk

Dalam melakukan Analisis keuangan saham, yang dilakukan memilih Saham yang terdaftar di LQ45, lalu melakukan analisis membandingkan Price Earning Ratio (PER) sebagai merupakan rasio yang digunakan investor untuk menilai saham suatu perusahaan karena PER menjadi perbandingan antara market price per share (harga pasar per lembar saham) dengan earning per share (laba perlembar saham).

Price Earning Ratio bertujuan untuk memberikan investor pengindraan yang lebih baik atau peka terhadap nilai perusahaan yang lebih besar.

Selanjutnya melihat nilai beta pada saham. memilih saham melihat nilai beta adalah fleksible. jadi jika Indeks Harga Saham Gabungan (IHSG) sedang dalam tren turun maka investor harus menghindari nilai beta yang tinggi.

Tetapi nilai beta juga mencerminkan risiko suatu saham, Saham dengan beta tinggi menunjukkan tingkat risiko yang tinggi. Namun, keuntungan pun berbanding lurus dengan risikonya. Sementara, saham dengan beta di bawah 1,0 memiliki risiko lebih kecil, linier dengan tingkat keuntungan.


dilihat dari gambar di atas diketahui bahwa BBCA memiliki nilai Price Earning Ratio yang tertinggi artinya yang memiliki potensi keuntungan terbesar dari 5 Saham di atas.

Selanjutnya perubahan pendapatan dalam 1 Tahun terakhir adalah BBNI yang terbesar sebesar 39.55%

yang terakhir Beta, dilihat dari nilai Beta di atas bahwa yang memberikan risiko paling kecil adalah BBCA dan risiko tertinggi BBNI.

jadi jika dilihat dari tabel diatas (sebelum portofolio) saham BBCA adalah saham yang terbaik, low risk and medium return selanjutnya akan dilakukan portofolio yang akan memudahkan investor memilih saham mana yang akan diinvestasikannya.

R

library(tidyquant) 
library(plotly) 
library(timetk)
tick <- c('BBCA.JK', 'BBRI.JK', 'BBNI.JK', 'JSMR.JK','BSDE.JK')


price_data <- tq_get(tick,
                     from = '2020-01-01',
                     to   = Sys.Date(),
                     get  = 'stock.prices')

head(price_data)

Return

Selanjutnya menghitung pengembalian (Return) harian untuk saham-saham ini dengan menggunakan pengembalian logaritmik (untuk memastikan data stasioner).

log_ret_tidy <- price_data %>%
  group_by(symbol) %>%
  tq_transmute(select     = adjusted,
               mutate_fun = periodReturn,
               period     = 'daily',
               col_rename = 'ret',
               type       = 'log')

library(DT) 
datatable(log_ret_tidy)

Selanjutnya akan digunakan fungsi spread() untuk mengubahnya menjadi format lebar. dan kita juga akan mengubahnya menjadi objek time series menggunakan fungsi xts() .

library(tidyr)

log_ret_xts <- log_ret_tidy %>%
  spread(symbol, value = ret) %>%
  tk_xts()
## Warning: Non-numeric columns being dropped: date
## Using column `date` for date_var.
datatable(log_ret_xts)

Rata-rata Pengembalian

Menghitung rata-rata pengembalian harian untuk setiap aset.

mean_ret <- colMeans(log_ret_xts)
print ( round (mean_ret, 4))
## BBCA.JK BBNI.JK BBRI.JK BSDE.JK JSMR.JK 
##   4e-04   3e-04   3e-04  -4e-04  -8e-04

Matriks Kovariansi

Selanjutnya, menghitung matriks kovarians untuk semua stok dengan mengalikannya dengan 252 dalam format tahunan.

cov_mat <- cov(log_ret_xts) * 252
print(round(cov_mat,4))
##         BBCA.JK BBNI.JK BBRI.JK BSDE.JK JSMR.JK
## BBCA.JK  0.0824  0.0673  0.0632  0.0486  0.0471
## BBNI.JK  0.0673  0.1576  0.1086  0.0864  0.0772
## BBRI.JK  0.0632  0.1086  0.1445  0.0721  0.0611
## BSDE.JK  0.0486  0.0864  0.0721  0.1927  0.0882
## JSMR.JK  0.0471  0.0772  0.0611  0.0882  0.1618

Penerapan Metode Portofolio

  • Bobot acak

  • Rata-rata pengembalian aset

  • Risiko portofolio

  • Bobot portofolio

wts <- runif(n = length(tick))
wts <- wts/sum(wts)

port_returns <- (sum(wts * mean_ret) + 1)^252 - 1
port_risk <- sqrt(t(wts) %*% (cov_mat %*% wts))
sharpe_ratio <- port_returns/port_risk

Selanjutnya dilakukan proses pembetukan portofolio secara acak dengan simulasi 5000 kali untuk memastikan signifikansinya secara statistik. Persiapkan vektor kosong untuk masing-masing langkah diatas:

num_port <- 5000

all_wts <- matrix(nrow = num_port, ncol = length(tick))
port_returns <- vector('numeric', length = num_port)
port_risk <- vector('numeric', length = num_port)
sharpe_ratio <- vector('numeric', length = num_port)

Selanjutnya mari kita jalankan for loop 5000 kali.

for (i in seq_along(port_returns)) {
  
  wts <- runif(length(tick))
  wts <- wts/sum(wts)
  
  all_wts[i,] <- wts
  
  
  port_ret <- sum(wts * mean_ret)
  port_ret <- ((port_ret + 1)^252) - 1
  
  port_returns[i] <- port_ret
  
  
  port_sd <- sqrt(t(wts) %*% (cov_mat  %*% wts))
  port_risk[i] <- port_sd
  
  sr <- port_ret/port_sd
  sharpe_ratio[i] <- sr
  
}

Selanjutnya, membuat tabel data untuk menyimpan semua nilai secara bersamaan.

portfolio_values <- tibble(Return = port_returns,
                             Risk = port_risk,
                      SharpeRatio = sharpe_ratio)


all_wts <- tk_tbl(all_wts)
## Warning in tk_tbl.data.frame(as.data.frame(data), preserve_index,
## rename_index, : Warning: No index to preserve. Object otherwise converted to
## tibble successfully.
colnames(all_wts) <- colnames(log_ret_xts)

portfolio_values <- tk_tbl(cbind(all_wts, portfolio_values))
## Warning in tk_tbl.data.frame(cbind(all_wts, portfolio_values)): Warning: No
## index to preserve. Object otherwise converted to tibble successfully.
datatable(portfolio_values)

Sekarang, kita sudah memiliki bobot di setiap aset dengan risiko dan pengembalian bersama dengan Sharpe ratio dari setiap portofolio. Selanjutnya, mari kita lihat portofolio yang paling penting dengan varians minimum dan juga Sharpe ratio tertinggi.

Variansi Minimum

library(forcats)

min_var <- portfolio_values[which.min(portfolio_values$Risk),]

p <- min_var %>%
  gather(BBCA.JK:JSMR.JK, key = Asset,
         value = Weights) %>%
  mutate(Asset = as.factor(Asset)) %>%
  ggplot(aes(x = fct_reorder(Asset,Weights), y = Weights, fill = Asset)) +
  geom_bar(stat = 'identity') +
  theme_minimal() +
  labs(x = 'Aset', 
       y = 'Bobot', 
       title = "Bobot Portofolio dengan Variansi Minimum") +
  scale_y_continuous(labels = scales::percent) +
  theme(legend.position = "none" )

ggplotly(p)

Seperti yang dapat kita amati, portofolio Varian minimum tidak memiliki alokasi untuk BBNI.JK dan BSDE.JK sangat sedikit. Mayoritas portofolio diinvestasikan di saham BBCA.JK, BJSMR.JK & BBRI.JK.

Portofolio Tangensi

Selanjutnya, mari kita lihat portofolio tangency atau portofolio dengan Sharpe ratio tertinggi.

max_sr <- portfolio_values[which.max(portfolio_values$SharpeRatio),]

p <- max_sr %>%
  gather(BBCA.JK:JSMR.JK, key = Asset,
         value = Weights) %>%
  mutate(Asset = as.factor(Asset)) %>%
  ggplot(aes(x = fct_reorder(Asset,Weights), y = Weights, fill = Asset)) +
  geom_bar(stat = 'identity') +
  theme_minimal() +
  labs(x = 'Aset', 
       y = 'Bobot', 
       title = "Bobot Portofolio Tangensi (Maksimum Sharpe Ratio)") +
  scale_y_continuous(labels = scales::percent) +
  theme(legend.position = "none" )

ggplotly(p)

Ternyata dalam rasio sharpe tertinggi BBCA.JK memiliki banyak investasi. dalam 5 saham yang terpilih dengan return dan risk yang optimal BBCA.JK adalah yang terbaik.

Batas Efisien Portofolio

Akhirnya mari kita plot semua portofolio acak dan memvisualisasikan batas efisien.

p <- portfolio_values %>%
  ggplot(aes(x = Risk, y = Return, color = SharpeRatio)) +
  geom_point() +
  theme_classic() +
  scale_y_continuous(labels = scales::percent) +
  scale_x_continuous(labels = scales::percent) +
  labs(x = 'Risiko Tahunan',
       y = 'Pengembalian Tahunan',
       title = "Optimasi Portofolio & Perbatasan yang Efisien") +
  geom_point(aes(x = Risk,y = Return), data = min_var, color = 'red') +
  geom_point(aes(x = Risk,y = Return), data = max_sr, color = 'red') +
  annotate('text', x = 0.29, y = 0.18, label = "Portofolio Tangensi") +
  annotate('text', x = 0.267, y = 0.09, label = "Portofolio Varians minimum") +
  annotate(geom = 'segment', x = 0.28462, xend = 0.28462,  y = 0.09, 
           yend = 0.15, color = 'red', arrow = arrow(type = "open")) +
  annotate(geom = 'segment', x = 0.2679, xend = 0.2679,  y = 0.034, 
           yend = 0.09, color = 'red', arrow = arrow(type = "open"))
  

ggplotly(p)

Python

Import Data

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import date
import yfinance as yf

tick = ['BBCA.JK', 'BBRI.JK', 'BBNI.JK', 'JSMR.JK','BSDE.JK']
price_data = yf.download(tick,  start = '2020-01-01',
                           end = date.today())['Adj Close']
## 
[                       0%                       ]
[*******************   40%                       ]  2 of 5 completed
[**********************60%****                   ]  3 of 5 completed
[**********************80%*************          ]  4 of 5 completed
[*********************100%***********************]  5 of 5 completed
price_data
##                 BBCA.JK      BBNI.JK      BBRI.JK  BSDE.JK      JSMR.JK
## Date                                                                   
## 2020-01-02  6322.238770  7353.527832  4009.360107   1270.0  5155.947754
## 2020-01-03  6426.191406  7377.172852  4018.451416   1290.0  5230.671387
## 2020-01-06  6364.764648  7211.659180  3972.993652   1280.0  5155.947754
## 2020-01-07  6369.490234  7140.725098  4000.268311   1250.0  5056.315918
## 2020-01-08  6312.788086  7022.500488  3982.085449   1220.0  5056.315918
## ...                 ...          ...          ...      ...          ...
## 2022-12-16  8600.000000  9800.000000  4980.000000    945.0  2960.000000
## 2022-12-19  8650.000000  9425.000000  4970.000000    935.0  2990.000000
## 2022-12-20  8575.000000  9450.000000  4910.000000    925.0  2970.000000
## 2022-12-21  8675.000000  9350.000000  4890.000000    915.0  2960.000000
## 2022-12-22  8575.000000  9425.000000  4960.000000    915.0  2960.000000
## 
## [729 rows x 5 columns]

Matriks Kovariansi

log_ret = np.log(price_data/price_data.shift(1))
cov_mat = log_ret.cov() * 252
print(cov_mat)
##           BBCA.JK   BBNI.JK   BBRI.JK   BSDE.JK   JSMR.JK
## BBCA.JK  0.082466  0.067439  0.063327  0.048692  0.047149
## BBNI.JK  0.067439  0.157840  0.108764  0.086518  0.077346
## BBRI.JK  0.063327  0.108764  0.144706  0.072173  0.061220
## BSDE.JK  0.048692  0.086518  0.072173  0.192946  0.088292
## JSMR.JK  0.047149  0.077346  0.061220  0.088292  0.162057
num_port = 5000
all_wts = np.zeros((num_port, len(price_data.columns)))
port_returns = np.zeros((num_port))
port_risk = np.zeros((num_port))
sharpe_ratio = np.zeros((num_port))
for i in range(num_port):
  wts = np.random.uniform(size = len(price_data.columns))
  wts = wts/np.sum(wts)
  
  all_wts[i,:] = wts
  
  port_ret = np.sum(log_ret.mean() * wts)
  port_ret = (port_ret + 1) ** 252 - 1
  
  port_returns[i] = port_ret
  
  port_sd = np.sqrt(np.dot(wts.T, np.dot(cov_mat, wts)))
  
  port_risk[i] = port_sd
  
  sr = port_ret / port_sd
  sharpe_ratio[i] = sr
names = price_data.columns
min_var = all_wts[port_risk.argmin()]
print(min_var)
## [0.67029143 0.04707169 0.02680259 0.12136951 0.13446479]
max_sr = all_wts[sharpe_ratio.argmax()]
print(max_sr)
## [0.65368735 0.3252541  0.00656379 0.00969316 0.00480161]
print(sharpe_ratio.max())
## 0.34648300936703674
print(port_risk.min())
## 0.26882074460454347

Variansi Minimum

import plotly.express as px
min_var = pd.Series(min_var, index=names)
min_var = min_var.sort_values()

fig = px.bar(min_var, color=min_var.index, title="Portofolio dengan Variansi minimum")
fig.show()

Portofolio Tangensi

import plotly.express as px
max_sr = pd.Series(max_sr, index=names)
max_sr = max_sr.sort_values()

fig = px.bar(max_sr, color=max_sr.index, title="Portofolio dengan Tangensi")
fig.show()

Batas Efisien Portofolio

fig = plt.figure()
ax1 = fig.add_axes([0.1,0.1,0.8,0.8])
ax1.set_xlabel('Risk')
ax1.set_ylabel("Returns")
ax1.set_title("Portfolio optimization and Efficient Frontier")
plt.scatter(port_risk, port_returns)
plt.show();

LS0tDQp0aXRsZTogIk9wdGltYXNpIg0Kc3VidGl0bGU6ICJ+IFVqaWFuIEFraGlyIFNlbWVzdGVyIH4iDQphdXRob3I6ICJZb3NpYSINCmRhdGU6ICAiYHIgZm9ybWF0KFN5cy5EYXRlKCksICclQiAlZCwgJVknKWAiDQpvdXRwdXQ6DQogIHJtZGZvcm1hdHM6OnJlYWR0aGVkb3duOiAgICMgaHR0cHM6Ly9naXRodWIuY29tL2p1YmEvcm1kZm9ybWF0cw0KICAgIHNlbGZfY29udGFpbmVkOiB0cnVlDQogICAgdGh1bWJuYWlsczogdHJ1ZQ0KICAgIGxpZ2h0Ym94OiB0cnVlDQogICAgZ2FsbGVyeTogdHJ1ZQ0KICAgIGxpYl9kaXI6IGxpYnMNCiAgICBkZl9wcmludDogInBhZ2VkIg0KICAgIGNvZGVfZm9sZGluZzogInNob3ciDQogICAgY29kZV9kb3dubG9hZDogeWVzDQogICAgY3NzOiAic3R5bGUuY3NzIg0KDQotLS0NCg0KYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9DQpsaWJyYXJ5KHJldGljdWxhdGUpDQpsaWJyYXJ5KFJjcHApDQp1c2VfY29uZGFlbnYoInB5M18xMCIsIHJlcXVpcmVkID0gVFJVRSkNCmBgYA0KDQoNCjxicj4NCg0KPGltZyBzdHlsZT0iZmxvYXQ6IHJpZ2h0OyBtYXJnaW46IDBweCA1MHB4IDBweCA1MHB4OyB3aWR0aDozMCUiIHNyYz0icG90by5qcGciLz4gDQoNCnwNCjotLS0tIHw6LS0tLQ0KKipLb250YWsqKnwgKio6ICRcZG93bmFycm93JCoqDQpFbWFpbHwgeW9zaWEueW9zaWFAc3R1ZGVudC5tYXRhbmF1bml2ZXJzaXR5LmFjLmlkDQpJbnN0YWdyYW0gfCBbeXlvc2lhXShodHRwczovL3d3dy5pbnN0YWdyYW0uY29tL3l5b3NpYS8pIA0KUlB1YnMgIHwgaHR0cHM6Ly9ycHVicy5jb20veW9zaWEvIA0KDQoqKioNCg0KIyBLYXN1cyBPcHRpbWlzYXNpIFByb2dyYW0gTGluZWFyIGRhbGFtIEluZHVzdHJpIGF0YXUgYmlzbmlzIHsudGFic2V0IC50YWJzZXQtZmFkZSAudGFic2V0LXBpbGxzfQ0KDQpQVC4gSEFSQVBBTiBURUtTVElMIG1lbWlsaWtpIHNlYnVhaCBwYWJyaWsgeWFuZyBha2FuIG1lbXByb2R1a3NpIDIgamVuaXMgcHJvZHVrLCB5YWl0dSBrYWluIHN1dGVyYSBkYW4ga2FpbiB3b2wuIFVudHVrIG1lbXByb2R1a3NpIGtlZHVhIHByb2R1ayBkaXBlcmx1a2FuIGJhaGFuIGJha3UgYmVuYW5nIHN1dGVyYSwgYmFoYW4gYmFrdSBiZW5hbmcgd29sIGRhbiB0ZW5hZ2Ega2VyamEuIE1ha3NpbXVtIHBlbnllZGlhYW4gYmVuYW5nIHN1dGVyYSBhZGFsYWggNjAga2cgcGVyIGhhcmksIGJlbmFuZyB3b2wgMzAga2cgcGVyIGhhcmkgZGFuIHRlbmFnYSBrZXJqYSA0MCBqYW0gcGVyIGhhcmkuIEtlYnV0dWhhbiBzZXRpYXAgdW5pdCBwcm9kdWsgYWthbiBiYWhhbiBiYWt1IGRhbiBqYW0gdGVuYWdhIGtlcmphIGRhcGF0IGRpbGloYXQgZGFsYW0gdGFiZWwgYmVyaWt1dCA6IA0KDQo8aW1nIHN0eWxlPSJmbG9hdDogY2VudGVyOyBtYXJnaW46IDBweCA1MHB4IDBweCA1MHB4OyB3aWR0aDo4MCUiIHNyYz0ibHAucG5nIi8+DQoNCjxicj4NCg0KS2VkdWEgamVuaXMgcHJvZHVrIG1lbWJlcmlrYW4ga2V1bnR1bmdhbiBzZWJlc2FyIFJwLjQwLjAwMC4wMDAsLSB1bnR1ayBrYWluIHN1dGVyYSBkYW4gUnAuMzAuMDAwLjAwMCwtIHVudHVrIGthaW4gd29sLiBNYXNhbGFobnlhIGFkYWxhaCBiYWdhaW1hbmEgbWVuZW50dWthbiBqdW1sYWggdW5pdCBzZXRpYXAgamVuaXMgcHJvZHVrIHlhbmcgYWthbiBkaXByb2R1a3NpIHNldGlhcCBoYXJpIGFnYXIga2V1bnR1bmdhbiB5YW5nIGRpcGVyb2xlaCBiaXNhIG1ha3NpbWFsPw0KDQoxLiBNZW5lbnR1a2FuIFZhcmlhYmxlDQoNCiQkeF8xPSBrYWluIFxzcGFjZSBzdXRlcmEkJA0KJCR4XzIgPSBrYWluIFxzcGFjZSB3b2wkJA0KDQoyLiBGdW5nc2kgVHVqdWFuDQoNCiQkWl97bWF4fSA9IDQweF8xICsgMzB4XzIkJA0KDQozLiBLZW5kYWxhDQoNClxiZWdpbnthbGlnbip9DQogICAgIDJ4XzEgICsgM3hfMiAmIFxsZXEgIDYwIFxzcGFjZSAoYmVuYW5nIHN1dGVyYSlcXCANCiAgICAgMnhfMiAgICAgICAgICYgXGxlcSAgMzAgXHNwYWNlIChiZW5hbmcgd29sKSAgIFxcDQogICAgIDJ4XzEgICsgeF8yICAmIFxsZXEgIDQwIFxzcGFjZSAodGVuYWdhIGtlcmphKSBcXCAgICAgDQpcZW5ke2FsaWduKn0NCg0KIyMgUg0KDQpgYGB7cn0NCmxpYnJhcnkgKGxwU29sdmUpDQojIE1lbWFzdWtrYW4ga29lZmlzaWVuIGRhcmkgZnVuZ3NpIHR1anVhbiBwYWRhIGYub2JqDQpmLm9iaiA8LSBjKDQwLCAzMCkNCg0KIyBNZW1hc3Vra2FuIGtvZWZpZXNpYW4gZnVuZ3NpIGtlbmRhbGEgZGFsYW0gYmVudHVrIG1hdHJpa3MgDQojIERlbmdhbiBucm93IG1lbnVuanVra2FuIGJhbnlha255YSBrZW5kYWxhIHlhaXR1IDMgZGFuIGFuZ2thIHlhbmcgDQojIGRpaW5wdXQgZGlzdXN1biBwZXJiYXJpcyBzZWhpbmdnYSBieXJvdyA9IFRSVUUNCmYuY29uIDwtIG1hdHJpeChjKDIsIDMsDQogICAgICAgICAgICAgICAgICAwLCAyLA0KICAgICAgICAgICAgICAgICAgMiwgMSksIG5yb3cgPSAzLCBieXJvdyA9IFRSVUUpDQoNCiMgTWVtYXN1a2thbiB0YW5kYSBwZXJ0aWRha3NhbWFhbiBwYWRhIHNldGlhcCBrZW5kYWxhDQpmLmRpciA8LSBjKCI8PSIsDQogICAgICAgICAgICI8PSIsDQogICAgICAgICAgICI8PSIpDQoNCiMgTWVtYXN1a2thbiBrb2VmaXNpZW4gcnVhcyBrYW5hbg0KZi5yaHMgPC0gYyg2MCwNCiAgICAgICAgICAgMzAsDQogICAgICAgICAgIDQwKQ0KYGBgDQoNCmBgYHtyfQ0KIyBLZXVudHVuZ2FuIE1ha3NpbXVtDQoNCmxwKCJtYXgiLCBmLm9iaiwgZi5jb24sIGYuZGlyLCBmLnJocykNCg0KIyBOaWxhaSBWYXJpYWJlbCBhZ2FyIG1lbmNhcGFpIGtldW50dW5nYW4gbWFrc2ltdW0NCmxwKCJtYXgiLCBmLm9iaiwgZi5jb24sIGYuZGlyLCBmLnJocykkc29sdXRpb24NCmBgYA0KDQpOaWxhaSB6IG1ha3NpbXVtIHlhbmcgZGFwYXQgZGlwZXJvbGVoIHNhYXQgbWVtZW51aGkga2VuZGFsYSB5YW5nIGRpYmVyaWthbiBhZGFsYWggOTAwDQoNCihrZXVudHVuZ2FuIHNlYmVzYXIgUnAgOTAwIGp1dGEpDQoNCmRpIG1hbmENCg0KJCR4XzEgPSAxNSQkDQoNCiQkeF8yPTEwJCQNCg0KIyMgUHl0aG9uDQoNCmBgYHtweXRob259DQpmcm9tIHB1bHAgaW1wb3J0IExwTWF4aW1pemUsIExwUHJvYmxlbSwgTHBTdGF0dXMsIGxwU3VtLCBMcFZhcmlhYmxlLCBMcE1pbmltaXplDQoNCm15X2xwX3Byb2JsZW0gPSBMcFByb2JsZW0oIk15X0xQX1Byb2JsZW0iLCBMcE1heGltaXplKQ0KDQp4MSA9IExwVmFyaWFibGUoJ3gxJywgbG93Qm91bmQ9MCwgY2F0PSdDb250aW51b3VzJykNCngyID0gTHBWYXJpYWJsZSgneDInLCBsb3dCb3VuZD0wLCBjYXQ9J0NvbnRpbnVvdXMnKQ0KDQojIE9iamVjdGl2ZSBmdW5jdGlvbg0KbXlfbHBfcHJvYmxlbSArPSA0MCp4MSszMCp4MiwgIloiDQoNCiMgQ29uc3RyYWludHMNCm15X2xwX3Byb2JsZW0gKz0gMip4MSszKngyIDw9IDYwDQpteV9scF9wcm9ibGVtICs9IDIqeDIgICAgICA8PSAxMDANCm15X2xwX3Byb2JsZW0gKz0gMip4MSt4MiAgIDw9IDQwDQpgYGANCg0KYGBge3B5dGhvbn0NCm15X2xwX3Byb2JsZW0NCmBgYA0KDQpgYGB7cHl0aG9ufQ0KbXlfbHBfcHJvYmxlbS5zb2x2ZSgpDQpMcFN0YXR1c1tteV9scF9wcm9ibGVtLnN0YXR1c10NCmBgYA0KDQpgYGB7cHl0aG9ufQ0KcHJpbnQoIlZhcmlhYmxlIHgxID0ge30iLmZvcm1hdCh4MS52YXJWYWx1ZSkpDQpwcmludCgiVmFyaWFibGUgeDIgPSB7fSIuZm9ybWF0KHgyLnZhclZhbHVlKSkNCmBgYA0KDQpgYGB7cHl0aG9ufQ0KaW1wb3J0IHB1bHANCnByaW50KHB1bHAudmFsdWUobXlfbHBfcHJvYmxlbS5vYmplY3RpdmUpKQ0KYGBgDQoNCkRlbmdhbiBoYXNpbCB5YW5nIHNhbWEgbWVuZGFwYXRrYW4gbmlsYWkgTWF4aW11bSBkZW5nYW4gUHJvZ3JhbSBSDQoNClB5dGhvbiBqdWdhIG1lbmRhcGF0a2FuIG5pbGFpDQoNCiQkeF8xID0gMTUkJA0KDQokJHhfMj0xMCQkDQoNCmRhbiAkWl97bWF4fSA9IDkwMCQgZGFsYW0ganV0YQ0KDQojIEthc3VzIE9wdGltaXNhc2kgUHJvZ3JhbSBOb25MaW5lYXIgZGFsYW0gSW5kdXN0cmkgYXRhdSBiaXNuaXMhey50YWJzZXQgLnRhYnNldC1mYWRlIC50YWJzZXQtcGlsbHN9DQoNCiMjIFINCg0KJCRNaW5feiA9IHhfMV4yK3hfMl4yJCQNCg0KXGJlZ2lue2FsaWduKn0NCiAgICAgMS14XzEreF8yICAgICAmIFxsZXEgIDBcXCANCiAgICAgMS14XzFeMit4XzJeMiAmIFxsZXEgIDBcXA0KICAgICA5eF8xXjIreF8yXjIgICYgXGdlcSAgOVxcIA0KICAgICB4XzFeMi14XzIgICAgICYgXGdlcSAgMFxcDQogICAgIHhfMl4yLXhfMSAgICAgJiBcZ2VxICAwXFwNCiAgICAgXFwNCiAgICAgLTUwXGxlcSBcc3BhY2UgeF8xJix4XzIgXHNwYWNlIFxnZXEgNTANClxlbmR7YWxpZ24qfQ0KDQpgYGB7cn0NCmV2YWxfZiA8LSBmdW5jdGlvbih4KQ0Kew0KcmV0dXJuICggeFsxXV4yICsgeFsyXV4yICkNCn0NCmBgYA0KDQpgYGB7cn0NCmV2YWxfZ19pbmVxIDwtIGZ1bmN0aW9uICh4KSB7DQpjb25zdHIgPC0gYygxIC0geFsxXSAtIHhbMl0sDQoxIC0geFsxXV4yIC0geFsyXV4yLA0KOSAtIDkqeFsxXV4yIC0geFsyXV4yLA0KeFsyXSAtIHhbMV1eMiwNCnhbMV0gLSB4WzJdXjIpDQpyZXR1cm4gKGNvbnN0cikNCn0NCmxiIDwtIGMoLTUwLCAtNTApDQp1YiA8LSBjKDUwLCA1MCkNCngwIDwtIGMoMywgMSkNCmBgYA0KDQpgYGB7cn0NCm9wdHMgPC0gbGlzdCggImFsZ29yaXRobSIgPSAiTkxPUFRfR05fSVNSRVMiLA0KInh0b2xfcmVsIiA9IDEuMGUtMTUsDQoibWF4ZXZhbCI9IDE2MDAwMCwNCiJ0b2xfY29uc3RyYWludHNfaW5lcSIgPSByZXAoIDEuMGUtMTAsIDUgKSkNCmBgYA0KDQpgYGB7cn0NCmxpYnJhcnkobmxvcHRyKQ0KcmVzIDwtIG5sb3B0cigNCiAgeDAgPSB4MCwNCiAgZXZhbF9mID0gZXZhbF9mLA0KICBsYiA9IGxiLA0KICB1YiA9IHViLA0KICBldmFsX2dfaW5lcSA9IGV2YWxfZ19pbmVxLA0KICBvcHRzID0gb3B0cyApDQpwcmludChyZXMpDQpgYGANCg0KTWFrYSBvcHRpbWFsIHNvbHVzaSBkZW5nYW4gbmlsYWkgMS45OTk5IGRhbg0KJCR4XzE9MSQkDQokJHhfMj0xJCQNCg0KIyMgUHl0aG9uDQoNClBlbWlsaWsgcmVzdG9yYW4gbWVtYnVrYSBrZW1iYWxpIGJpc25pcyBtZXJla2Egc2VsYW1hIHBhbmRlbWkgQ09WSUQtMTksIHBlbWVyaW50YWggbWVtYnVhdCBpbmRla3MgcmlzaWtvIGthcGFzaXRhcyB1bXVtIChkaWRlZmluaXNpa2FuIGRpIGJhd2FoKSwgYXRhdSBzaW5na2F0bnlhIENSSShjYXBhY2l0eSByaXNrIGluZGV4KSB1bnR1ayBtZW1iYW50dSBwZW1pbGlrIG1lbWFoYW1pIGJldGFwYSBiZXJpc2lrbyBtZW1idWthIGFyZWEgdGVtcGF0IGR1ZHVrIGRhbGFtIGRhbiBsdWFyIHJ1YW5nYW4gbWVyZWthLg0KDQpNYXJ5LCBzZW9yYW5nIHBlbWlsaWsgcmVzdG9yYW4gc2V0ZW1wYXQgaW5naW4gbWVtYnVrYSBrZW1iYWxpIHJlc3RvcmFubnlhIGRlbmdhbiByaXNpa28gc2VtaW5pbWFsIG11bmdraW4uIE5hbXVuIGRpYSBtZW5nZXJ0aSBiYWh3YSB1bnR1ayBtZW1iZXJpa2FuIGphbSBrZXJqYSB5YW5nIGJlcmhhcmdhIGtlcGFkYSBzdGFmbnlhLCBkaWEgaGFydXMgbWVtYnVrYSBzZXRpZGFrbnlhIDEwJSBkYXJpIGthcGFzaXRhcyB0ZW1wYXQgZHVkdWsgZGkgZGFsYW0gcnVhbmdhbiBkYW4gMjUlIGRhcmkga2FwYXNpdGFzIHRlbXBhdCBkdWR1ayBkaSBsdWFyIHJ1YW5nYW4uDQoNCkFwYSBjYXJhIG9wdGltYWwgKG1lbnVydXQgQ1JJKSBiYWdpIE1hcnkgdW50dWsgbWVtYnVrYSBrZW1iYWxpIGJpc25pc255YT/igJ0NCg0KPGltZyBzdHlsZT0iZmxvYXQ6IGNlbnRlcjsgbWFyZ2luOiAwcHggNTBweCAwcHggNTBweDsgd2lkdGg6ODAlIiBzcmM9Im5vbmxwLnBuZyIvPg0KDQo8YnI+DQoNCioqS2VuZGFsYSoqDQoNCiQkTWluIFxzcGFjZSB6ID0gNHt4XzF9XjIgLSA0e3hfMX1eNCArIHt4XzF9Xns2LzN9ICsgeF8xeF8yIC0gNHt4XzJ9XjIgKyA0e3hfMn1eNCQkDQoNCioqU3ViamVjdCB0byA6KioNClxiZWdpbnthbGlnbip9DQogICAgIHhfMSAgIFxsZXEgICYgXHNwYWNlIDAuMVxcIA0KICAgICB4XzIgICAgICAgICAgKz0gICYgXHNwYWNlIDAuMjUgICBcXA0KICAgICB4XzEseF8yICAgXGdlcSAgJiBcc3BhY2UgMSBcXCAgICAgDQpcZW5ke2FsaWduKn0NCg0KKipQbG90KiogDQoNCmBgYHtweXRob259DQppbXBvcnQgbWF0cGxvdGxpYi5weXBsb3QgYXMgcGx0DQppbXBvcnQgbnVtcHkgYXMgbnANCg0Kb2JqZWN0aXZlX2Z1bmN0aW9uID0gbGFtYmRhIHg6ICg0KnhbMF0qKjIpIC0gKDQqeFswXSoqNCkgKyAoeFswXSoqKDYvMykgKyAoeFswXSp4WzFdKSAtICg0KnhbMV0qKjIpICsgKDQqeFsxXSoqNCkpDQoNClhfMSA9IG5wLmxpbnNwYWNlKDAsMSwgMTAwKQ0KWF8yID0gbnAubGluc3BhY2UoMCwxLCAxMDApDQpYXzEsIFhfMiA9IG5wLm1lc2hncmlkKFhfMSwgWF8yKQ0KWiA9IG9iamVjdGl2ZV9mdW5jdGlvbigoWF8xLCBYXzIpKQ0KDQojQ29udG91ciBwbG90DQpmaWcsIGF4ID0gcGx0LnN1YnBsb3RzKCkNCmZpZy5zZXRfc2l6ZV9pbmNoZXMoMTQuNywgOC4yNykNCg0KY3MgPSBheC5jb250b3VyKFhfMSwgWF8yLCBaLCA1MCwgY21hcD0namV0JykNCnBsdC5jbGFiZWwoY3MsIGlubGluZT0xLCBmb250c2l6ZT0xMCkgIyBwbG90IG9iamVjdGl2ZSBmdW5jdGlvbg0KDQpwbHQuYXh2bGluZSgwLjEsIGNvbG9yPSdnJywgbGFiZWw9cickeF8xIFxnZXEgMC4xJCcpICMgY29uc3RyYWludCAxDQpwbHQuYXhobGluZSgwLjI1LCBjb2xvcj0ncicsIGxhYmVsPXInJHhfMiBcZ2VxIDAuMjUkJykgIyBjb25zdHJhaW50IDINCnBsdC5sZWdlbmQoYmJveF90b19hbmNob3I9KDEsIDEpLCBsb2M9MiwgYm9yZGVyYXhlc3BhZD0wLjEpDQoNCiMgdGlkeSB1cA0KcGx0LnhsYWJlbChyJyR4XzEkJywgZm9udHNpemU9MTYpDQpwbHQueWxhYmVsKHInJHhfMiQnLCBmb250c2l6ZT0xNikNCmF4LnNwaW5lc1sncmlnaHQnXS5zZXRfdmlzaWJsZShGYWxzZSkNCmF4LnNwaW5lc1sndG9wJ10uc2V0X3Zpc2libGUoRmFsc2UpDQpwbHQuc2hvdygpDQpgYGANCg0KYGBge3B5dGhvbn0NCmltcG9ydCByYW5kb20NCg0KZGVmIGdlbmVhdGVfc3RhcnRpbmdfcG9pbnRzKG51bWJlcl9vZl9wb2ludHMpOg0KICAgICcnJw0KICAgIG51bWJlcl9vZl9wb2ludHM6IGhvdyBtYW55IHBvaW50cyB3ZSB3YW50IHRvIGdlbmVyYXRlDQogICAgDQogICAgcmV0dXJucyBhIGxpc3Qgb2Ygc3RhcnRpbmcgcG9pbnRzDQogICAgJycnDQogICAgc3RhcnRpbmdfcG9pbnRzID0gW10NCiAgICANCiAgICBmb3IgcG9pbnQgaW4gcmFuZ2UobnVtYmVyX29mX3BvaW50cyk6DQogICAgICAgIA0KICAgICAgICBzdGFydGluZ19wb2ludHMuYXBwZW5kKChyYW5kb20ucmFuZG9tKCksIHJhbmRvbS5yYW5kb20oKSkpDQogICAgICAgIA0KICAgIHJldHVybiBzdGFydGluZ19wb2ludHMNCiAgDQojIGRlZmluZSBvdXIgb2JqZWN0aXZlIGZ1bmN0aW9uDQpvYmplY3RpdmVfZnVuY3Rpb24gPSBsYW1iZGEgeDogKDQqeFswXSoqMikgLSAoNCp4WzBdKio0KSArICh4WzBdKiooNi8zKSArICh4WzBdKnhbMV0pIC0gKDQqeFsxXSoqMikgKyAoNCp4WzFdKio0KSkNCg0KIyBkZWZpbmUgb3VyIGNvbnN0cmFpbnRzDQojIG5vdGUgdGhhdCBzaW5jZSB3ZSBoYXZlID49IGNvbnN0cmFpbnRzIHdlIHVzZSB0aGUgJ2luZXEnIGNvbnN0YWludCB0eXBlDQpjb25zID0gWw0KICAgIHsndHlwZSc6ICdpbmVxJywgJ2Z1bic6IGxhbWJkYSB4OiB4WzBdIC0gMC4xfSwgIyBpbmRvb3Igc2VhdGluZyA+PSAwLjENCiAgICB7J3R5cGUnOiAnaW5lcScsICdmdW4nOiBsYW1iZGEgeDogeFsxXSAtIDAuMjV9ICMgb3V0ZG9vciBzZWF0aW5nID49IDAuMjUNCl0NCg0KIyBkZWZpbmUgYm91bmRhcmllcw0KYm91bmRhcmllcyA9IFsoMCwxKSwgKDAsMSldDQoNCmZyb20gc2NpcHkub3B0aW1pemUgaW1wb3J0IG1pbmltaXplDQoNCiMgZ2VuZXJhdGUgYSBsaXN0IG9mIE4gcG90ZW50aWFsIHN0YXJ0aW5nIHBvaW50cw0Kc3RhcnRpbmdfcG9pbnRzID0gZ2VuZWF0ZV9zdGFydGluZ19wb2ludHMoNTApDQoNCmZpcnN0X2l0ZXJhdGlvbiA9IFRydWUNCmZvciBwb2ludCBpbiBzdGFydGluZ19wb2ludHM6DQogICAgIyBmb3IgZWFjaCBwb2ludCBydW4gdGhlIGFsZ29yaXRoaW0NCiAgICByZXMgPSBtaW5pbWl6ZSgNCiAgICAgICAgb2JqZWN0aXZlX2Z1bmN0aW9uLA0KICAgICAgICBbcG9pbnRbMF0sIHBvaW50WzFdXSwNCiAgICAgICAgbWV0aG9kPSdTTFNRUCcsDQogICAgICAgIGJvdW5kcz1ib3VuZGFyaWVzLA0KICAgICAgICBjb25zdHJhaW50cz1jb25zDQogICAgKQ0KICAgICMgZmlyc3QgaXRlcmF0aW9uIGFsd2F5cyBnb25uYSBiZSB0aGUgYmVzdCBzbyBmYXINCiAgICBpZiBmaXJzdF9pdGVyYXRpb246DQogICAgICAgIGJldHRlcl9zb2x1dGlvbl9mb3VuZCA9IEZhbHNlDQogICAgICAgIGJlc3QgPSByZXMNCiAgICBlbHNlOg0KICAgICAgICAjIGlmIHdlIGZpbmQgYSBiZXR0ZXIgc29sdXRpb24sIGxldHMgdXNlIGl0DQogICAgICAgIGlmIHJlcy5zdWNjZXNzIGFuZCByZXMuZnVuIDwgYmVzdC5mdW46DQogICAgICAgICAgICBiZXR0ZXJfc29sdXRpb25fZm91bmQgPSBUcnVlDQogICAgICAgICAgICBiZXN0ID0gcmVzDQogICAgICAgICAgICANCiMgcHJpbnQgcmVzdWx0cyBpZiBhbGdvcml0aGltIHdhcyBzdWNjZXNzZnVsDQppZiBiZXN0LnN1Y2Nlc3M6DQogICAgcHJpbnQoZiIiIk9wdGltYWwgc29sdXRpb24gZm91bmQ6DQogICAgICAtICBQcm9wb3J0aW9uIG9mIGluZG9vciBzZWF0aW5nIHRvIG1ha2UgYXZhaWxhYmxlOiB7cm91bmQoYmVzdC54WzBdLCAzKX0NCiAgICAgIC0gIFByb3BvcnRpb24gb2Ygb3V0ZG9vciBzZWF0aW5nIHRvIG1ha2UgYXZhaWxhYmxlOiB7cm91bmQoYmVzdC54WzFdLCAzKX0NCiAgICAgIC0gIFJpc2sgaW5kZXggc2NvcmU6IHtyb3VuZChiZXN0LmZ1biwgMyl9IiIiKQ0KZWxzZTogDQogICAgcHJpbnQoIk5vIHNvbHV0aW9uIGZvdW5kIHRvIHByb2JsZW0iKQ0KYGBgDQoNCkRhbGFtIGNvbnRvaCBrZWx1YXJhbiBkaSBhdGFzLCBraXRhIGRhcGF0IG1lbGloYXQgYmFod2EgcGVuZ29wdGltYWwgbXVsdGktc3RhcnQga2l0YSBiZXJoYXNpbCBtZW5lbXVrYW4gbWluaW11bSBnbG9iYWwgKGtldGlrYSBraXRhIG1lbWJhbmRpbmdrYW4gaGFzaWwgeWFuZyBkaWhhcmFwa2FuIGJlcmRhc2Fya2FuIHBsb3Qga2FtaSkuIERhbGFtIGthc3VzIGluaSBraXRhIG1lbGloYXQgYmFod2EgTWFyeSBkYXBhdCBtZW1pbmltYWxrYW4gQ1JJLW55YSBtZW5qYWRpIC0wLDg4IGRlbmdhbiBtZW1idWthIDEwJSB0ZW1wYXQgZHVkdWsgZGkgZGFsYW0gcnVhbmdhbiBkYW4gNzAlIHRlbXBhdCBkdWR1ayBkaSBsdWFyIHJ1YW5nYW4uDQoNCiMgS2FzdXMgT3B0aW1pc2FzaSBQb3J0b2ZvbGlvIGRlbmdhbiBtZW1pbGloIDUgc2FoYW0gdGVyYmFpayBkaSBJbmRvbmVzaWEgZGVuZ2FuIGNhcmEgbWVsYWt1a2FuIGFuYWxpc2lzIGRhdGEga2V1YW5nYW4gdGVybGViaWggZGFodWx1IHBhZGEgc2FoYW0gTFE0NSB7LnRhYnNldCAudGFic2V0LWZhZGUgLnRhYnNldC1waWxsc30NCg0KKipCQkNBLkpLKiogPSBQVCBCYW5rIENlbnRyYWwgQXNpYSBUYmsNCg0KKipCQk5JLkpLKiogPSBQVCBCYW5rIE5lZ2FyYSBJbmRvbmVzaWEgKFBlcnNlcm8pIFRiaw0KDQoqKkpTTVIuSksqKiA9IFBUIEphc2EgTWFyZ2EgKFBlcnNlcm8pIFRiaw0KDQoqKkJCUkkuSksqKiA9IFBUIEJhbmsgUmFreWF0IEluZG9uZXNpYSAoUGVyc2VybykgVGJrDQoNCioqQlNERS5KSyoqID0gUFQgQnVtaSBTZXJwb25nIERhbWFpIFRiaw0KDQpEYWxhbSBtZWxha3VrYW4gQW5hbGlzaXMga2V1YW5nYW4gc2FoYW0sIHlhbmcgZGlsYWt1a2FuIG1lbWlsaWggU2FoYW0geWFuZyB0ZXJkYWZ0YXIgZGkgTFE0NSwgbGFsdSBtZWxha3VrYW4gYW5hbGlzaXMgbWVtYmFuZGluZ2thbiBQcmljZSBFYXJuaW5nIFJhdGlvIChQRVIpIHNlYmFnYWkgbWVydXBha2FuIHJhc2lvIHlhbmcgZGlndW5ha2FuIGludmVzdG9yIHVudHVrIG1lbmlsYWkgc2FoYW0gc3VhdHUgcGVydXNhaGFhbiBrYXJlbmEgUEVSIG1lbmphZGkgcGVyYmFuZGluZ2FuIGFudGFyYSBtYXJrZXQgcHJpY2UgcGVyIHNoYXJlIChoYXJnYSBwYXNhciBwZXIgbGVtYmFyIHNhaGFtKSBkZW5nYW4gZWFybmluZyBwZXIgc2hhcmUgKGxhYmEgcGVybGVtYmFyIHNhaGFtKS4NCg0KUHJpY2UgRWFybmluZyBSYXRpbyBiZXJ0dWp1YW4gdW50dWsgbWVtYmVyaWthbiBpbnZlc3RvciBwZW5naW5kcmFhbiB5YW5nIGxlYmloIGJhaWsgYXRhdSBwZWthIHRlcmhhZGFwIG5pbGFpIHBlcnVzYWhhYW4geWFuZyBsZWJpaCBiZXNhci4NCg0KU2VsYW5qdXRueWEgbWVsaWhhdCBuaWxhaSBiZXRhIHBhZGEgc2FoYW0uIG1lbWlsaWggc2FoYW0gbWVsaWhhdCBuaWxhaSBiZXRhIGFkYWxhaCBmbGVrc2libGUuIGphZGkgamlrYSBJbmRla3MgSGFyZ2EgU2FoYW0gR2FidW5nYW4gKElIU0cpIHNlZGFuZyBkYWxhbSB0cmVuIHR1cnVuIG1ha2EgaW52ZXN0b3IgaGFydXMgbWVuZ2hpbmRhcmkgbmlsYWkgYmV0YSB5YW5nIHRpbmdnaS4NCg0KVGV0YXBpIG5pbGFpIGJldGEganVnYSBtZW5jZXJtaW5rYW4gcmlzaWtvIHN1YXR1IHNhaGFtLCBTYWhhbSBkZW5nYW4gYmV0YSB0aW5nZ2kgbWVudW5qdWtrYW4gdGluZ2thdCByaXNpa28geWFuZyB0aW5nZ2kuIE5hbXVuLCBrZXVudHVuZ2FuIHB1biBiZXJiYW5kaW5nIGx1cnVzIGRlbmdhbiByaXNpa29ueWEuIFNlbWVudGFyYSwgc2FoYW0gZGVuZ2FuIGJldGEgZGkgYmF3YWggMSwwIG1lbWlsaWtpIHJpc2lrbyBsZWJpaCBrZWNpbCwgbGluaWVyIGRlbmdhbiB0aW5na2F0IGtldW50dW5nYW4uDQoNCjxpbWcgc3R5bGU9ImZsb2F0OiBjZW50ZXI7IG1hcmdpbjogMHB4IDUwcHggMHB4IDUwcHg7IHdpZHRoOjgwJSIgc3JjPSJzYWhhbS5wbmciLz4NCg0KPGJyPg0KDQpkaWxpaGF0IGRhcmkgZ2FtYmFyIGRpIGF0YXMgZGlrZXRhaHVpIGJhaHdhIEJCQ0EgbWVtaWxpa2kgbmlsYWkgUHJpY2UgRWFybmluZyBSYXRpbyB5YW5nIHRlcnRpbmdnaSBhcnRpbnlhIHlhbmcgbWVtaWxpa2kgcG90ZW5zaSBrZXVudHVuZ2FuIHRlcmJlc2FyIGRhcmkgNSBTYWhhbSBkaSBhdGFzLg0KDQpTZWxhbmp1dG55YSBwZXJ1YmFoYW4gcGVuZGFwYXRhbiBkYWxhbSAxIFRhaHVuIHRlcmFraGlyIGFkYWxhaCBCQk5JIHlhbmcgdGVyYmVzYXIgc2ViZXNhciAzOS41NSUNCg0KeWFuZyB0ZXJha2hpciBCZXRhLCBkaWxpaGF0IGRhcmkgbmlsYWkgQmV0YSBkaSBhdGFzIGJhaHdhIHlhbmcgbWVtYmVyaWthbiByaXNpa28gcGFsaW5nIGtlY2lsIGFkYWxhaCBCQkNBIGRhbiByaXNpa28gdGVydGluZ2dpIEJCTkkuDQoNCmphZGkgamlrYSBkaWxpaGF0IGRhcmkgdGFiZWwgZGlhdGFzIChzZWJlbHVtIHBvcnRvZm9saW8pIHNhaGFtIEJCQ0EgYWRhbGFoIHNhaGFtIHlhbmcgdGVyYmFpaywgbG93IHJpc2sgYW5kIG1lZGl1bSByZXR1cm4gc2VsYW5qdXRueWEgYWthbiBkaWxha3VrYW4gcG9ydG9mb2xpbyB5YW5nIGFrYW4gbWVtdWRhaGthbiBpbnZlc3RvciBtZW1pbGloIHNhaGFtIG1hbmEgeWFuZyBha2FuIGRpaW52ZXN0YXNpa2FubnlhLg0KDQojIyBSDQoNCmBgYHtyLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KbGlicmFyeSh0aWR5cXVhbnQpIA0KbGlicmFyeShwbG90bHkpIA0KbGlicmFyeSh0aW1ldGspDQpgYGANCg0KYGBge3J9DQp0aWNrIDwtIGMoJ0JCQ0EuSksnLCAnQkJSSS5KSycsICdCQk5JLkpLJywgJ0pTTVIuSksnLCdCU0RFLkpLJykNCg0KDQpwcmljZV9kYXRhIDwtIHRxX2dldCh0aWNrLA0KICAgICAgICAgICAgICAgICAgICAgZnJvbSA9ICcyMDIwLTAxLTAxJywNCiAgICAgICAgICAgICAgICAgICAgIHRvICAgPSBTeXMuRGF0ZSgpLA0KICAgICAgICAgICAgICAgICAgICAgZ2V0ICA9ICdzdG9jay5wcmljZXMnKQ0KDQpoZWFkKHByaWNlX2RhdGEpDQpgYGANCg0KIyMjIFJldHVybg0KDQpTZWxhbmp1dG55YSBtZW5naGl0dW5nIHBlbmdlbWJhbGlhbiAoUmV0dXJuKSBoYXJpYW4gdW50dWsgc2FoYW0tc2FoYW0gaW5pIGRlbmdhbiBtZW5nZ3VuYWthbiBwZW5nZW1iYWxpYW4gbG9nYXJpdG1payAodW50dWsgbWVtYXN0aWthbiBkYXRhIHN0YXNpb25lcikuDQoNCmBgYHtyfQ0KbG9nX3JldF90aWR5IDwtIHByaWNlX2RhdGEgJT4lDQogIGdyb3VwX2J5KHN5bWJvbCkgJT4lDQogIHRxX3RyYW5zbXV0ZShzZWxlY3QgICAgID0gYWRqdXN0ZWQsDQogICAgICAgICAgICAgICBtdXRhdGVfZnVuID0gcGVyaW9kUmV0dXJuLA0KICAgICAgICAgICAgICAgcGVyaW9kICAgICA9ICdkYWlseScsDQogICAgICAgICAgICAgICBjb2xfcmVuYW1lID0gJ3JldCcsDQogICAgICAgICAgICAgICB0eXBlICAgICAgID0gJ2xvZycpDQoNCmxpYnJhcnkoRFQpIA0KZGF0YXRhYmxlKGxvZ19yZXRfdGlkeSkNCmBgYA0KDQpTZWxhbmp1dG55YSBha2FuIGRpZ3VuYWthbiBmdW5nc2kgYHNwcmVhZCgpYCB1bnR1ayBtZW5ndWJhaG55YSBtZW5qYWRpIGZvcm1hdCBsZWJhci4gZGFuIGtpdGEganVnYSBha2FuIG1lbmd1YmFobnlhIG1lbmphZGkgb2JqZWsgdGltZSBzZXJpZXMgbWVuZ2d1bmFrYW4gZnVuZ3NpIGB4dHMoKWAgLg0KDQpgYGB7cn0NCmxpYnJhcnkodGlkeXIpDQoNCmxvZ19yZXRfeHRzIDwtIGxvZ19yZXRfdGlkeSAlPiUNCiAgc3ByZWFkKHN5bWJvbCwgdmFsdWUgPSByZXQpICU+JQ0KICB0a194dHMoKQ0KDQpkYXRhdGFibGUobG9nX3JldF94dHMpDQpgYGANCg0KIyMjIFJhdGEtcmF0YSBQZW5nZW1iYWxpYW4NCg0KTWVuZ2hpdHVuZyByYXRhLXJhdGEgcGVuZ2VtYmFsaWFuIGhhcmlhbiB1bnR1ayBzZXRpYXAgYXNldC4NCg0KYGBge3J9DQptZWFuX3JldCA8LSBjb2xNZWFucyhsb2dfcmV0X3h0cykNCnByaW50ICggcm91bmQgKG1lYW5fcmV0LCA0KSkNCmBgYA0KDQojIyMgTWF0cmlrcyBLb3ZhcmlhbnNpDQoNClNlbGFuanV0bnlhLCBtZW5naGl0dW5nIG1hdHJpa3Mga292YXJpYW5zIHVudHVrIHNlbXVhIHN0b2sgZGVuZ2FuIG1lbmdhbGlrYW5ueWEgZGVuZ2FuIDI1MiBkYWxhbSBmb3JtYXQgdGFodW5hbi4NCg0KYGBge3J9DQpjb3ZfbWF0IDwtIGNvdihsb2dfcmV0X3h0cykgKiAyNTINCnByaW50KHJvdW5kKGNvdl9tYXQsNCkpDQpgYGANCg0KIyMjIFBlbmVyYXBhbiBNZXRvZGUgUG9ydG9mb2xpbw0KDQoqIEJvYm90IGFjYWsNCg0KKiBSYXRhLXJhdGEgcGVuZ2VtYmFsaWFuIGFzZXQNCg0KKiBSaXNpa28gcG9ydG9mb2xpbw0KDQoqIEJvYm90IHBvcnRvZm9saW8NCg0KYGBge3J9DQp3dHMgPC0gcnVuaWYobiA9IGxlbmd0aCh0aWNrKSkNCnd0cyA8LSB3dHMvc3VtKHd0cykNCg0KcG9ydF9yZXR1cm5zIDwtIChzdW0od3RzICogbWVhbl9yZXQpICsgMSleMjUyIC0gMQ0KcG9ydF9yaXNrIDwtIHNxcnQodCh3dHMpICUqJSAoY292X21hdCAlKiUgd3RzKSkNCnNoYXJwZV9yYXRpbyA8LSBwb3J0X3JldHVybnMvcG9ydF9yaXNrDQpgYGANCg0KU2VsYW5qdXRueWEgZGlsYWt1a2FuIHByb3NlcyBwZW1iZXR1a2FuIHBvcnRvZm9saW8gc2VjYXJhIGFjYWsgZGVuZ2FuIHNpbXVsYXNpIDUwMDAga2FsaSB1bnR1ayBtZW1hc3Rpa2FuIHNpZ25pZmlrYW5zaW55YSBzZWNhcmEgc3RhdGlzdGlrLiBQZXJzaWFwa2FuIHZla3RvciBrb3NvbmcgdW50dWsgbWFzaW5nLW1hc2luZyBsYW5na2FoIGRpYXRhczoNCg0KYGBge3J9DQpudW1fcG9ydCA8LSA1MDAwDQoNCmFsbF93dHMgPC0gbWF0cml4KG5yb3cgPSBudW1fcG9ydCwgbmNvbCA9IGxlbmd0aCh0aWNrKSkNCnBvcnRfcmV0dXJucyA8LSB2ZWN0b3IoJ251bWVyaWMnLCBsZW5ndGggPSBudW1fcG9ydCkNCnBvcnRfcmlzayA8LSB2ZWN0b3IoJ251bWVyaWMnLCBsZW5ndGggPSBudW1fcG9ydCkNCnNoYXJwZV9yYXRpbyA8LSB2ZWN0b3IoJ251bWVyaWMnLCBsZW5ndGggPSBudW1fcG9ydCkNCmBgYA0KDQpTZWxhbmp1dG55YSBtYXJpIGtpdGEgamFsYW5rYW4gZm9yIGxvb3AgNTAwMCBrYWxpLg0KDQpgYGB7cn0NCmZvciAoaSBpbiBzZXFfYWxvbmcocG9ydF9yZXR1cm5zKSkgew0KICANCiAgd3RzIDwtIHJ1bmlmKGxlbmd0aCh0aWNrKSkNCiAgd3RzIDwtIHd0cy9zdW0od3RzKQ0KICANCiAgYWxsX3d0c1tpLF0gPC0gd3RzDQogIA0KICANCiAgcG9ydF9yZXQgPC0gc3VtKHd0cyAqIG1lYW5fcmV0KQ0KICBwb3J0X3JldCA8LSAoKHBvcnRfcmV0ICsgMSleMjUyKSAtIDENCiAgDQogIHBvcnRfcmV0dXJuc1tpXSA8LSBwb3J0X3JldA0KICANCiAgDQogIHBvcnRfc2QgPC0gc3FydCh0KHd0cykgJSolIChjb3ZfbWF0ICAlKiUgd3RzKSkNCiAgcG9ydF9yaXNrW2ldIDwtIHBvcnRfc2QNCiAgDQogIHNyIDwtIHBvcnRfcmV0L3BvcnRfc2QNCiAgc2hhcnBlX3JhdGlvW2ldIDwtIHNyDQogIA0KfQ0KYGBgDQoNClNlbGFuanV0bnlhLCBtZW1idWF0IHRhYmVsIGRhdGEgdW50dWsgbWVueWltcGFuIHNlbXVhIG5pbGFpIHNlY2FyYSBiZXJzYW1hYW4uDQoNCmBgYHtyfQ0KcG9ydGZvbGlvX3ZhbHVlcyA8LSB0aWJibGUoUmV0dXJuID0gcG9ydF9yZXR1cm5zLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICBSaXNrID0gcG9ydF9yaXNrLA0KICAgICAgICAgICAgICAgICAgICAgIFNoYXJwZVJhdGlvID0gc2hhcnBlX3JhdGlvKQ0KDQoNCmFsbF93dHMgPC0gdGtfdGJsKGFsbF93dHMpDQoNCmNvbG5hbWVzKGFsbF93dHMpIDwtIGNvbG5hbWVzKGxvZ19yZXRfeHRzKQ0KDQpwb3J0Zm9saW9fdmFsdWVzIDwtIHRrX3RibChjYmluZChhbGxfd3RzLCBwb3J0Zm9saW9fdmFsdWVzKSkNCg0KZGF0YXRhYmxlKHBvcnRmb2xpb192YWx1ZXMpDQpgYGANCg0KU2VrYXJhbmcsIGtpdGEgc3VkYWggbWVtaWxpa2kgYm9ib3QgZGkgc2V0aWFwIGFzZXQgZGVuZ2FuIHJpc2lrbyBkYW4gcGVuZ2VtYmFsaWFuIGJlcnNhbWEgZGVuZ2FuIFNoYXJwZSByYXRpbyBkYXJpIHNldGlhcCBwb3J0b2ZvbGlvLiBTZWxhbmp1dG55YSwgbWFyaSBraXRhIGxpaGF0IHBvcnRvZm9saW8geWFuZyBwYWxpbmcgcGVudGluZyBkZW5nYW4gdmFyaWFucyBtaW5pbXVtIGRhbiBqdWdhIFNoYXJwZSByYXRpbyB0ZXJ0aW5nZ2kuDQoNCiMjIyBWYXJpYW5zaSBNaW5pbXVtDQpgYGB7cn0NCmxpYnJhcnkoZm9yY2F0cykNCg0KbWluX3ZhciA8LSBwb3J0Zm9saW9fdmFsdWVzW3doaWNoLm1pbihwb3J0Zm9saW9fdmFsdWVzJFJpc2spLF0NCg0KcCA8LSBtaW5fdmFyICU+JQ0KICBnYXRoZXIoQkJDQS5KSzpKU01SLkpLLCBrZXkgPSBBc3NldCwNCiAgICAgICAgIHZhbHVlID0gV2VpZ2h0cykgJT4lDQogIG11dGF0ZShBc3NldCA9IGFzLmZhY3RvcihBc3NldCkpICU+JQ0KICBnZ3Bsb3QoYWVzKHggPSBmY3RfcmVvcmRlcihBc3NldCxXZWlnaHRzKSwgeSA9IFdlaWdodHMsIGZpbGwgPSBBc3NldCkpICsNCiAgZ2VvbV9iYXIoc3RhdCA9ICdpZGVudGl0eScpICsNCiAgdGhlbWVfbWluaW1hbCgpICsNCiAgbGFicyh4ID0gJ0FzZXQnLCANCiAgICAgICB5ID0gJ0JvYm90JywgDQogICAgICAgdGl0bGUgPSAiQm9ib3QgUG9ydG9mb2xpbyBkZW5nYW4gVmFyaWFuc2kgTWluaW11bSIpICsNCiAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6cGVyY2VudCkgKw0KICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAibm9uZSIgKQ0KDQpnZ3Bsb3RseShwKQ0KYGBgDQoNClNlcGVydGkgeWFuZyBkYXBhdCBraXRhIGFtYXRpLCBwb3J0b2ZvbGlvIFZhcmlhbiBtaW5pbXVtIHRpZGFrIG1lbWlsaWtpIGFsb2thc2kgdW50dWsgQkJOSS5KSyBkYW4gQlNERS5KSyBzYW5nYXQgc2VkaWtpdC4gTWF5b3JpdGFzIHBvcnRvZm9saW8gZGlpbnZlc3Rhc2lrYW4gZGkgc2FoYW0gQkJDQS5KSywgQkpTTVIuSksgJiBCQlJJLkpLLg0KDQojIyMgUG9ydG9mb2xpbyBUYW5nZW5zaQ0KDQpTZWxhbmp1dG55YSwgbWFyaSBraXRhIGxpaGF0IHBvcnRvZm9saW8gdGFuZ2VuY3kgYXRhdSBwb3J0b2ZvbGlvIGRlbmdhbiBTaGFycGUgcmF0aW8gdGVydGluZ2dpLg0KDQpgYGB7cn0NCm1heF9zciA8LSBwb3J0Zm9saW9fdmFsdWVzW3doaWNoLm1heChwb3J0Zm9saW9fdmFsdWVzJFNoYXJwZVJhdGlvKSxdDQoNCnAgPC0gbWF4X3NyICU+JQ0KICBnYXRoZXIoQkJDQS5KSzpKU01SLkpLLCBrZXkgPSBBc3NldCwNCiAgICAgICAgIHZhbHVlID0gV2VpZ2h0cykgJT4lDQogIG11dGF0ZShBc3NldCA9IGFzLmZhY3RvcihBc3NldCkpICU+JQ0KICBnZ3Bsb3QoYWVzKHggPSBmY3RfcmVvcmRlcihBc3NldCxXZWlnaHRzKSwgeSA9IFdlaWdodHMsIGZpbGwgPSBBc3NldCkpICsNCiAgZ2VvbV9iYXIoc3RhdCA9ICdpZGVudGl0eScpICsNCiAgdGhlbWVfbWluaW1hbCgpICsNCiAgbGFicyh4ID0gJ0FzZXQnLCANCiAgICAgICB5ID0gJ0JvYm90JywgDQogICAgICAgdGl0bGUgPSAiQm9ib3QgUG9ydG9mb2xpbyBUYW5nZW5zaSAoTWFrc2ltdW0gU2hhcnBlIFJhdGlvKSIpICsNCiAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6cGVyY2VudCkgKw0KICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAibm9uZSIgKQ0KDQpnZ3Bsb3RseShwKQ0KYGBgDQoNClRlcm55YXRhIGRhbGFtIHJhc2lvIHNoYXJwZSB0ZXJ0aW5nZ2kgQkJDQS5KSyBtZW1pbGlraSBiYW55YWsgaW52ZXN0YXNpLiBkYWxhbSA1IHNhaGFtIHlhbmcgdGVycGlsaWggZGVuZ2FuIHJldHVybiBkYW4gcmlzayB5YW5nIG9wdGltYWwgQkJDQS5KSyBhZGFsYWggeWFuZyB0ZXJiYWlrLg0KDQojIyMgQmF0YXMgRWZpc2llbiBQb3J0b2ZvbGlvDQoNCkFraGlybnlhIG1hcmkga2l0YSBwbG90IHNlbXVhIHBvcnRvZm9saW8gYWNhayBkYW4gbWVtdmlzdWFsaXNhc2lrYW4gYmF0YXMgZWZpc2llbi4NCg0KYGBge3J9DQpwIDwtIHBvcnRmb2xpb192YWx1ZXMgJT4lDQogIGdncGxvdChhZXMoeCA9IFJpc2ssIHkgPSBSZXR1cm4sIGNvbG9yID0gU2hhcnBlUmF0aW8pKSArDQogIGdlb21fcG9pbnQoKSArDQogIHRoZW1lX2NsYXNzaWMoKSArDQogIHNjYWxlX3lfY29udGludW91cyhsYWJlbHMgPSBzY2FsZXM6OnBlcmNlbnQpICsNCiAgc2NhbGVfeF9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6cGVyY2VudCkgKw0KICBsYWJzKHggPSAnUmlzaWtvIFRhaHVuYW4nLA0KICAgICAgIHkgPSAnUGVuZ2VtYmFsaWFuIFRhaHVuYW4nLA0KICAgICAgIHRpdGxlID0gIk9wdGltYXNpIFBvcnRvZm9saW8gJiBQZXJiYXRhc2FuIHlhbmcgRWZpc2llbiIpICsNCiAgZ2VvbV9wb2ludChhZXMoeCA9IFJpc2sseSA9IFJldHVybiksIGRhdGEgPSBtaW5fdmFyLCBjb2xvciA9ICdyZWQnKSArDQogIGdlb21fcG9pbnQoYWVzKHggPSBSaXNrLHkgPSBSZXR1cm4pLCBkYXRhID0gbWF4X3NyLCBjb2xvciA9ICdyZWQnKSArDQogIGFubm90YXRlKCd0ZXh0JywgeCA9IDAuMjksIHkgPSAwLjE4LCBsYWJlbCA9ICJQb3J0b2ZvbGlvIFRhbmdlbnNpIikgKw0KICBhbm5vdGF0ZSgndGV4dCcsIHggPSAwLjI2NywgeSA9IDAuMDksIGxhYmVsID0gIlBvcnRvZm9saW8gVmFyaWFucyBtaW5pbXVtIikgKw0KICBhbm5vdGF0ZShnZW9tID0gJ3NlZ21lbnQnLCB4ID0gMC4yODQ2MiwgeGVuZCA9IDAuMjg0NjIsICB5ID0gMC4wOSwgDQogICAgICAgICAgIHllbmQgPSAwLjE1LCBjb2xvciA9ICdyZWQnLCBhcnJvdyA9IGFycm93KHR5cGUgPSAib3BlbiIpKSArDQogIGFubm90YXRlKGdlb20gPSAnc2VnbWVudCcsIHggPSAwLjI2NzksIHhlbmQgPSAwLjI2NzksICB5ID0gMC4wMzQsIA0KICAgICAgICAgICB5ZW5kID0gMC4wOSwgY29sb3IgPSAncmVkJywgYXJyb3cgPSBhcnJvdyh0eXBlID0gIm9wZW4iKSkNCiAgDQoNCmdncGxvdGx5KHApDQpgYGANCg0KIyMgUHl0aG9uDQoNCiMjIyBJbXBvcnQgRGF0YQ0KDQpgYGB7cHl0aG9ufQ0KaW1wb3J0IHBhbmRhcyBhcyBwZA0KaW1wb3J0IG51bXB5IGFzIG5wDQppbXBvcnQgbWF0cGxvdGxpYi5weXBsb3QgYXMgcGx0DQpmcm9tIGRhdGV0aW1lIGltcG9ydCBkYXRlDQppbXBvcnQgeWZpbmFuY2UgYXMgeWYNCg0KdGljayA9IFsnQkJDQS5KSycsICdCQlJJLkpLJywgJ0JCTkkuSksnLCAnSlNNUi5KSycsJ0JTREUuSksnXQ0KcHJpY2VfZGF0YSA9IHlmLmRvd25sb2FkKHRpY2ssICBzdGFydCA9ICcyMDIwLTAxLTAxJywNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIGVuZCA9IGRhdGUudG9kYXkoKSlbJ0FkaiBDbG9zZSddDQpwcmljZV9kYXRhDQpgYGANCg0KIyMjIE1hdHJpa3MgS292YXJpYW5zaQ0KYGBge3B5dGhvbn0NCmxvZ19yZXQgPSBucC5sb2cocHJpY2VfZGF0YS9wcmljZV9kYXRhLnNoaWZ0KDEpKQ0KY292X21hdCA9IGxvZ19yZXQuY292KCkgKiAyNTINCnByaW50KGNvdl9tYXQpDQpgYGANCg0KYGBge3B5dGhvbn0NCm51bV9wb3J0ID0gNTAwMA0KYWxsX3d0cyA9IG5wLnplcm9zKChudW1fcG9ydCwgbGVuKHByaWNlX2RhdGEuY29sdW1ucykpKQ0KcG9ydF9yZXR1cm5zID0gbnAuemVyb3MoKG51bV9wb3J0KSkNCnBvcnRfcmlzayA9IG5wLnplcm9zKChudW1fcG9ydCkpDQpzaGFycGVfcmF0aW8gPSBucC56ZXJvcygobnVtX3BvcnQpKQ0KYGBgDQoNCmBgYHtweXRob259DQpmb3IgaSBpbiByYW5nZShudW1fcG9ydCk6DQogIHd0cyA9IG5wLnJhbmRvbS51bmlmb3JtKHNpemUgPSBsZW4ocHJpY2VfZGF0YS5jb2x1bW5zKSkNCiAgd3RzID0gd3RzL25wLnN1bSh3dHMpDQogIA0KICBhbGxfd3RzW2ksOl0gPSB3dHMNCiAgDQogIHBvcnRfcmV0ID0gbnAuc3VtKGxvZ19yZXQubWVhbigpICogd3RzKQ0KICBwb3J0X3JldCA9IChwb3J0X3JldCArIDEpICoqIDI1MiAtIDENCiAgDQogIHBvcnRfcmV0dXJuc1tpXSA9IHBvcnRfcmV0DQogIA0KICBwb3J0X3NkID0gbnAuc3FydChucC5kb3Qod3RzLlQsIG5wLmRvdChjb3ZfbWF0LCB3dHMpKSkNCiAgDQogIHBvcnRfcmlza1tpXSA9IHBvcnRfc2QNCiAgDQogIHNyID0gcG9ydF9yZXQgLyBwb3J0X3NkDQogIHNoYXJwZV9yYXRpb1tpXSA9IHNyDQpgYGANCg0KYGBge3B5dGhvbn0NCm5hbWVzID0gcHJpY2VfZGF0YS5jb2x1bW5zDQptaW5fdmFyID0gYWxsX3d0c1twb3J0X3Jpc2suYXJnbWluKCldDQpwcmludChtaW5fdmFyKQ0KYGBgDQoNCmBgYHtweXRob259DQptYXhfc3IgPSBhbGxfd3RzW3NoYXJwZV9yYXRpby5hcmdtYXgoKV0NCnByaW50KG1heF9zcikNCmBgYA0KDQpgYGB7cHl0aG9ufQ0KcHJpbnQoc2hhcnBlX3JhdGlvLm1heCgpKQ0KYGBgDQoNCmBgYHtweXRob259DQpwcmludChwb3J0X3Jpc2subWluKCkpDQpgYGANCg0KIyMjIFZhcmlhbnNpIE1pbmltdW0NCmBgYHtweXRob259DQppbXBvcnQgcGxvdGx5LmV4cHJlc3MgYXMgcHgNCm1pbl92YXIgPSBwZC5TZXJpZXMobWluX3ZhciwgaW5kZXg9bmFtZXMpDQptaW5fdmFyID0gbWluX3Zhci5zb3J0X3ZhbHVlcygpDQoNCmZpZyA9IHB4LmJhcihtaW5fdmFyLCBjb2xvcj1taW5fdmFyLmluZGV4LCB0aXRsZT0iUG9ydG9mb2xpbyBkZW5nYW4gVmFyaWFuc2kgbWluaW11bSIpDQpmaWcuc2hvdygpDQpgYGANCg0KIyMjIFBvcnRvZm9saW8gVGFuZ2Vuc2kNCmBgYHtweXRob259DQppbXBvcnQgcGxvdGx5LmV4cHJlc3MgYXMgcHgNCm1heF9zciA9IHBkLlNlcmllcyhtYXhfc3IsIGluZGV4PW5hbWVzKQ0KbWF4X3NyID0gbWF4X3NyLnNvcnRfdmFsdWVzKCkNCg0KZmlnID0gcHguYmFyKG1heF9zciwgY29sb3I9bWF4X3NyLmluZGV4LCB0aXRsZT0iUG9ydG9mb2xpbyBkZW5nYW4gVGFuZ2Vuc2kiKQ0KZmlnLnNob3coKQ0KYGBgDQoNCiMjIyBCYXRhcyBFZmlzaWVuIFBvcnRvZm9saW8NCmBgYHtweXRob259DQpmaWcgPSBwbHQuZmlndXJlKCkNCmF4MSA9IGZpZy5hZGRfYXhlcyhbMC4xLDAuMSwwLjgsMC44XSkNCmF4MS5zZXRfeGxhYmVsKCdSaXNrJykNCmF4MS5zZXRfeWxhYmVsKCJSZXR1cm5zIikNCmF4MS5zZXRfdGl0bGUoIlBvcnRmb2xpbyBvcHRpbWl6YXRpb24gYW5kIEVmZmljaWVudCBGcm9udGllciIpDQpwbHQuc2NhdHRlcihwb3J0X3Jpc2ssIHBvcnRfcmV0dXJucykNCnBsdC5zaG93KCk7DQpgYGANCg0KIyBSZWZlcmVuY2UNCg0KMS4gaHR0cHM6Ly9ycHVicy5jb20vYnJpeWFuYS84ODkwMDINCg0KMi4gaHR0cHM6Ly9hcnhpdi5vcmcvcGRmLzIxMDEuMDI5MTIucGRmDQoNCjMuIGh0dHBzOi8vanVybmFsLnBuai5hYy5pZC9pbmRleC5waHAvZWtiaXMvYXJ0aWNsZS92aWV3LzE0MDQvcGRmDQoNCjQuIGh0dHBzOi8vd3d3Lm9jYmNuaXNwLmNvbS9pZC9hcnRpY2xlLzIwMjIvMTIvMDYvY2FyYS1tZW5nYW5hbGlzaXMtcmFzaW8ta2V1YW5nYW4NCg0KNS4gaHR0cHM6Ly9ycHVicy5jb20vZHNjaWVuY2VsYWJzL29wdGltYXNpOQ0KDQoNCg==