## Subsurface Data Analytics

### Naive Bayes Classification for Subsurface Data Analytics in Python

#### Michael Pyrcz, Associate Professor, University of Texas at Austin

##### [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1)

### PGE 383 Exercise: Naive Bayes Classification for Subsurface Data Analytics in Python

Here's a simple workflow, demonstration of naive Bayes classification for subsurface modeling workflows. This should help you get started with building subsurface models that with predictions based on multiple sources of information.

This method is great as it builds directly on our knowledge Bayesian statistics to provide a simple, but flexible classification method.

#### Bayesian Updating

The naive Bayes classifier is based on the conditional probability of a category, $k$, given $n$ features, $x_1, \dots , x_n$.

\begin{equation}
p(C_k | x_1, \dots , x_n)
\end{equation}

we can solve this with Bayesian updating:

\begin{equation}
p(C_k | x_1, \dots , x_n) = \frac{p(x_1, \dots , x_n | C_k) p(C_k)}{p(x_1, \dots , x_n)}
\end{equation}

let's combine the likelihood and prior for the momment:

\begin{equation}
p(x_1, \dots , x_n | C_k) p(C_k) = p(x_1, \dots , x_n, C_k)
\end{equation}

we can exand the full joint distribution recursively as follows:

\begin{equation}
p(x_1, \dots , x_n, C_k)
\end{equation}

expansion of the joint with the conditional and prior

\begin{equation}
p(x_1 | x_2, \dots , x_n, C_k) p(x_2, \dots , x_n, C_k)
\end{equation}

continue recursively expanding

\begin{equation}
p(x_1 | x_2, \dots , x_n, C_k) p(x_2 | x_3, \dots , x_n, C_k) p(x_3, \dots , x_n, C_k)
\end{equation}

we can generalize as

\begin{equation}
p(x_1 | x_2, \dots , x_n, C_k) p(x_2 | x_3, \dots , x_n, C_k) p(x_3 | x_4, \dots , x_n, C_k) \ldots (x_{n-1} | x_n, C_k)  (x_{n} | C_k) p(C_k)
\end{equation}

#### Naive Bayes Approach

The likelihood, conditional probability with the joint conditional is difficult to calculate.  It requires information about the joint relationship between $x_1, \dots , x_n$ features. As $n$ increases this requires a lot of data to inform the joint distribution.

With the naive bayes approach we make the 'naive' assumption that the features are all **conditionally independent**.  This entails:

\begin{equation}
p(x_i | x_{i+1}, \ldots , x_n, C_k) = p(x_i | C_k)
\end{equation}

for all $i = 1, \ldots, n$ features.

We can now solve for the needed conditional probability as:

\begin{equation}
p(C_k | x_1, \dots , x_n) = \frac{p(C_k) \prod_{i=1}^{n} p(x_i | C_k)}{p(x_1, \dots , x_n)}
\end{equation}

We only need the prior, $p(C_k)$, and a set of conditionals, $p(x_i | C_k)$, for all predictor features, $i = 1,\ldots,n$ and all categories, $k = 1,\ldots,K$.

The evidence term, $p(x_1, \dots , x_n)$, is only based on the features $x_1, \dots , x_n$; therefore, is a constant over the categories $k = 1,\ldots,n$.

* it ensures closure - probabilities over all categories sum to one

* we simply standardize the numerators to sum to one over the categories.

The naive Bayes approach is:

* simple to understand, builds on fundamental Bayesian statistics

* pratical even with small datasets since with the conditional independence we only need to estimate simple conditional distributions

#### Objective

In the PGE 383: Stochastic Subsurface Modeling class I want to provide hands-on experience with building subsurface modeling workflows. Python provides an excellent vehicle to accomplish this. I have coded a package called GeostatsPy with GSLIB: Geostatistical Library (Deutsch and Journel, 1998) functionality that provides basic building blocks for building subsurface modeling workflows.

The objective is to remove the hurdles of subsurface modeling workflow construction by providing building blocks and sufficient examples. This is not a coding class per se, but we need the ability to 'script' workflows working with numerical methods.

#### Getting Started

Here's the steps to get setup in Python with the GeostatsPy package:

1. Install Anaconda 3 on your machine (https://www.anaconda.com/download/).
2. From Anaconda Navigator (within Anaconda3 group), go to the environment tab, click on base (root) green arrow and open a terminal.
3. In the terminal type: pip install geostatspy.
4. Open Jupyter and in the top block get started by copy and pasting the code block below from this Jupyter Notebook to start using the geostatspy functionality.

You will need to copy the data file to your working directory.  They are available here:

* Tabular data - [unconv_MV_v4.csv](https://git.io/fhHLT).

There are exampled below with these functions. You can go here to see a list of the available functions, https://git.io/fh4eX, other example workflows and source code.

#### Import Required Packages

Let's import the GeostatsPy package.  I actually don't use it in this workflow, but just incase.

```python
import geostatspy.GSLIB as GSLIB                            # GSLIB utilies, visualization and wrapper
import geostatspy.geostats as geostats                      # GSLIB methods convert to Python
```

We will also need some standard packages. These should have been installed with Anaconda 3.

```python
import numpy as np                                          # ndarrys for gridded data
import pandas as pd                                         # DataFrames for tabular data
import os                                                   # set working directory, run executables
import matplotlib.pyplot as plt                             # for plotting
from scipy import stats                                     # summary statistics
import math                                                 # trig etc.
from sklearn.model_selection import train_test_split        # train and test split
from sklearn.naive_bayes import GaussianNB                  # naive Bayes model and prediction
from sklearn import metrics                                 # measures to check our models
```

If you get a package import error, you may have to first install some of these packages. This can usually be accomplished by opening up a command window on Windows and then typing 'python -m pip install [package-name]'. More assistance is available with the respective package docs.

#### Declare functions

Let's define a couple of functions to streamline plotting correlation matrices and visualization of a decision tree regression model.

```python
def plot_corr(dataframe,size=10):                               # plots a graphical correlation matrix
    corr = dataframe.corr()
    fig, ax = plt.subplots(figsize=(size, size))
    im = ax.matshow(corr,vmin = -1.0, vmax = 1.0)
    plt.xticks(range(len(corr.columns)), corr.columns);
    plt.yticks(range(len(corr.columns)), corr.columns);
    plt.colorbar(im, orientation = 'vertical')
    plt.title('Correlation Matrix')

def visualize_model(model,xfeature,x_min,x_max,yfeature,y_min,y_max,response,z_min,z_max,title,):# plots the data points and the decision tree prediction
    cmap = plt.cm.plasma
    xplot_step = (x_max - x_min)/300.0; yplot_step = (y_max - y_min)/300.0 # resolution of the model visualization
    xx, yy = np.meshgrid(np.arange(x_min, x_max, xplot_step), # set up the mesh
                     np.arange(y_min, y_max, yplot_step))
    Z = model.predict(np.c_[xx.ravel(), yy.ravel()])      # predict with our trained model over the mesh
    Z = Z.reshape(xx.shape)
    cs = plt.contourf(xx, yy, Z, cmap=cmap,vmin=z_min, vmax=z_max, levels = 50) # plot the predictions
    # add the data values as a colored by response feature scatter plot
    im = plt.scatter(xfeature,yfeature,s=None, c=response, marker=None, cmap=cmap, norm=None, vmin=z_min, vmax=z_max, alpha=0.8, linewidths=0.3, edgecolors="black")
    plt.title(title)                                       # add the labels
    plt.xlabel(xfeature.name); plt.ylabel(yfeature.name)
    plt.xlim([x_min,x_max]); plt.ylim([y_min,y_max])
    cbar = plt.colorbar(im, orientation = 'vertical')      # add the color bar
    cbar.set_label(response.name, rotation=270, labelpad=20)
    plt.subplots_adjust(left=0.0, bottom=0.0, right=1.0, top=1.2, wspace=0.2, hspace=0.2)
    return(plt)

def visualize_model_prob(model,xfeature,x_min,x_max,yfeature,y_min,y_max,response,title,):# plots the data points and the prediction probabilities
    n_classes = 10
    cmap = plt.cm.plasma
    xplot_step = (x_max - x_min)/300.0; yplot_step = (y_max - y_min)/300.0 # resolution of the model visualization
    xx, yy = np.meshgrid(np.arange(x_min, x_max, xplot_step), # set up the mesh
                     np.arange(y_min, y_max, yplot_step))

    z_min = 0.0; z_max = 1.0
    Z = model.predict_proba(np.c_[xx.ravel(), yy.ravel()])
    Z1 = Z[:,0].reshape(xx.shape); Z2 = Z[:,1].reshape(xx.shape)

    plt.subplot(121)
    cs1 = plt.contourf(xx, yy, Z1, cmap=cmap,vmin=z_min, vmax=z_max, levels=np.linspace(z_min, z_max, 100))
    im = plt.scatter(xfeature,yfeature,s=None, c=response, marker=None, cmap=plt.cm.Greys, norm=None, vmin=z_min, vmax=z_max, alpha=0.8, linewidths=0.3, edgecolors="black")
    plt.title(title + ' Probability of Low Production')
    plt.xlabel(xfeature.name)
    plt.ylabel(yfeature.name)
    cbar = plt.colorbar(cs1, orientation = 'vertical')
    cbar.set_label('Probability', rotation=270, labelpad=20)

    plt.subplot(122)
    cs2 = plt.contourf(xx, yy, Z2, cmap=cmap,vmin=z_min, vmax=z_max, levels=np.linspace(z_min, z_max, 100))
    im = plt.scatter(xfeature,yfeature,s=None, c=response, marker=None, cmap=plt.cm.Greys, norm=None, vmin=z_min, vmax=z_max, alpha=0.8, linewidths=0.3, edgecolors="black")
    plt.title(title + ' Probability of High Production')
    plt.xlabel(xfeature.name)
    plt.ylabel(yfeature.name)
    cbar = plt.colorbar(cs2, orientation = 'vertical')
    cbar.set_label('Probability', rotation=270, labelpad=20)
    plt.subplots_adjust(left=0.0, bottom=0.0, right=2.0, top=1.0, wspace=0.2, hspace=0.2)
    plt.show()

```

#### Set the working directory

I always like to do this so I don't lose files and to simplify subsequent read and writes (avoid including the full address each time).

```python
#os.chdir(r"C:\Users\pm27995\Downloads")                                       # set the working directory
```

You will have to update the part in quotes with your own working directory and the format is different on a Mac (e.g. "~/PGE").

#### Read the data table

First copy the "unconv_MV.csv" comma delimited file from https://github.com/GeostatsGuy/GeoDataSets to your working directory, then run this command to read the file into a DataFrame object (part of Pandas package).

```python
#my_data = pd.read_csv("unconv_MV_v4.csv")                   # load the comma delimited data file
my_data = pd.read_csv(r"https://raw.githubusercontent.com/GeostatsGuy/GeoDataSets/master/unconv_MV_v4.csv") # load from Dr. Pyrcz's GitHub repo
```

Let's visualize the first several rows of our data stored in a DataFrame so we can make sure we successfully loaded the data file.

```python
my_data.head(n=13)                                          # preview the first n rows of the DataFrame
```

      Well
      Por
      Perm
      AI
      Brittle
      TOC
      VR
      Prod

      0
      1
      12.08
      2.92
      2.80
      81.40
      1.16
      2.31
      1695.360819

      1
      2
      12.38
      3.53
      3.22
      46.17
      0.89
      1.88
      3007.096063

      2
      3
      14.02
      2.59
      4.01
      72.80
      0.89
      2.72
      2531.938259

      3
      4
      17.67
      6.75
      2.63
      39.81
      1.08
      1.88
      5288.514854

      4
      5
      17.52
      4.57
      3.18
      10.94
      1.51
      1.90
      2859.469624

      5
      6
      14.53
      4.81
      2.69
      53.60
      0.94
      1.67
      4017.374438

      6
      7
      13.49
      3.60
      2.93
      63.71
      0.80
      1.85
      2952.812773

      7
      8
      11.58
      3.03
      3.25
      53.00
      0.69
      1.93
      2670.933846

      8
      9
      12.52
      2.72
      2.43
      65.77
      0.95
      1.98
      2474.048178

      9
      10
      13.25
      3.94
      3.71
      66.20
      1.14
      2.65
      2722.893266

      10
      11
      15.04
      4.39
      2.22
      61.11
      1.08
      1.77
      3828.247174

      11
      12
      16.19
      6.30
      2.29
      49.10
      1.53
      1.86
      5095.810104

      12
      13
      16.82
      5.42
      2.80
      66.65
      1.17
      1.98
      4091.637316

Let's remove the well index and check the summary summary statistics.

```python
my_data = my_data.iloc[:,1:]                                # remove the well index
my_data.describe().transpose()                              # calculate summary statistics for the data
```

      count
      mean
      std
      min
      25%
      50%
      75%
      max

      Por
      200.0
      14.991150
      2.971176
      6.550000
      12.912500
      15.070000
      17.402500
      23.550000

      Perm
      200.0
      4.330750
      1.731014
      1.130000
      3.122500
      4.035000
      5.287500
      9.870000

      AI
      200.0
      2.968850
      0.566885
      1.280000
      2.547500
      2.955000
      3.345000
      4.630000

      Brittle
      200.0
      48.161950
      14.129455
      10.940000
      37.755000
      49.510000
      58.262500
      84.330000

      TOC
      200.0
      0.990450
      0.481588
      -0.190000
      0.617500
      1.030000
      1.350000
      2.180000

      VR
      200.0
      1.964300
      0.300827
      0.930000
      1.770000
      1.960000
      2.142500
      2.870000

      Prod
      200.0
      3864.407081
      1553.277558
      839.822063
      2686.227611
      3604.303506
      4752.637555
      8590.384044

It is good that we checked the summary statistics, because we have some negative values for brittleness and total organic carbon. The is physically imposible.  The values must be in error. We know the lowest possible values are 0.0, so we will truncate on 0.0.  We use the *get_numerical_data()* DataFrame member function to get a shallow copy of the data from the DataFrame.  Since it is a shallow copy, any changes we make to the copy are made to the data in the original DataFrame.  This allows us to apply this simple conditional statement to all the data values in the DataFrame all at once.

Let's also make a categorical variable for production, based on a threshold of 4,000 MCFPD.

* high production > 4,000 MCFPD, cprod = 1
* low production <= 4,000 MCFPD, cprod = 0

```python
num = my_data._get_numeric_data()                           # get shallow copy of the numerical values from the DataFrame
num[num < 0] = 0                                            # truncate negative values to 0.0
my_data['cProd'] = np.where(my_data['Prod']>=4000, 1, 0)    # conditional statement assign a new feature
my_data.describe().transpose()                              # calculate summary statistics for the data
```

      count
      mean
      std
      min
      25%
      50%
      75%
      max

      Por
      200.0
      14.991150
      2.971176
      6.550000
      12.912500
      15.070000
      17.402500
      23.550000

      Perm
      200.0
      4.330750
      1.731014
      1.130000
      3.122500
      4.035000
      5.287500
      9.870000

      AI
      200.0
      2.968850
      0.566885
      1.280000
      2.547500
      2.955000
      3.345000
      4.630000

      Brittle
      200.0
      48.161950
      14.129455
      10.940000
      37.755000
      49.510000
      58.262500
      84.330000

      TOC
      200.0
      0.991950
      0.478264
      0.000000
      0.617500
      1.030000
      1.350000
      2.180000

      VR
      200.0
      1.964300
      0.300827
      0.930000
      1.770000
      1.960000
      2.142500
      2.870000

      Prod
      200.0
      3864.407081
      1553.277558
      839.822063
      2686.227611
      3604.303506
      4752.637555
      8590.384044

      cProd
      200.0
      0.435000
      0.497001
      0.000000
      0.000000
      0.000000
      1.000000
      1.000000

Let's make sure that we have the new categorical feature for production.

```python
my_data.head()                                              # preview the first n rows of the updated DataFrame
```

      Por
      Perm
      AI
      Brittle
      TOC
      VR
      Prod
      cProd

      0
      12.08
      2.92
      2.80
      81.40
      1.16
      2.31
      1695.360819
      0

      1
      12.38
      3.53
      3.22
      46.17
      0.89
      1.88
      3007.096063
      0

      2
      14.02
      2.59
      4.01
      72.80
      0.89
      2.72
      2531.938259
      0

      3
      17.67
      6.75
      2.63
      39.81
      1.08
      1.88
      5288.514854
      1

      4
      17.52
      4.57
      3.18
      10.94
      1.51
      1.90
      2859.469624
      0

This dataset has variables from 200 unconventional wells including well average porosity, log transform of permeability (to linearize the relationships with other variables), accoustic impedance (kg/m2s*10^6), brittness ratio (%), total organic carbon (%), vitrinite reflectance (%), and initial production 90 day average (MCFPD).  Note, the dataset is synthetic.

#### Calculate the correlation matrix

For multivariate analysis it is a good idea to check the correlation matrix.  We can calculate it and view it in the console with these commands.

```python
corr_matrix = np.corrcoef(my_data.iloc[:,:7], rowvar = False)  # correlation matrix without the categorical value
print(np.around(corr_matrix,2))                           # print the correlation matrix to 2 decimals
```

    [[ 1.    0.76 -0.46 -0.22  0.71  0.11  0.88]
     [ 0.76  1.   -0.24 -0.12  0.47  0.05  0.71]
     [-0.46 -0.24  1.    0.13 -0.53  0.5  -0.37]
     [-0.22 -0.12  0.13  1.   -0.21  0.32 -0.02]
     [ 0.71  0.47 -0.53 -0.21  1.    0.3   0.64]
     [ 0.11  0.05  0.5   0.32  0.3   1.    0.23]
     [ 0.88  0.71 -0.37 -0.02  0.64  0.23  1.  ]]

Note the 1.0 diagonal resulting from the correlation of each variable with themselves.

Let's use our function declared above to make a graphical correlation matrix visualization.  This may inprove our ability to spot features.  It relies on the built in correlation matrix method with Numpy DataFrames and MatPlotLib for plotting.

```python
plot_corr(my_data.iloc[:,:7],10)                            # using our correlation matrix visualization function
plt.show()
```

#### Working with Only Two Features

Let's simplify the problem to 2 feature), Porosity and Brittleness to predict Production rate.  By working with only 2 features, it is very easy to visualize the segmentation of the feature space (it is only 2D and can be shown compleltely on a single plot).

```python
my_data_subset = my_data.iloc[:,[0,3,7]]                # extract just por, brittle and prod with 100 samples
X_train, X_test, y_train, y_test = train_test_split(my_data_subset.iloc[:,[0,1]], my_data_subset.iloc[:,2], test_size=0.25, random_state=73073)
y_train = pd.DataFrame({'cprod':y_train.values})
y_test = pd.DataFrame({'cprod':y_test.values})
```

#### Set the Min and Max for Plotting

x1 and x2 are the predictor features

* x1 on x axis and x2 on the y axis for the plots below

y is the response feature

* used for the color bar

```python
x1min = 5.0; x1max = 25.0
x2min = 0.0; x2max = 100.0
ymin = 0.0; ymax = 9000.0
```

Let's first check the univariate statistics of Porosity, Brittleness and Producton.

```python
X_train.describe().transpose()                         # calculate summary statistics for the data
```

      count
      mean
      std
      min
      25%
      50%
      75%
      max

      Por
      150.0
      15.005267
      2.971274
      6.55
      12.8975
      15.055
      17.500
      23.55

      Brittle
      150.0
      47.857067
      13.886701
      10.94
      37.8400
      49.150
      57.985
      81.40

```python
X_test.describe().transpose()                         # calculate summary statistics for the data
```

      count
      mean
      std
      min
      25%
      50%
      75%
      max

      Por
      50.0
      14.9488
      3.00064
      7.38
      13.3525
      15.195
      16.9325
      20.96

      Brittle
      50.0
      49.0766
      14.94183
      15.68
      37.3550
      52.545
      60.0575
      84.33

```python
y_train.describe()[:2]                                    # calculate summary statistics for the data
```

      cprod

      count
      150.000000

      mean
      0.433333

```python
y_test.describe()[:2]                                    # calculate summary statistics for the data
```

      cprod

      count
      50.00

      mean
      0.44

Let's first check the univariate distributions of Porosity, Brittleness and Producton.

```python
plt.subplot(231)
plt.hist(X_train["Por"], alpha = 0.2, color = 'red', edgecolor = 'black', bins=20)
plt.title('Porosity Training Data (%)')

plt.subplot(232)
plt.hist(X_train["Brittle"], alpha = 0.2, color = 'red', edgecolor = 'black', bins=20)
plt.title('Britteness Training Data (%)')

plt.subplot(233)
plt.hist(y_train['cprod'], alpha = 0.2, color = 'red', edgecolor = 'black', bins=20)
plt.title('Production Training Data (MCFPD)')

plt.subplot(234)
plt.hist(X_test["Por"], alpha = 0.2, color = 'red', edgecolor = 'black', bins=20)
plt.title('Porosity Testing Data (%)')

plt.subplot(235)
plt.hist(X_test["Brittle"], alpha = 0.2, color = 'red', edgecolor = 'black', bins=20)
plt.title('Britteness Testing Data (%)')

plt.subplot(236)
plt.hist(y_test['cprod'], alpha = 0.2, color = 'red', edgecolor = 'black', bins=20)
plt.title('Production Testing Data (MCFPD)')

plt.subplots_adjust(left=0.0, bottom=0.0, right=3.0, top=3.2, wspace=0.2, hspace=0.2)
plt.show()
```

The distributions are well behaved, we cannot observe obvious gaps nor truncations.  Let's look at a scatter plot of Porosity vs. Brittleness with points colored by Production.

```python
plt.subplot(121)
im = plt.scatter(X_train["Por"],X_train["Brittle"],s=None, c=y_train['cprod'], marker=None, cmap=None, norm=None, vmin=None, vmax=None, alpha=0.8, linewidths=0.3, edgecolors="black")
plt.title('Training Production vs. Brittleness and Porosity'); plt.xlabel('Porosity (%)'); plt.ylabel('Brittleness (%)')
cbar = plt.colorbar(im, orientation = 'vertical')
cbar.set_label("Production", rotation=270, labelpad=20)

plt.subplot(122)
im = plt.scatter(X_test["Por"],X_test["Brittle"],s=None, c=y_test['cprod'], marker=None, cmap=None, norm=None, vmin=None, vmax=None, alpha=0.8, linewidths=0.3, edgecolors="black")
plt.title('Testing Production vs. Brittleness and Porosity'); plt.xlabel('Porosity (%)'); plt.ylabel('Brittleness (%)')
cbar = plt.colorbar(im, orientation = 'vertical')
cbar.set_label("Production", rotation=270, labelpad=20)

plt.subplots_adjust(left=0.0, bottom=0.0, right=2.0, top=1.2, wspace=0.2, hspace=0.2)

plt.show()
```

#### Instantiate, Fit and Predict with Gausian Naive Bayes

Let's build a Gaussian naive Bayes model.

We select the Gaussian model as it simplifies the inference problem to just a set of conditional means and variances given each feature.

Recall we can set a prior probability of each response category

* We will use the proportions from the training dataset.

* 0.43 for high production (the mean of the binary dataset is the proportion of 1's)

* 0.57 for low production (1 - proportion of high production)

Another option would be to assume a naive, uniform prior, substitute the following:

```python
priors = (0.5,0.5)   # naive prior
```

```python
priors = (0.5,0.5)                                        # set the prior probabilities of low and high production
```

Let's build our Gaussian naive Bayes model.

* instantiate it with the priors

* train with the training data, we use the standard fit function

```python
gnb = GaussianNB(priors = priors)                           # instantiate the Gaussian naive Bayes model
GaussianNB_fit = gnb.fit(X_train,y_train['cprod'].values)   # train with the training data
```

Let's predict with our new model over the testing dataset.

* test by predicting with the testing data, we use the standard prediction function

```python
y_pred = GaussianNB_fit.predict(np.c_[X_test['Por'].values,X_test['Brittle'].values]) # predict over the testing data
```

#### Model Checking

Let's check our model. With scikit learn we have great built in tools to evaluate our classification model. Let's try the classification report first.

```python
classification_report(truth, predicted)                # build a classification report to check our classification model
```

We get a table with summary metrics for model performance.

```python
from sklearn.metrics import classification_report
print(classification_report(y_test['cprod'].values, y_pred, labels=[0,1]))
```

                  precision    recall  f1-score   support

               0       0.96      0.96      0.96        28
               1       0.95      0.95      0.95        22

        accuracy                           0.96        50
       macro avg       0.96      0.96      0.96        50
    weighted avg       0.96      0.96      0.96        50

The metrics include:

    * recall - the ratio of true positives divided by all cases of the category in the testing dataset

    * precision - the ratio of true positives divided by all positives (true positives + false positives)

    * f1-score - the harmonic mean of recall and precision

    * support - the number of samples of each category in the testing data

I also like to look at the confusion matrix.

* the x axis is the prediction - category 0 or 1

* the y axis is the truth - category 0 or 1

```python
from sklearn.metrics import confusion_matrix
print(confusion_matrix(y_test['cprod'].values, y_pred))
```

    [[27  1]
     [ 1 21]]

From above we can observe:

* 26 low production wells classified correctly as low production

* 1 high production well misclassified as low production

* 2 low production wells misclassified as high production

* 21 high production wells classified correctly as high production

#### Visualizing the Classification Model

Let's visualize the model over the entire feature space.

* here's the training data with the classification over the full range of predictor features.

* blue for low production and yellow for high production

Note: naive Bayes provides the posterior probability of high and low production

* the classifications below are based on maximum apriori selection (MAPS), selecting the category with the highest probability

Let's visualize the classification model (blue - low production, yellow - high production) over the predictor feature space with the training data plotted (white - low production, black - high production).

```python
visualize_model(GaussianNB_fit,X_train["Por"],x1min,x1max,X_train["Brittle"],x2min,x2max,y_train['cprod'],0.0,1.0,'Training Data and Naive Bayes Model')
```

We could also visualize the posterior probabilities of low and high production.

* here's the posterior probability of low and high production over the predictor feature space

```python
visualize_model_prob(GaussianNB_fit,X_train["Por"],x1min,x1max,X_train["Brittle"],x2min,x2max,y_train['cprod'],'Training Data and Naive Bayes Model')
```

Finally, let's look at the classification model over the predictor feature space (blue - low production, yellow - high production) with the testing data plotted (white - low production, black - high production).

```python
visualize_model(GaussianNB_fit,X_test["Por"],x1min,x1max,X_test["Brittle"],x2min,x2max,y_test['cprod'],0.0,1.0,'Testing Data and Naive Bayes Model')
```

We have a reasonable model to predict well production from porosity and brittleness for an unconventional reservoir.

#### Comments

This was a basic demonstration of naive Bayes for prediction. A lot more could be done, for example, we could have applied variants such as:

* multinomial naive Bayes

* compliment naive Bayes

* Bernoulli naive Bayes

We could have worked with more predictor features, but for learning the method, it is nice to be able to visualize the entire classification in one plot!

If you struggled with the basic Python used here check out my other basic demonstrations for DataFrames, ndarrays, univariate statistics, plotting data, declustering, data transformations, trend modeling and many other workflows available at https://github.com/GeostatsGuy/PythonNumericalDemos and https://github.com/GeostatsGuy/GeostatsPy.

I hope this was helpful,

*Michael*

#### The Author:

### Michael Pyrcz, Associate Professor, University of Texas at Austin
*Novel Data Analytics, Geostatistics and Machine Learning Subsurface Solutions*

With over 17 years of experience in subsurface consulting, research and development, Michael has returned to academia driven by his passion for teaching and enthusiasm for enhancing engineers' and geoscientists' impact in subsurface resource development.

For more about Michael check out these links:

#### [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1)

#### Want to Work Together?

I hope this content is helpful to those that want to learn more about subsurface modeling, data analytics and machine learning. Students and working professionals are welcome to participate.

* Want to invite me to visit your company for training, mentoring, project review, workflow design and / or consulting? I'd be happy to drop by and work with you!

* Interested in partnering, supporting my graduate student research or my Subsurface Data Analytics and Machine Learning consortium (co-PIs including Profs. Foster, Torres-Verdin and van Oort)? My research combines data analytics, stochastic modeling and machine learning theory with practice to develop novel methods and workflows to add value. We are solving challenging subsurface problems!

* I can be reached at mpyrcz@austin.utexas.edu.

I'm always happy to discuss,

*Michael*

Michael Pyrcz, Ph.D., P.Eng. Associate Professor The Hildebrand Department of Petroleum and Geosystems Engineering, Bureau of Economic Geology, The Jackson School of Geosciences, The University of Texas at Austin

I have other demonstrations on the basics of working with DataFrames, ndarrays, univariate statistics, plotting data, declustering, data transformations, trend modeling and many other workflows available at https://github.com/GeostatsGuy/PythonNumericalDemos and https://github.com/GeostatsGuy/GeostatsPy.

I hope this was helpful,

*Michael*

Michael Pyrcz, Ph.D., P.Eng. Associate Professor The Hildebrand Department of Petroleum and Geosystems Engineering, Bureau of Economic Geology, The Jackson School of Geosciences, The University of Texas at Austin

#### More Resources Available at: [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1)

```python

```


$$
    % LaTeX macros
    \newcommand{\genericdel}[3]{%
      \left#1{#3}\right#2
    }
    \newcommand{\del}[1]{\genericdel(){#1}}
    \newcommand{\sbr}[1]{\genericdel[]{#1}}
    \newcommand{\cbr}[1]{\genericdel\{\}{#1}}
    \newcommand{\abs}[1]{\genericdel||{#1}}
    \DeclareMathOperator*{\argmin}{arg\,min}
    \DeclareMathOperator*{\argmax}{arg\,max}
    \DeclareMathOperator{\Pr}{\mathbb{p}}
    \DeclareMathOperator{\E}{\mathbb{E}}
    \DeclareMathOperator{\Ind}{\mathbb{I}}
    \DeclareMathOperator{\V}{\mathbb{V}}
    \DeclareMathOperator{\cov}{Cov}
    \DeclareMathOperator{\var}{Var}
    \DeclareMathOperator{\ones}{\mathbf{1}}
    \DeclareMathOperator*{\argmin}{arg\,min}
    \DeclareMathOperator*{\argmax}{arg\,max}
    \DeclareMathOperator{\normal}{\mathcal{N}}
    \DeclareMathOperator{\unif}{Uniform}
    \DeclareMathOperator{\poisson}{Poisson}
    \DeclareMathOperator{\dif}{d}
    \newcommand{\od}[2]{\frac{\dif{#1}}{\dif{#2}}}
    \DeclareMathOperator{\Forall}{\forall}
    \newcommand{\iid}{iid}
    \newcommand{\indep}{\perp}
    \newcommand{\trans}{^{\intercal}}
$$

# Introduction

## Reminder of Setup

We have:
1. Noise-free outcomes $X_i$ and $Y_i$ drawn from distributions $F_X$ and $F_Y$ respectively.
2. Under the null, $F_X=F_Y$.
3. What we see are noisy-observations
\begin{equation}
    \widetilde{X_i} = X_i + \epsilon_{X,i}
    \quad\text{and}\quad
    \widetilde{Y_i} = Y_i + \epsilon_{Y,i}
\end{equation}
4. The distribution of the errors are known, for example they can be assumed to be normal
\begin{equation}
    \epsilon_{X,i} \sim \normal\del{0, \sigma^2_{X,i}}
    \quad\text{and}\quad
    \epsilon_{Y,i} \sim \normal\del{0, \sigma^2_{Y,i}}
\end{equation}
with known variances $\sigma^2_{X,i}$ and $\sigma^2_{Y,i}$.
5. In the homoscedastic case, the focus of this notebook, all variances are the same:
\begin{equation}
\sigma^2_{X,i} = \sigma^2_{Y,j} = \sigma^2
\end{equation}

## Purpose

The purpose of this notebook is to use simulations to study the power of our tests in the homoscedastic scenario.
When the errors are the same for every observation, the KS test is valid.
Under the null, $F_X$ and $F_Y$ are the same.
If $\sigma_{X,i}$ and $\sigma_{Y,i}$ are also all equal, then $\widetilde{X_i}$ and $\widetilde{Y_i}$ have the same distribution.

The question then becomes: when the errors are homoscedastic, is there are loss of power going from the classical KS test to the deconvolved tests that we have developed?
To be specific, I will compare three tests:
1. The **classical** KS test.
2. The **singly deconvolved** KS test: uses Fourier deconvolution to estimate the null distribution (which when then sample from to get bootstrap samples under the null), but otherwise uses the regular KS test statistic, which is the $L_\infty$ distance between the *empirical* CDFs of $\widetilde{X}$ and $\widetilde{Y}$.
3. The **doubly deconvolved** KS test: uses Fourier deconvolution to estimate the null distribution, and also uses deconvolution for the test statistic, which is the $L_\infty$ distances between the *deconvolved* CDFs of $\widetilde{X}$ and $\widetilde{Y}$.

With these three tests, I use simulations with homoscedastic errors to estimate the power under the null (should be 0.05) and under the alternative.

# Notebook setup

```julia
using Distributions
using Dates
using Printf

using LaTeXStrings
import PyPlot; plt=PyPlot
plt.rc("figure", dpi=300.0)
# plt.rc("figure", figsize=(6,4))
# plt.rc("figure", autolayout=true)
plt.rc("savefig", dpi=300.0)
plt.rc("text", usetex=true)
plt.rc("font", family="serif")
plt.rc("font", serif="Palatino")
cbbPalette = ["#E69F00", "#56B4E9", "#009E73", "#F0E442", "#0072B2", "#D55E00", "#CC79A7"]
;
```

```julia
# a few convenience functions for plotting p-values and estimating power
function plot_ecdf(x; kwargs...)
    plt.plot(sort(x), range(0.0, stop=1.0, length=length(x)); kwargs...)
end
sim_power(pvals) = mean(pvals .<= 0.05)
function power_se(pvals)
    p = sim_power(pvals)
    q = 1-p
    n = length(pvals)
    return √(p*q/n)
end
estimate_power(pvals) = (sim_power(pvals), power_se(pvals))
;
```

```julia
using Revise
import DeconvolveDistribution
import DeconvolutionTests
```

```julia
module KS
    import DeconvolveDistribution
    using DeconvolveDistribution: FourierDeconv, EfronDeconv
    using HypothesisTests
    using HypothesisTests: ApproximateTwoSampleKSTest, pvalue
    using Distributions
    using StatsBase: sample, midpoints
    using DeconvolutionTests
    using DeconvolutionTests: deconv_boot_test,
            distance_test_statistic,
            KS_test_statistic

    include("../src/simulate.jl")

    function sim_KS(F_X, F_Y, σ, n_X, n_Y)
        # simulate homoscedastic data
        data = sim_homo(F_X, F_Y, σ, n_X, n_Y)

        Xtilde = data[:Xtilde]
        Ytilde = data[:Ytilde]

        # classical KS test ignoring errors
        KS_test = HypothesisTests.ApproximateTwoSampleKSTest(Xtilde, Ytilde)
        KS_pval = pvalue(KS_test; tail=:both)

        return KS_pval
    end

    function sim_tests(F_X, F_Y, σ, n_X, n_Y)
        # simulate homoscedastic data
        data = sim_homo(F_X, F_Y, σ, n_X, n_Y)

        Xtilde = data[:Xtilde]
        Ytilde = data[:Ytilde]
        σ_X = data[:σ_X] # all the same
        σ_Y = data[:σ_Y] # all the same

        # classical KS test ignoring errors
        KS_test = HypothesisTests.ApproximateTwoSampleKSTest(Xtilde, Ytilde)
        KS_pval = pvalue(KS_test; tail=:both)

        low = min(quantile(F_X, 0.01), quantile(F_Y, 0.01))
        high = max(quantile(F_X, 0.99), quantile(F_Y, 0.99))
        Xgrid = range(low, stop=high, length=75)
        method_null = FourierDeconv(0.3, Xgrid, 100)
        method_boot = FourierDeconv(0.3, Xgrid, 30)

        # deconvolved KS test using classical KS test statistic
        t_KS = KS_test_statistic
        deconvKS_pval = deconv_boot_test(Xtilde, Ytilde, σ_X, σ_Y, t_KS, method_null; niter=200)

        # deconvolved KS test using deconvolved KS test statistic
        p = Inf # L∞ norm (KS)
        bw = 0.3
        obsmin = min(minimum(Xtilde), minimum(Ytilde))
        obsmax = max(maximum(Xtilde), maximum(Ytilde))
        t_Lp = distance_test_statistic(p, method_boot, Xgrid)
        deconv2_pval = deconv_boot_test(Xtilde, Ytilde, σ_X, σ_Y, t_Lp, method_null; niter=200)

        return (KS_pval, deconvKS_pval, deconv2_pval)
    end

end
;
```

```julia
# simulate homoscedastic data under the null hypothesis,
# 200 samples for each group
# where X, Y ~ Normal(0, 2²) and the noise is iid Normal(0, 0.8²).
# Then run the three tests and return the three p-values.
@time KS.sim_tests(Normal(0,2), Normal(0,2), 0.8, 200, 200)
```

      8.875299 seconds (24.61 M allocations: 981.073 MiB, 6.83% gc time)

    (0.39273070794065434, 0.33, 0.445)

The Fourier deconvolution is slow, and for the doubly deconvolved test we need to perform it for every bootstrap sample, so each simulation takes a while (approx. 3 seconds).

# Power under the null

```julia
null_KS_arr       = Float64[]
null_deconvKS_arr = Float64[]
null_deconv2_arr  = Float64[]
;
```

```julia
@time for _ in 1:4
    KS_pval, deconvKS_pval, deconv2_pval = KS.sim_tests(
        Normal(0,2), Normal(0,2), # F_X and F_Y are the same
        0.8, # σ
        200, 200 # sample sizes
        )
end
```

     11.423943 seconds (37.76 M allocations: 889.143 MiB, 0.90% gc time)

```julia
@time Threads.@threads for _ in 1:4
    KS_pval, deconvKS_pval, deconv2_pval = KS.sim_tests(
        Normal(0,2), Normal(0,2), # F_X and F_Y are the same
        0.8, # σ
        200, 200 # sample sizes
        )
end
```

      6.728666 seconds (35.54 M allocations: 841.035 MiB, 2.92% gc time)

```julia
tstart = now()
while now() - tstart < Dates.Hour(4) # keep doing this for 4 hours
    KS_pval, deconvKS_pval, deconv2_pval = KS.sim_tests(
        Normal(0,2), Normal(0,2), # F_X and F_Y are the same
        0.8, # σ
        200, 200 # sample sizes
        )
    push!(null_KS_arr, KS_pval)
    push!(null_deconvKS_arr, deconvKS_pval)
    push!(null_deconv2_arr, deconv2_pval)
end
```

    WARNING: Base.now is deprecated: it has been moved to the standard library package `Dates`.
    Add `using Dates` to your imports.
      likely near /Users/imolk/Library/Julia/packages/IJulia/nBp96/src/kernel.jl:41
    WARNING: Base.now is deprecated: it has been moved to the standard library package `Dates`.
    Add `using Dates` to your imports.
      likely near /Users/imolk/Library/Julia/packages/IJulia/nBp96/src/kernel.jl:41
    WARNING: Base.Dates is deprecated, run `using Dates` instead
      likely near /Users/imolk/Library/Julia/packages/IJulia/nBp96/src/kernel.jl:41
    WARNING: Base.now is deprecated: it has been moved to the standard library package `Dates`.
    Add `using Dates` to your imports.
      likely near /Users/imolk/Library/Julia/packages/IJulia/nBp96/src/kernel.jl:41
    WARNING: Base.Dates is deprecated, run `using Dates` instead
      likely near /Users/imolk/Library/Julia/packages/IJulia/nBp96/src/kernel.jl:41
    WARNING: Base.now is deprecated: it has been moved to the standard library package `Dates`.
    Add `using Dates` to your imports.
      likely near /Users/imolk/Library/Julia/packages/IJulia/nBp96/src/kernel.jl:41
    WARNING: Base.Dates is deprecated, run `using Dates` instead
      likely near /Users/imolk/Library/Julia/packages/IJulia/nBp96/src/kernel.jl:41
    WARNING: Base.now is deprecated: it has been moved to the standard library package `Dates`.
    Add `using Dates` to your imports.
      likely near /Users/imolk/Library/Julia/packages/IJulia/nBp96/src/kernel.jl:41
    in top-level scope at In[8]

```julia
plt.plot([0,1], [0,1])
plot_ecdf(null_KS_arr, label="KS")
plot_ecdf(null_deconvKS_arr, label="deconvolved KS")
plot_ecdf(null_deconv2_arr, label="doubly deconvolved")
plt.legend(loc="lower right")
plt.title("Under the null")
```

```julia
@printf("Power of classical     KS test under the null: %.3f ± %.3f\n", estimate_power(null_KS_arr)...)
@printf("Power of singly-deconv KS test under the null: %.3f ± %.3f\n", estimate_power(null_deconvKS_arr)...)
@printf("Power of doubly-deconv KS test under the null: %.3f ± %.3f\n", estimate_power(null_deconv2_arr)...)
```

    Power of classical     KS test under the null: 0.044 ± 0.003
    Power of singly-deconv KS test under the null: 0.059 ± 0.003
    Power of doubly-deconv KS test under the null: 0.057 ± 0.003

    WARNING: Base.@printf is deprecated: it has been moved to the standard library package `Printf`.
    Add `using Printf` to your imports.
      likely near /Users/imolk/Library/Julia/packages/IJulia/nBp96/src/kernel.jl:41
    WARNING: Base.@printf is deprecated: it has been moved to the standard library package `Printf`.
    Add `using Printf` to your imports.
      likely near /Users/imolk/Library/Julia/packages/IJulia/nBp96/src/kernel.jl:41
    WARNING: Base.@printf is deprecated: it has been moved to the standard library package `Printf`.
    Add `using Printf` to your imports.
      likely near /Users/imolk/Library/Julia/packages/IJulia/nBp96/src/kernel.jl:41

The above simply shows that all three tests are valid.

# Choosing alternative

The alternative will simply be that:
\begin{equation}
\begin{split}
    X & \sim \mathcal{N}(0, 2^2) \text{, and}\\
    Y & \sim \mathcal{N}(\mu, 2^2)\,.
\end{split}
\end{equation}
The purpose of this section is to choose $\mu$ so the power
of the KS test is somewhere in the range 0.8-0.95,
so that we can easily pick up differences in power
between the three tests in a reasonable scenario.

```julia
null_KS = [KS.sim_KS(Normal(0,2), Normal(0,2), 0.8, 200, 200) for _ in 1:10000]
alt1_KS = [KS.sim_KS(Normal(0,2), Normal(0.5,2), 0.8, 200, 200) for _ in 1:10000]
alt2_KS = [KS.sim_KS(Normal(0,2), Normal(0.8,2), 0.8, 200, 200) for _ in 1:10000]
alt3_KS = [KS.sim_KS(Normal(0,2), Normal(1.0,2), 0.8, 200, 200) for _ in 1:10000]
;
```

```julia
mean(null_KS .<= 0.05)
```

    0.0395

```julia
mean(alt1_KS .<= 0.05)
```

    0.4695

```julia
mean(alt2_KS .<= 0.05)
```

    0.8744

```julia
mean(alt3_KS .<= 0.05)
```

    0.9741

```julia
plt.plot([0, 1], [0, 1], color="grey")
plot_ecdf(null_KS, label="null")
plot_ecdf(alt1_KS, label="alt 1")
plot_ecdf(alt2_KS, label="alt 2")
plot_ecdf(alt3_KS, label="alt 3")
plt.legend(loc="lower right")
plt.xlim(0,0.1)
;
```

We will choose the alternative to have $Y \sim \mathcal{N}(0.8, 2)$, under which the power of the classical KS test is 0.8, leaving plenty of room on either side for the other tests to show their worth.

# Power under the alternative

```julia
alt_KS_arr       = Float64[]
alt_deconvKS_arr = Float64[]
alt_deconv2_arr  = Float64[]
;
```

```julia
tstart = now()
while now() - tstart < Dates.Hour(4)
    KS_pval, deconvKS_pval, deconv2_pval = KS.sim_tests(Normal(0,2), Normal(0.8,2), 0.8, 200, 200)
    push!(alt_KS_arr, KS_pval)
    push!(alt_deconvKS_arr, deconvKS_pval)
    push!(alt_deconv2_arr, deconv2_pval)
end
```

    WARNING: Base.now is deprecated: it has been moved to the standard library package `Dates`.
    Add `using Dates` to your imports.
      likely near /Users/imolk/Library/Julia/packages/IJulia/nBp96/src/kernel.jl:41
    WARNING: Base.now is deprecated: it has been moved to the standard library package `Dates`.
    Add `using Dates` to your imports.
      likely near /Users/imolk/Library/Julia/packages/IJulia/nBp96/src/kernel.jl:41
    WARNING: Base.now is deprecated: it has been moved to the standard library package `Dates`.
    Add `using Dates` to your imports.
      likely near /Users/imolk/Library/Julia/packages/IJulia/nBp96/src/kernel.jl:41
    WARNING: Base.now is deprecated: it has been moved to the standard library package `Dates`.
    Add `using Dates` to your imports.
      likely near /Users/imolk/Library/Julia/packages/IJulia/nBp96/src/kernel.jl:41
    WARNING: Base.now is deprecated: it has been moved to the standard library package `Dates`.
    Add `using Dates` to your imports.
      likely near /Users/imolk/Library/Julia/packages/IJulia/nBp96/src/kernel.jl:41
    in top-level scope at In[18]

```julia
plt.plot([0,1], [0,1])
plot_ecdf(alt_KS_arr, label="KS")
plot_ecdf(alt_deconvKS_arr, label="deconvolved KS")
plot_ecdf(alt_deconv2_arr, label="doubly deconvolved")
plt.legend(loc="lower right")
plt.title("Under the alternative")
```

```julia
@printf("Power of classical     KS test under the altv: %.3f ± %.3f\n", estimate_power(alt_KS_arr)...)
@printf("Power of singly-deconv KS test under the altv: %.3f ± %.3f\n", estimate_power(alt_deconvKS_arr)...)
@printf("Power of doubly-deconv KS test under the altv: %.3f ± %.3f\n", estimate_power(alt_deconv2_arr)...)
```

    Power of classical     KS test under the altv: 0.874 ± 0.005
    Power of singly-deconv KS test under the altv: 0.901 ± 0.005
    Power of doubly-deconv KS test under the altv: 0.820 ± 0.006

    WARNING: Base.@printf is deprecated: it has been moved to the standard library package `Printf`.
    Add `using Printf` to your imports.
      likely near /Users/imolk/Library/Julia/packages/IJulia/nBp96/src/kernel.jl:41
    WARNING: Base.@printf is deprecated: it has been moved to the standard library package `Printf`.
    Add `using Printf` to your imports.
      likely near /Users/imolk/Library/Julia/packages/IJulia/nBp96/src/kernel.jl:41
    WARNING: Base.@printf is deprecated: it has been moved to the standard library package `Printf`.
    Add `using Printf` to your imports.
      likely near /Users/imolk/Library/Julia/packages/IJulia/nBp96/src/kernel.jl:41

# Conclusion

First of all, all tests seem to be valid under the null. If anything, the classical KS test seems to be conservative.

There appears to be no loss of power going from the classical KS test to the singly deconvolved KS test.
This is how I make sense of that: as the distribution of the KS statistic under the null is supposed to be invariant to the null distribution, at least asymptotically.
In the homoscedastic case, what we're doing with the single deconvolution is to Monte Carlo the distribution of the KS statistic under a distribution, which in this case happens to be the deconvolved null.
The classical test relies on asymptotics, although there are also published tables that were also derived from Monte Carlo simulations.
It's perhaps no surprise then that the deconvolved KS test performs slightly better than the classical KS test---the power goes from 0.875 to 0.902 under the alternative.
One thing to note is the under both the null and the alternative, the distribution of $p$-values is smooth, whereas the CDF of $p$-values of the classical KS tests is a step function.

In the case of the doubly deconvolved test, there is a significant loss of power.
Under the alternative, the power dropped from 0.875 to 0.820.
It does seem like the deconvolution inside of the test statistic is erasing some information.

## Next steps
* It would be interesting to see if the Efron deconvolution retains more information.
* I think for *highly* heteroscedastic situations (like the one that motivates this project), the doubly deconvolved test will become more powerful that the singly deconvolved test. So the question becomes: how much heteroscedasticity until the power crosses over?


## IBM Quantum Challenge Fall 2021
# Challenge 4: 배터리 수익 최적화

최고의 경험을 위해 오른쪽 상단의 계정 메뉴에서 light 워크스페이스 테마로 전환하는 것을 추천합니다.

# QAOA 소개

최적화 문제 중 조합 최적화 문제에 대한 근사 솔루션을 찾는데 사용되는 잘 알려진 알고리즘은 **QAOA(양자 근사 최적화 알고리즘)** 입니다. Challenge-1의 금융 실습에서 이미 한 번 사용해보았지만 여전히 QAOA가 무엇인지 구석구석 살펴보지는 못했습니다. 이 챌린지에서 우리는 QAOA에 대해 더 자세히 배우게 됩니다----QAOA는 어떻게 작동하나요? 왜 필요할까요?

우선, QAOA는 무엇일까요? 간단히 말해서, QAOA는 ansatz로 알려진 매개변수화된 양자 회로와 고전적인 부분을 결합해 회로를 최적화 하는 고전-양자 하이브리드 알고리즘으로 Farhi, Goldstone 및 Gutmann(2014)에 의해 제안되었습니다[[1]](https://arxiv.org/abs/1411.4028).

QAOA는 매개변수 $(\boldsymbol{\beta}, \boldsymbol{\gamma})$를 사용하는 유니터리 $U(\boldsymbol{\beta}, \boldsymbol{\gamma})$를 사용하여 양자 상태 $|\psi(\boldsymbol{\beta}, \boldsymbol{\gamma})\rangle$를 마련하는 변분 알고리즘입니다.

알고리즘의 목표는 문제의 양자 상태 $|\psi(\boldsymbol{\beta}_{opt}, \boldsymbol{\gamma}_{opt})\rangle$가 솔루션을 인코딩 하게 되는 최적의 매개변수 $(\boldsymbol{\beta}_{opt}, \boldsymbol{\gamma}_{opt})$를 찾는 것입니다.

유니터리 $U(\boldsymbol{\beta}, \boldsymbol{\gamma})$은 특정한 형태를 가지며 두 개의 유니터리 $U(\boldsymbol{\beta}) = e^{-i \boldsymbol{\beta} H_B}$ 와 $U(\boldsymbol{\gamma}) = e^{-i \boldsymbol{\gamma} H_P}$로 구성되는데 이때 $H_{B}$는 믹싱 해밀토니안이며 $H_{P}$는 문제의 해밀토니안입니다. 이와 같은 유니터리 선택은 양자 어닐링이라는 방식에서 영감을 얻습니다.

양자 상태는 두 유니터리가 교대로 나타나는 연산자 블록을 $p$ 번 적용하는 다음과 같은 블럭을 사용해 계산됩니다:

$$\lvert \psi(\boldsymbol{\beta}, \boldsymbol{\gamma}) \rangle = \underbrace{U(\boldsymbol{\beta}) U(\boldsymbol{\gamma})
                                            \cdots U(\boldsymbol{\beta}) U(\boldsymbol{\gamma})}_{p \; \text{times}}
\lvert \psi_0 \rangle$$

여기에서 $|\psi_{0}\rangle$는 적절한 초기 상태입니다.

Qiskit의 QAOA 구현은 VQE를 직접 확장한 것으로 VQE의 일반적인 하이브리드 최적화 구조를 상속합니다. QAOA에 대한 자세한 내용은 Qiskit 텍스트북의 [QAOA 챕터](https://qiskit.org/textbook/ch-applications/qaoa.html)을 참조하십시오.

목표

배터리 수익 문제에 대한 양자 최적화 코드를 구현합니다.

계획

먼저 QAOA 및 배낭 문제에 대해 배웁니다.

**도전 4a** - QAOA를 사용한 간단한 배낭 문제: 일반적인 배낭 문제를 배우고 QAOA로 최적화된 솔루션을 찾으세요.

**최종 과제 4b** - Qiskit knapsack 클래스를 사용한 배터리 수익 최적화: 배터리 수익 최적화 문제를 배우고 QAOA로 최적화된 솔루션을 찾습니다. 4b까지의 모든 도전 과제를 해결하면 배지를 받을 수 있습니다.

**최종 과제 4c** - 직접 작성한 양자 회로로 배터리 수익 최적화: 배터리 수익 최적화 문제를 해결할 수 있는 가장 낮은 회로 비용과 회로 깊이를 지닌 양자 회로를 찾아 봅시다. 더 작은 회로로 더 나은 정확도를 달성하십시오. 이 연습문제를 풀면 순위와 함께 점수를 얻을 수 있습니다.

도전을 시작하기 전에 , [**Atsushi Matsuo와 함께하는 Qiskit 최적화 데모**](https://youtu.be/claoY57eVIc?t=104)와 세션에 사용된 [**데모 노트북**](https://github.com/qiskit-community/qiskit-application-modules-demo-sessions/tree/main/qiskit-optimization)을 살펴보고 QSVM을 분류 문제에 사용하는 법을 배워보시길 추천합니다.

방금 언급했듯이 QAOA는 다음과 같은 문제들을 포함하는 조합 최적화 문제에 대한 근사적인 솔루션을 찾는 데 사용할 수 있는 알고리즘입니다.

- TSP(Traveling Salesman Problem) 문제
- 이동 수단 라우팅(Vehicle routing) 문제
- 세트 커버(Set cover) 문제
- 배낭(Knapsack) 문제
- 일정(Scheduling) 문제 등

이 중 일부는 풀기 어려운 NP-hard 문제이거나, 합리적인 시간 내에 정확한 솔루션을 찾는 것이 비현실적이기 때문에 근사 알고리즘을 필요로 하는 문제들입니다. 이어서 QAOA를 사용하여 조합 최적화 문제 중 하나인 ----**knapsack problem**을 해결하는 사례를 소개합니다.

# 배낭(Knapsack) Problem #

[배낭(Knapsack) 문제](https://en.wikipedia.org/wiki/Knapsack_problem)는 다음과 같은 최적화 문제입니다. 각각 중량와 가치를 갖는 항목들의 목록과 최대 중량을 담을 수 있는 배낭이 주어집니다. 배낭이 담을 수 있는 최대 무게를 초과하지 않고 짐의 전체 가치을 최대화할 수 있도록 배낭에 넣을 품목을 결정하십시오. 가장 효율적인 접근 방식은 비싼것을 위주로 넣는 욕심 많은 접근 방식이겠지만 최상의 결과를 보장하지는 않습니다.

이미지 출처: [Knapsack.svg.](https://commons.wikimedia.org/w/index.php?title=File:Knapsack.svg&oldid=457280382)

노트: 배낭 문제에는 많은 변형이 있지만 여기에서는 0-1 배낭 문제에 대해서만 살펴 보겠습니다. 항목을 가져갈지 여부(0-1 속성)를 결정하는 것인데, 이는 NP-하드 문제입니다. 하나의 품목은 나눌 수 없으며 동일한 품목을 여러 개 가져갈 수 없습니다.

## 도전 4a: QAOA로 간단한 배낭 문제 풀기

**도전 4a**

용량이 18인 배낭과 5개의 짐이 문제에서 제시됩니다. 각 짐 $W$의 무게는 $w_i = [4,5,6,7,8]$이고 가치 $V$는 $v_i = [5,6,7,8,9]$일 때, 18의 용량 제한 내에서 짐의 가치가 최대가 되게 짐을 싸봅시다.

```python
from qiskit_optimization.algorithms import MinimumEigenOptimizer
from qiskit import Aer
from qiskit.utils import algorithm_globals, QuantumInstance
from qiskit.algorithms import QAOA, NumPyMinimumEigensolver
import numpy as np
```

## 동적 프로그래밍 접근 방식 ##

정확한 해답을 찾기 위해 고전적으로 보통 사용하는 방법인 동적 프로그래밍 접근 방식은 다음과 같습니다:

```python
val = [5,6,7,8,9]
wt = [4,5,6,7,8]
W = 18
```

```python
def dp(W, wt, val, n):
    k = [[0 for x in range(W + 1)] for x in range(n + 1)]
    for i in range(n + 1):
        for w in range(W + 1):
            if i == 0 or w == 0:
                k[i][w] = 0
            elif wt[i-1] <= w:
                k[i][w] = max(val[i-1] + k[i-1][w-wt[i-1]], k[i-1][w])
            else:
                k[i][w] = k[i-1][w]

    picks=[0 for x in range(n)]
    volume=W
    for i in range(n,-1,-1):
        if (k[i][volume]>k[i-1][volume]):
            picks[i-1]=1
            volume -= wt[i-1]
    return k[n][W],picks

n = len(val)
print("optimal value:", dp(W, wt, val, n)[0])
print('\n index of the chosen items:')
for i in range(n):
    if dp(W, wt, val, n)[1][i]:
        print(i,end=' ')
```

이 방식의 시간 복잡도는 $O(N*W)$입니다. 여기서 $N$은 항목 수이고 $W$는 배낭의 최대 무게입니다. 이 문제는 조합의 수가 제한되어 있기 때문에 합리적인 시간 내에 정확한 해를 얻을 수 있지만, 항목 수가 많아지면 정확한 솔루션을 구하는 것이 비현실적이 됩니다.

## QAOA 접근법 ##

Qiskit은 배낭 문제를 비롯한 다양한 최적화 문제에 대한 응용 클래스를 제공하여 사용자가 양자 컴퓨터에서 여러가지 최적화 문제를 쉽게 시도해 볼수 있게 해줍니다. 이 연습에서는 `Knapsack` 문제에 대한 응용 프로그램 클래스를 사용할 것입니다.

다른 최적화 문제에 대한 응용 프로그램 클래스도 사용해 볼 수 있습니다. 자세한 내용은 [최적화 문제의 응용 프로그램 클래스](https://qiskit.org/documentation/optimization/tutorials/09_application_classes.html#Knapsack-problem)을 살펴보세요.

```python
# import packages necessary for application classes.
from qiskit_optimization.applications import Knapsack
```

배낭 문제를 QAOA로 해결할 수 있는 최적화 문제로 구성하려면 이 문제에 대한 비용 함수를 정의하고 수식화해야 합니다.

```python
def knapsack_quadratic_program():
    # Put values, weights and max_weight parameter for the Knapsack()

    ##############################
    # Provide your code here

    # prob = Knapsack('Insert parameters here')

    #
    ##############################

    # to_quadratic_program generates a corresponding QuadraticProgram of the instance of the knapsack problem.
    kqp = prob.to_quadratic_program()
    return prob, kqp

prob,quadratic_program=knapsack_quadratic_program()
quadratic_program
```

고전적인 'NumPy MinimumEigensolver'를 사용하여 동적 프로그래밍을 사용하지 않고도 기준값으로 사용할 수 있는 최소 고유 벡터를 찾을 수 있습니다: QAOA도 사용해 봅시다.

```python
# Numpy Eigensolver
meo = MinimumEigenOptimizer(min_eigen_solver=NumPyMinimumEigensolver())
result = meo.solve(quadratic_program)
print('result:\n', result)
print('\n index of the chosen items:', prob.interpret(result))
```

```python
# QAOA
seed = 123
algorithm_globals.random_seed = seed
qins = QuantumInstance(backend=Aer.get_backend('qasm_simulator'), shots=1000, seed_simulator=seed, seed_transpiler=seed)

meo = MinimumEigenOptimizer(min_eigen_solver=QAOA(reps=1, quantum_instance=qins))
result = meo.solve(quadratic_program)
print('result:\n', result)
print('\n index of the chosen items:', prob.interpret(result))
```

`knapsack_quadratic_program` 함수로 만든 2차 프로그램을 채점을 위해 제출합니다.

```python
# Check your answer and submit using the following code
from qc_grader import grade_ex4a
grade_ex4a(quadratic_program)
```

노트: QAOA는 근사해를 찾기 때문에 QAOA의 해가 항상 최고의 해답은 아닙니다.

# 배터리 수익 최적화 문제 #

이 연습에서는 양자 알고리즘을 사용하여 조합 최적화 문제의 실제 대상인 배터리 수익 최적화 문제를 해결해봅니다.

배터리 저장 시스템은 대규모 재생 에너지(예: 풍력 및 태양열)를 전력 시스템에 유연하게 통합하는 솔루션을 제공해 왔습니다. 배터리 수익은 그리드에 판매되는 다양한 유형의 서비스에서 발생합니다. 배터리 저장 자산의 에너지 거래 프로세스는 다음과 같습니다. 규제 기관은 각 배터리 공급업체에 각 시간대별로 사전에 시장을 선택하도록 요청합니다. 배터리 운영자는 재생 가능한 에너지로 배터리를 충전하고 사전 합의된 계약에 따라 전력망에 에너지를 방출합니다. 따라서 공급업체는 최고의 수익을 얻기 위해 각 시간대에 대한 수익 및 배터리의 충전/방전 주기를 잘 예측해야 합니다.

배터리 기반 에너지 저장 장치의 수익을 극대화하는 전략은 모든 배터리 저장소 투자자의 관심사입니다. 가장 많은 비용을 시장이 지불하는 시간에 배터리가 항상 전력을 공급하도록 하면 된다고 생각할 수 있지만 실제로는 다른 많은 요소를 고려해야 합니다.

무시할 수 없는 요소 중 하나는 **열화(degradation)** 라고도 하는 배터리의 노화입니다. 배터리 충전/방전 주기가 진행됨에 따라 배터리 용량이 점차 저하됩니다(배터리가 저장할 수 있는 에너지의 양 또는 전달할 수 있는 전력의 양이 영구적으로 감소함). 여러 사이클 후에 배터리는 그 수명을 다하게 됩니다. 배터리는 사용하는 동안 성능이 저하되기 때문에 성능 저하를 고려하지 않고 최상의 현금 수익을 얻는 시간대를 차례로 선택하는 것은 배터리의 수명이내에서 - 다시말해 충전/방전 한계에 다다르기 전에- 최고의 수익으로 이어지지 않습니다.

따라서 배터리 수익을 최적화하려면 가격 예측을 기반으로 **이 시장의 수익(가치)** 와 예상되는 배터리의 **시간 경과에 따른 성능 저하(비용)** 을 모두 고려하여 각 시간대에 배터리 시장을 선택해야 합니다 —— 다른 최적화 문제들과 비슷해 보이죠?

이 문제를 해결하기 위해 양자 최적화 알고리즘을 어떻게 적용할 수 있는지 알아가보도록 하겠습니다.

이미지 출처: [pixabay](https://pixabay.com/photos/renewable-energy-environment-wind-1989416/)

## 문제 설정

이 부분은, de la Grand'rive와 Hullo의 논문의 문제 설정을 참조했습니다.[[2]](https://arxiv.org/abs/1908.02210).

두 시장 $M_{1}$ , $M_{2}$을 가정하면 모든 시간대(일반적으로 하루)동안 배터리는 최대 $n$ 시간 동안 한 시장 또는 다른 시장에서 작동합니다.
매일 매일은 독립적인 것으로 간주되어 하루동안의 최적화는 분리가능한 문제로 가정합니다. 매일 아침 배터리는 동일한 수준의 전력으로 시작하므로 충전 문제를 고려하지 않습니다. 두 시장에 대한 예측은 $n$ 시간대에 대해 가능하며, $t$(일) 및 각 시장의 특징은 모두 알고있다고 가정합니다:

- 매일 보상 $\lambda_{1}^{t}$ , $\lambda_{2}^{t}$

- 배터리의 일일 성능 저하 또는 건강 비용(사이클 수) $c_{1}^{t}$, $c_{2}^{t}$

우리는 최적의 일정, 즉 $C_{max}$ 주기보다 적은 값으로 수명 수익률을 최적화하기를 원합니다. $d = max_{t}\left\{c_{1}^{t}, c_{2}^{t}\right\} $를 정의해 봅시다.

결정 변수 $z_{t}, \forall t \in [1, n]$는 공급자가 $M_{1}$을 선택하면 $z_{t} = 0$ , $M_{2}$를 선택하면 $z_{t} = 1$이 된다고 설정하면, 모든 가능한 벡터 $z = [z_{1}, ..., z_{n}]$는 모든 가능한 일정을 나타내게 됩니다. 앞에서 공식화된 문제는 다음과 같이 표현할 수 있습니다:

\begin{equation}
\underset{z \in \left\{0,1\right\}^{n}}{max} \displaystyle\sum_{t=1}^{n}(1-z_{t})\lambda_{1}^{t}+z_{t}\lambda_{2}^{t}
\end{equation}

\begin{equation}
    s.t. \sum_{t=1}^{n}[(1-z_{t})c_{1}^{t}+z_{t}c_{2}^{t}]\leq C_{max}
\end{equation}

여기까지는 이미 다뤄본 조합 최적화 문제들과 달라 보이지만 걱정할 필요가 없습니다! 힌트를 따라 단계적으로 양자 컴퓨팅으로 이 문제를 해결해 봅시다.

# 도전 4b: qiskit knapsack 클래스를 사용한 배터리 수익 최적화 #

**도전 4b**

다음 조건에 대해 $C_{max}$ 이내의 비용으로 총 수익을 극대화하기 위해 QAOA와 함께 Qiskit 최적화 배낭 클래스를 사용하여 배터리 일정을 최적화합니다;

- the time window $t = 7$

- the daily return $\lambda_{1} = [5, 3, 3, 6, 9, 7, 1]$

- the daily return $\lambda_{2} = [8, 4, 5, 12, 10, 11, 2]$

- the daily degradation for the battery $c_{1} = [1, 1, 2, 1, 1, 1, 2]$

- the daily degradation for the battery $c_{2} = [3, 2, 3, 2, 4, 3, 3]$

- $C_{max} = 16$

여러분의 과제는 Qiskit 최적화 배낭 클래스에 사용될 인수들, `values`, `weights`, 그리고 `max_weight`를 찾아내어 "0"이 시장 $M_{1}$을,  "1"이  시장 $M_{2}$하는 경우를 나타내는 솔루션을 얻는 것입니다. 여러분의 솔루션은 다른 데이터세트인 $\lambda_{1}, \lambda_{2}, c_{1}, c_{2}, C_{max}$를 사용해 평가됩니다.

4b까지의 모든 도전 과제를 해결하면 배지를 받을 수 있습니다.

```python
L1 = [5,3,3,6,9,7,1]
L2 = [8,4,5,12,10,11,2]
C1 = [1,1,2,1,1,1,2]
C2 = [3,2,3,2,4,3,3]
C_max = 16
```

```python
def knapsack_argument(L1, L2, C1, C2, C_max):

    ##############################
    # Provide your code here

    #
    ##############################
    return values, weights, max_weight

values, weights, max_weight = knapsack_argument(L1, L2, C1, C2, C_max)
print(values, weights, max_weight)
prob = Knapsack(values = values, weights = weights, max_weight = max_weight)
qp = prob.to_quadratic_program()
qp
```

```python
# Check your answer and submit using the following code
from qc_grader import grade_ex4b
grade_ex4b(knapsack_argument)
```

 QAOA를 사용해 문제를 풀어봅시다.

```python
# QAOA
seed = 123
algorithm_globals.random_seed = seed
qins = QuantumInstance(backend=Aer.get_backend('qasm_simulator'), shots=1000, seed_simulator=seed, seed_transpiler=seed)

meo = MinimumEigenOptimizer(min_eigen_solver=QAOA(reps=1, quantum_instance=qins))
result = meo.solve(qp)
print('result:', result.x)

item = np.array(result.x)
revenue=0
for i in range(len(item)):
    if item[i]==0:
        revenue+=L1[i]
    else:
        revenue+=L2[i]

print('total revenue:', revenue)
```

## 도전 4c:  단열 양자 계산을 통한 배터리 수익 최적화

드디어 마지막 연습에 도착했습니다! 마지막 도전에서 여러분은 서로 경쟁하게 됩니다.

## 배경 지식

QAOA는 단열 양자 계산에서 영감을 받아 개발되었습니다. 단열 양자 계산은 양자 단열 정리에 기초하여 주어진 해밀토니안의 바닥 상태를 이상적으로 얻을 수 있습니다. 따라서 최적화 문제를 이 해밀토니안에 매핑하여 단열 양자 계산으로 최적화 문제를 해결할 수 있습니다.

단열 양자 계산과 양자 회로의 계산이 이론적으로 동일하다는 것은 증명되었지만, 양자 회로로 단열 양자 계산을 시뮬레이션하려면 현재의 노이즈가 있는 장치로는 달성하기 어려운 많은 수의 게이트 작업이 필요합니다. QAOA는 양자-고전 하이브리드 접근 방식을 사용하여 이 어려움을 극복합니다.

이 추가 과제에서는 단열 양자 계산 프레임워크를 기반으로 고전적 최적화 없이 최적화 문제를 해결하는 양자 회로를 구현해야 합니다. 즉, 작성한 회로는 한 번의 실행으로 대략적인 솔루션을 제공해야 합니다.

Qiskit Optimization Module 및 Knapsack 클래스를 사용하는 대신 가능한 한 적은 수의 게이트가 사용되는, 즉 가능한 한 작은 양자 회로를 구현해 보도록 도전해 보십시오. 최적화 문제의 제약을 완화하는 것으로 더 작은 회로로 최적의 솔루션을 찾는 것이 가능합니다. 솔루션 팁을 따르는 것을 추천합니다.

**도전 4c**

다음 조건에서 $C_{max}$ 이내의 비용으로 총 수익을 극대화하기 위해 단열 양자 계산을 사용하여 배터리 일정을 최적화합니다:

- the time window $t = 11$

- the daily return $\lambda_{1} = [3, 7, 3, 4, 2, 6, 2, 2, 4, 6, 6]$

- the daily return $\lambda_{2} = [7, 8, 7, 6, 6, 9, 6, 7, 6, 7, 7]$

- the daily degradation for the battery $c_{1} = [2, 2, 2, 3, 2, 4, 2, 2, 2, 2, 2]$

- the daily degradation for the battery $c_{2} = [4, 3, 3, 4, 4, 5, 3, 4, 4, 3, 4]$

- $C_{max} = 33$

    - **노트:** $\lambda_{1}[i] < \lambda_{2}[i]$ 와 $c_{1}[i] < c_{2}[i]$는 =$i \in \{1,2,...,t\}$에 대해 성립합니다.

"0"은 시장 $M_{1}$의 선택을 나타내고 "1"은 시장 $M_{2}$의 선택을 나타내며 최적 솔루션은 "00111111000"와 "10110111000"입니다. " 기대 수익의 값은 $67$이고 비용은 $33$입니다.
여러분의 과제는 정확도 기준을 만족하는 단열 양자 계산 회로를 구현하는 것입니다.
$\lambda_{1}, \lambda_{2}, c_{1}, c_{2}, C_{max}$의 다른 데이터 세트로 정답을 채점합니다. 아래에 채점을 위한 입력의 예가 있습니다. 실제 채점에는 이와 유사한 입력값을 사용합니다.

```python
instance_examples = [
    {
        'L1': [3, 7, 3, 4, 2, 6, 2, 2, 4, 6, 6],
        'L2': [7, 8, 7, 6, 6, 9, 6, 7, 6, 7, 7],
        'C1': [2, 2, 2, 3, 2, 4, 2, 2, 2, 2, 2],
        'C2': [4, 3, 3, 4, 4, 5, 3, 4, 4, 3, 4],
        'C_max': 33
    },
    {
        'L1': [4, 2, 2, 3, 5, 3, 6, 3, 8, 3, 2],
        'L2': [6, 5, 8, 5, 6, 6, 9, 7, 9, 5, 8],
        'C1': [3, 3, 2, 3, 4, 2, 2, 3, 4, 2, 2],
        'C2': [4, 4, 3, 5, 5, 3, 4, 5, 5, 3, 5],
        'C_max': 38
    },
    {
        'L1': [5, 4, 3, 3, 3, 7, 6, 4, 3, 5, 3],
        'L2': [9, 7, 5, 5, 7, 8, 8, 7, 5, 7, 9],
        'C1': [2, 2, 4, 2, 3, 4, 2, 2, 2, 2, 2],
        'C2': [3, 4, 5, 4, 4, 5, 3, 3, 5, 3, 5],
        'C_max': 35
    }
]
```

### 중요: 최종 도전과제 제출 규칙

이 과제를 풀기 위해서:

- 고전적인 방법으로 최적화하지 마십시오.
- 제시된 과정을 따라 함수에 소스 코드를 채워 양자 회로를 만듭니다.
- 매개변수 $p$ 및 $\alpha$에 대해, $p=5$ 및 $\alpha=1$의 값을 변경하지 마십시오.
- 양자회로는 28큐비트 이내로 구현해 주십시오.
- (L1, L2, C1, C2, C_max)를 입력으로 받아 QuantumCircuit을 반환하는 함수를 제출해야 합니다. (함수 이름은 마음대로 변경할 수 있습니다.)
- 여러분의 회로는 다른 입력 값을 받아 문제를 해결할 수 있어야 합니다. 여러분의 회로는 여러가지 값으로 평가 될 것입니다.
- 더 낮은 비용으로 0.8 이상의 정밀도를 제공하는 회로를 만들어 봅시다. 정밀도는 아래에 설명되어 있습니다. 비용은 낮을수록 좋습니다.

- 작업이 제대로 실행되었는지 확실하지 않다고 하더라도 **작업을 연속적으로 실행하지 마십시오**. 이 경우 긴 대기열이 생성되고 백엔드가 막힐 수 있습니다. 여러분의 작업이 잘 실행되고 있는지의 여부는 다음의 페이지에서 확인할 수 있습니다.
[**https://quantum-computing.ibm.com/jobs**](https://quantum-computing.ibm.com/jobs)

- 상위 10명의 참가자의 회로는 20개의 입력으로 확인됩니다.

- 심사위원은 상위 10개 솔루션을 수동으로 확인하여 솔루션이 규칙을 준수하는지 확인할 것입니다 **순위는 심사 과정에 따라 챌린지 기간 이후 변경될 수 있습니다.**

- 상위 10명의 참가자는 공지될 것이며, 어떻게 문제를 해결했는지 설명해 달라는 요청을 받을 수 있습니다.

**참고: 이 챌린지는 양자 회로로 문제를 해결해야 합니다. 그렇지 않으면 최종 순위에 포함될 수 없습니다.**

### Scoring Rule

제출된 함수의 점수는 두 단계로 계산됩니다.

1. 첫 번째 단계에서는 양자 회로의 출력 정밀도를 확인합니다.
이 단계를 통과하려면 회로가 8개의 인스턴스에 대해 **평균 정밀도가 0.80 이상**인 확률 분포를 출력해야 합니다. 그 중 4개는 고정 데이터이고 나머지 4개는 여러 데이터 세트에서 무작위로 선택한 데이터입니다.
여러분의 회로가 이 임계값 **0.8**을 충족할 수 없으면 점수를 얻을 수 없습니다.
제출된 양자 회로가 하나의 인스턴스를 풀 때 확률 분포의 정밀도가 어떻게 계산되는지 설명하겠습니다.

    1. 이 정밀도는 측정된 실현 가능한 솔루션의 값이 최적 솔루션의 값에 얼마나 가까운지를 평가합니다.
    2. 먼저 측정된 실현 가능한 솔루션의 수가 매우 적으며 정밀도는 0입니다(**"실현 가능한 솔루션 수"*** 아래 확인).
정밀도를 계산하기 전에 가장 낮은 값을 빼서 값이 가장 낮은 솔루션의 정밀도가 항상 0이 되도록 솔루션 값을 정규화합니다.

    $N_s$, $N_f$, $\lambda_{opt}$를 총 샷(실행 횟수), 측정된 실행 가능한 솔루션의 샷 숫자, 최적의 솔루션 값이라고 합시다. 또한 $R(x)$와 $C(x)$를 각각 솔루션 $x\in\{0,1\}^n$의 이득과 비용이라고 합시다. $\lambda_{1}$의 합계로 계산할 수 있는 인스턴스의 가장 낮은 값을 빼서 값을 정규화합니다.
     확률 분포가 주어지면 정밀도는 다음 공식으로 계산됩니다.

     \begin{equation*}
    \text{precision} = \frac 1 {N_f\cdot (\lambda_{opt}-\mathrm{sum}(\lambda_{1}) )} \sum_{x, \text{$\mathrm{shots}_x$}\in \text{ prob.dist.}} (R(x)-\mathrm{sum}(\lambda_{1})) \cdot \text{$\mathrm{shots}_x$} \cdot 1_{C(x) \leq C_{max}}
    \end{equation*}

    여기서 $\mathrm{shots}_x$는 측정된 솔루션 $x$의 샷입니다. 예를 들어, $N_s = 100$일때 확률 분포가 {"1000101": 26, "1000110": 35, "1000111": 12, "1001000": 16, "1001001": 11}인 경우 ,
     각 솔루션의 가치와 비용은 다음과 같습니다:

    | Solution | Value | Cost | Feasible or not | Shot counts|
    |:-------:|:-------:|:-------:|:-------:|:--------------:|
    | 1000101 | 46 | 16 | 1 | 26 |
    | 1000110 | 48 | 17 | 0 | 35 |
    | 1000111 | 45 | 15 | 1 | 12 |
    | 1001000 | 45 | 18 | 0 | 16 |
    | 1001001 | 42 | 16 | 1 | 11 |

    이때 $C_{max}= 16$이고, 솔루션 "1000101", "1000111" 및 "1001001"은 실행 가능하지만 솔루션 "1000110" 및 "1001000"은 실행 불가능합니다. 따라서 측정된 실행 가능한 솔루션 $N_f$의 샷은 $N_f = 26+12+11=49$로 계산됩니다. 그리고 가장 낮은 값은 $ \mathrm{sum}(\lambda_{1}) = 5+3+3+6+9+7+1=34$입니다.
    따라서, 정밀도는

    $$((46-34) \cdot 26 \cdot 1 + (48-34) \cdot 35 \cdot 0 + (45-34) \cdot 12 \cdot 1 + (45-34) \cdot 16 \cdot 0 + (42-34) \cdot 11 \cdot 1) / (49\cdot (50-34)) = 0.68$$

    이 됩니다.

2. 두 번째 단계에서는 첫 번째 단계를 통과한 양자 회로의 점수가 평가됩니다.
점수는 4개의 고정 인스턴스에 대한 회로 비용의 합계이며, 여기서 회로 비용은 다음과 같이 계산됩니다:
    1. 양자 회로를 최적화 없이 트랜스 파일 하고 게이트들을 "rz", "sx", "cx"로 분해(decompose)합니다.
    2. 그 다음 점수는 다음과 같이 계산 됩니다.

    \begin{equation*}
    \text{score} = 50 \cdot depth + 10 \cdot \#(cx) + \#(rz) + \#(sx)
    \end{equation*}

    여기에서 $\#(gate)$는 회로에 사용된 $gate$의 개수를 의미합니다.

여러분의 회로는 512번 실행될 것이며, 이는 $N_s = 512$ 임을 의미합니다.

점수가 낮을수록 순위가 높아집니다.

## 일반적 접근법

여기에서는 [[2]](https://arxiv.org/abs/1908.02210)에 제안된 방식을 따라 배낭 문제의 조건을 완화한 "느슨한" 공식을 풀어 솔루션을 얻어 보겠습니다. 조건이 완화된 문제는 다음과 같이 정의할 수 있습니다:
\begin{equation*}
\text{maximize } f(z)=return(z)+penalty(z)
\end{equation*}

\begin{equation*}
\text{where} \quad return(z)=\sum_{t=1}^{n} return_{t}(z) \quad \text{with} \quad return_{t}(z) \equiv\left(1-z_{t}\right) \lambda_{1}^{t}+z_{t} \lambda_{2}^{t}
\end{equation*}

\begin{equation*}
\quad \quad \quad \quad \quad \quad penalty(z)=\left\{\begin{array}{ll}
0 & \text{if}\quad  cost(z)<C_{\max } \\
-\alpha\left(cost(z)-C_{\max }\right) & \text{if} \quad cost(z) \geq C_{\max }, \alpha>0 \quad \text{constant}
\end{array}\right.
\end{equation*}

여기에서는 선형 페널티를 계산하기 위한 non-Ising 목표 함수가 사용됩니다.
이 방법을 통해 높은 정확도를 유지하면서 회로의 깊이를 감소시킬 수 있습니다.

각각의 기능을 하나씩 살펴봅시다.
느슨한 접근법은 다음 항목들로 구성됩니다.
1. 위상 연산자 $U(C, \gamma_i)$
     1. 반환 부분
     2. 패널티 부분
         1. 비용 계산(데이터 인코딩)
         2. 제약 조건 테스트($C_{max}$를 초과하는 데이터의 인덱스 표시)
         3. 페널티 디페이징(dephasing)(표시된 인덱스에 페널티 추가)
         4. 제약 테스트 및 비용 계산의 재초기화(데이터 레지스터 및 플래그 레지스터 정리)
2. 믹싱 연산자 $U(B, \beta_i)$

이 처리 유닛 $U(B, \beta_i)U(C, \gamma_i)$는 느슨한 접근법의 QAOA 전체 과정에서 $p$번 반복됩니다.

각각의 기능을 하나씩 살펴봅시다.

우리가 만들고자 하는 양자 회로는 인덱스 레지스터, 데이터 레지스터 및 플래그 레지스터의 세 가지 유형의 레지스터로 구성됩니다.
인덱스 레지스터와 데이터 레지스터는 가능한 모든 배터리 선택에 대한 비용 데이터를 포함하는 QRAM에 사용됩니다.
이 레지스터들은 다음과 같은 이름으로 함수 템플릿에 등장합니다.
- `qr_index`: 인덱스를 나타내는 양자 레지스터(각 시간대에서 0 또는 1 선택)
- `qr_data`: 각 인덱스와 관련된 총 비용을 나타내는 양자 레지스터
- `qr_f`: 패널티 디페이징을 위한 플래그를 저장하는 양자 레지스터

또한 다음 변수를 사용하여 각 레지스터의 큐비트 수를 나타냅니다.
- `index_qubits`: `qr_index`의 큐비트 수
- `data_qubits`: `qr_data`의 큐비트 수

도전 4c - Step 1

## 위상 연산자 $U(C, \gamma_i)$
### 기대 수익 파트
기대 수익 파트 $return (z)$는 다음과 같이 변환될 수 있습니다:

\begin{equation*}
\begin{aligned}
e^{-i \gamma_i . return(z)}\left|z\right\rangle
&=\prod_{t=1}^{n} e^{-i \gamma_i return_{t}(z)}\left|z\right\rangle \\
&=e^{i \theta} \bigotimes_{t=1}^{n} e^{-i \gamma_i z_{t}\left(\lambda_{2}^{t}-\lambda_{1}^{t}\right)}\left|z_{t}\right\rangle \\
\text{with}\quad \theta &=\sum_{t=1}^{n} \lambda_{1}^{t}\quad \text{constant}
\end{aligned}
\end{equation*}

일정한 위상 회전을 무시할 수 있으므로 기대 수익 파트 $return (z)$은 각 큐비트에 대한 회전 게이트 $U_1\left(\gamma_i \left(\lambda_{2}^{t}-\lambda_{1}^{t}\right)\right)= e^{-i \frac{\gamma_i \left(\lambda_{2}^{t}-\lambda_{1}^{t}\right)}  2}$로 구현될 수 있습니다.

다음 셀의 빈칸을 채워 `phase_return` 기능을 완성하세요.

```python
from typing import List, Union
import math
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, assemble
from qiskit.compiler import transpile
from qiskit.circuit import Gate
from qiskit.circuit.library.standard_gates import *
from qiskit.circuit.library import QFT
```

```python
def phase_return(index_qubits: int, gamma: float, L1: list, L2: list, to_gate=True) -> Union[Gate, QuantumCircuit]:
    qr_index = QuantumRegister(index_qubits, "index")
    qc = QuantumCircuit(qr_index)

    ##############################
    ### U_1(gamma * (lambda2 - lambda1)) for each qubit ###
    # Provide your code here

    ##############################

    return qc.to_gate(label=" phase return ") if to_gate else qc
```

## 위상 연산자 $U(C, \gamma_i)$
### 페널티 부분

이 부분에서는 총 비용이 제약 조건 $C_{max}$를 초과하는 인덱스 레지스터의 양자 상태에 페널티를 추가하는 방법을 고민해 봅시다.

이 부분은 위에서 사용한 방법과 같이 다음의 4단계로 구현할 수 있습니다.

1. 비용 계산(데이터 인코딩)
2. 제약 조건 테스트(데이터 값이 $C_{max}$를 초과하는 인덱스 표시)
3. 페널티 디페이징(표시된 인덱스에 페널티 추가)
4. 제약 테스트 및 비용 계산의 재초기화(데이터 레지스터 및 플래그 레지스터 정리)

도전 4c - Step 2

#### 비용 계산 (데이터 인코딩)

모든 선택가능한 답변에 대한 비용 합계를 나타내기 위해 QRAM 구조를 사용할 수 있습니다. QRAM을 양자회로로 구현하는데에 덧셈 기가 유용하게 사용됩니다. 먼저 상수 값을 더하기 위한 함수를 준비해 봅시다.

데이터에 상수 값을 더하기 위해서는
- Series of full adders
- Plain adder network [[3]](https://arxiv.org/abs/quant-ph/9511018)
- Ripple carry adder [[4]](https://arxiv.org/abs/quant-ph/0410184)
- QFT adder [[5](https://arxiv.org/abs/quant-ph/0008033), [6](https://arxiv.org/abs/1411.5949)]
- 등등...
을 사용할 수 있습니다.

QFT에 의한 상수 값 덧셈기는 다음과 같이 구현할 수 있습니다.
1. 타겟 양자 레지스터의 QFT
2. 상수값에 해당하는 양자 레지스터에 의해 제어되는 타겟 양자 레지스터의 국부 위상 회전
3. 타겟 양자 레지스터의 IQFT

다음 셀의 빈칸을 채워 `const_adder` 기능을 완성하세요.

```python
def subroutine_add_const(data_qubits: int, const: int, to_gate=True) -> Union[Gate, QuantumCircuit]:
    qc = QuantumCircuit(data_qubits)
    ##############################
    ### Phase Rotation ###
    # Provide your code here

    ##############################
    return qc.to_gate(label=" [+"+str(const)+"] ") if to_gate else qc

```

```python
def const_adder(data_qubits: int, const: int, to_gate=True) -> Union[Gate, QuantumCircuit]:

    qr_data = QuantumRegister(data_qubits, "data")
    qc = QuantumCircuit(qr_data)

    ##############################
    ### QFT ###
    # Provide your code here

    ##############################

    ##############################
    ### Phase Rotation ###
    # Use `subroutine_add_const`

    ##############################

    ##############################
    ### IQFT ###
    # Provide your code here

    ##############################
    return qc.to_gate(label=" [ +" + str(const) + "] ") if to_gate else qc
```

도전 4c - Step 3

비용을 QRAM 형식으로 다음과 같이 저장할 수 있습니다:

\begin{equation*}
\sum_{x\in\{0,1\}^t} \left|x\right\rangle\left|cost(x)\right\rangle
\end{equation*}

이때 $t$는 시간대의 개수(=인덱스 레지스터의 사이즈)이며, $x$는 모든 시간대에서의 배터리 선택 패턴을 나타냅니다.

두개의 리스트 $C^1 = \left[c_0^1, c_1^1, \cdots\right]$ 와 $C^2 = \left[c_0^2, c_1^2, \cdots\right]$가 주어졌을때,
각 인덱스 큐비트에 의해 제어된 게이트를 사용하여 데이터의 "각 선택의 총합"을 인코딩할 수 있습니다.
$i$번째 인덱스 큐비트가 $0$인 데이터에 $c_i^1$를 추가하고 $i$번째 인덱스 큐비트가 $1$인 데이터에 $c_i^2$를 추가하려면,
인덱스 레지스터의 $i$-번째 큐비트가 $0$일 때 $C_i^1$를 데이터 레지스터에 추가하고
인덱스 레지스터의 $i$-번째 큐비트가 $1$일 때 데이터 레지스터에 $C_i^2$를 추가합니다.
이 작업들은 제어된 게이트를 사용해 구현합니다.
'qiskit.circuit.Gate' 유형의 게이트에서 제어 게이트를 생성하려는 경우 'control()' 메서드를 사용하십시오.

다음 셀의 공백을 채워 `cost_calculation`  기능을 완성하세요.

```python
def cost_calculation(index_qubits: int, data_qubits: int, list1: list, list2: list, to_gate = True) -> Union[Gate, QuantumCircuit]:

    qr_index = QuantumRegister(index_qubits, "index")
    qr_data = QuantumRegister(data_qubits, "data")
    qc = QuantumCircuit(qr_index, qr_data)

    for i, (val1, val2) in enumerate(zip(list1, list2)):

        ##############################
        ### Add val2 using const_adder controlled by i-th index register (set to 1) ###
        # Provide your code here

        ##############################
        qc.x(qr_index[i])

        ##############################
        ### Add val1 using const_adder controlled by i-th index register (set to 0) ###
        # Provide your code here

        ##############################
        qc.x(qr_index[i])

    return qc.to_gate(label=" Cost Calculation ") if to_gate else qc
```

도전 4c - Step 4

#### 제한 조건 테스트

비용 계산 단계 후에, 모든 인덱스에 대해 플래그 큐비트가 0으로 설정된 얽힌 QRAM을 얻게 됩니다:

\begin{equation*}
\sum_{x\in\{0,1\}^t} \left|x\right\rangle\left|cost(x)\right\rangle\left|0\right\rangle
\end{equation*}

비용 값이 $C_{max}$보다 큰 인덱스에 선택적으로 패널티를 추가하려면 다음 상태를 준비합니다:

\begin{equation*}
\sum_{x\in\{0,1\}^t} \left|x\right\rangle\left|cost(x)\right\rangle\left|cost(x)\geq C_{max}\right\rangle
\end{equation*}

다음 셀의 공백을 채워 `constraint_testing`  기능을 완성하세요.

```python
def constraint_testing(data_qubits: int, C_max: int, to_gate = True) -> Union[Gate, QuantumCircuit]:

    qr_data = QuantumRegister(data_qubits, "data")
    qr_f = QuantumRegister(1, "flag")
    qc = QuantumCircuit(qr_data, qr_f)

    ##############################
    ### Set the flag register for indices with costs larger than C_max ###
    # Provide your code here

    ##############################

    return qc.to_gate(label=" Constraint Testing ") if to_gate else qc
```

도전 4c - Step 5

#### 페널티 디페이징

다음과 같이 총 비용이 $C_{max}$보다 큰 인덱스에 페널티를 추가합니다.

\begin{equation*}
\quad \quad \quad \quad \quad \quad penalty(z)=\left\{\begin{array}{ll}
0 & \text{if}\quad  cost(z)<C_{\max } \\
-\alpha\left(cost(z)-C_{\max }\right) & \text{if} \quad cost(z) \geq C_{\max }, \alpha>0 \quad \text{constant}
\end{array}\right.
\end{equation*}

이 패널티는 양자 오퍼레이터$e^{i \gamma \alpha\left(cost(z)-C_{\max }\right)}$로 표현됩니다.

이 유니터리 연산자를 양자회로로 구현하기 위해 다음의 속성을 살펴봅시다.

\begin{equation*}
\alpha\left(cost(z)-C_{m a x}\right)=\sum_{j=0}^{k-1} 2^{j} \alpha A_{1}[j]-2^{c} \alpha
\end{equation*}

여기서 $A_1$는 QRAM 데이터에 대한 양자 레지스터이고, $A_1[j]$는 $A_1$의 $j$-번째 큐비트이며, $k$ 및 $c$는 적절한 상수입니다.

이 속성을 사용하여 페널티 위상의 회전 부분은 플래그 레지스터에 의해 제어되는 QRAM의 데이터 레지스터의 각 자릿수에서 회전 게이트로 구현될 수 있습니다.

다음 셀의 공백을 채워 `penalty_dephasing` 기능을 완성하세요.

```python
def penalty_dephasing(data_qubits: int, alpha: float, gamma: float, to_gate = True) -> Union[Gate, QuantumCircuit]:

    qr_data = QuantumRegister(data_qubits, "data")
    qr_f = QuantumRegister(1, "flag")
    qc = QuantumCircuit(qr_data, qr_f)

    ##############################
    ### Phase Rotation ###
    # Provide your code here

    ##############################

    return qc.to_gate(label=" Penalty Dephasing ") if to_gate else qc
```

도전 4c - Step 6

#### 재초기화

데이터 레지스터 및 플래그 레지스터와 같은 보조 큐비트는 $U(C, \gamma_i)$ 연산자가 완료되면 0 상태로 다시 초기화되어야 합니다.

`qiskit.circuit.Gate`의 역(inverse) 유니터리를 적용하려면 `inverse()` 메소드를 사용하십시오

다음 셀의 공백을 채워 `reinitialization` 기능을 완성하세요.

```python
def reinitialization(index_qubits: int, data_qubits: int, C1: list, C2: list, C_max: int, to_gate = True) -> Union[Gate, QuantumCircuit]:

    qr_index = QuantumRegister(index_qubits, "index")
    qr_data = QuantumRegister(data_qubits, "data")
    qr_f = QuantumRegister(1, "flag")
    qc = QuantumCircuit(qr_index, qr_data, qr_f)

    ##############################
    ### Reinitialization Circuit ###
    # Provide your code here

    ##############################

    return qc.to_gate(label=" Reinitialization ") if to_gate else qc
```

도전 4c - Step 7

### 믹싱 연산자 $U(B, \beta_i)$

최종적으로, 믹싱 연산자 $U(B,\beta_i)$를 위상 연산자 $U(C,\gamma_i)$ 뒤에 추가합니다.
믹싱 연산자는 다음과 같이 나타낼 수 있습니다.
\begin{equation*}
U(B, \beta_i)=\exp (-i \beta_i B)=\prod_{i=j}^{n} \exp \left(-i \beta_i \sigma_{j}^{x}\right)
\end{equation*}

이 연산자는 인덱스 레지스터에 따라 각각의 큐빗에 $R_x(2\beta_i)$ 게이트로 구현됩니다. .

다음 셀의 공백을 채워 `mixing_operator` 기능을 완성하세요.

```python
def mixing_operator(index_qubits: int, beta: float, to_gate = True) -> Union[Gate, QuantumCircuit]:

    qr_index = QuantumRegister(index_qubits, "index")
    qc = QuantumCircuit(qr_index)

    ##############################
    ### Mixing Operator ###
    # Provide your code here

    ##############################

    return qc.to_gate(label=" Mixing Operator ") if to_gate else qc
```

도천 4c - Step 8

마지막으로, 위에서 제작한 함수들로, 전체 느슨한 QAOA 프로세스를 제출하는 `solver_function`을 구성하도록 합시다.

다음 셀의 비어있는 TODO 부분을 완성하여 정답 합수를 마무리 합시다.
- 위에서 만든 함수를 복사/붙여넣기 할 수 있습니다.
- 필요한 경우 큐비트의 숫자와 배열을 수정할 수 있습니다.

```python
def solver_function(L1: list, L2: list, C1: list, C2: list, C_max: int) -> QuantumCircuit:

    # the number of qubits representing answers
    index_qubits = len(L1)

    # the maximum possible total cost
    max_c = sum([max(l0, l1) for l0, l1 in zip(C1, C2)])

    # the number of qubits representing data values can be defined using the maximum possible total cost as follows:
    data_qubits = math.ceil(math.log(max_c, 2)) + 1 if not max_c & (max_c - 1) == 0 else math.ceil(math.log(max_c, 2)) + 2

    ### Phase Operator ###
    # return part
    def phase_return():
        ##############################
        ### TODO ###
        ### Paste your code from above cells here ###

        ##############################

    # penalty part
    def subroutine_add_const():
        ##############################
        ### TODO ###
        ### Paste your code from above cells here ###

        ##############################

    # penalty part
    def const_adder():
        ##############################
        ### TODO ###
        ### Paste your code from above cells here ###

        ##############################

    # penalty part
    def cost_calculation():
        ##############################
        ### TODO ###
        ### Paste your code from above cells here ###

        ##############################

    # penalty part
    def constraint_testing():
        ##############################
        ### TODO ###
        ### Paste your code from above cells here ###

        ##############################

    # penalty part
    def penalty_dephasing():
        ##############################
        ### TODO ###
        ### Paste your code from above cells here ###

        ##############################

    # penalty part
    def reinitialization():
        ##############################
        ### TODO ###
        ### Paste your code from above cells here ###

        ##############################

    ### Mixing Operator ###
    def mixing_operator():
        ##############################
        ### TODO ###
        ### Paste your code from above cells here ###

        ##############################

    qr_index = QuantumRegister(index_qubits, "index") # index register
    qr_data = QuantumRegister(data_qubits, "data") # data register
    qr_f = QuantumRegister(1, "flag") # flag register
    cr_index = ClassicalRegister(index_qubits, "c_index") # classical register storing the measurement result of index register
    qc = QuantumCircuit(qr_index, qr_data, qr_f, cr_index)

    ### initialize the index register with uniform superposition state ###
    qc.h(qr_index)

    ### DO NOT CHANGE THE CODE BELOW
    p = 5
    alpha = 1
    for i in range(p):

        ### set fixed parameters for each round ###
        beta = 1 - (i + 1) / p
        gamma = (i + 1) / p

        ### return part ###
        qc.append(phase_return(index_qubits, gamma, L1, L2), qr_index)

        ### step 1: cost calculation ###
        qc.append(cost_calculation(index_qubits, data_qubits, C1, C2), qr_index[:] + qr_data[:])

        ### step 2: Constraint testing ###
        qc.append(constraint_testing(data_qubits, C_max), qr_data[:] + qr_f[:])

        ### step 3: penalty dephasing ###
        qc.append(penalty_dephasing(data_qubits, alpha, gamma), qr_data[:] + qr_f[:])

        ### step 4: reinitialization ###
        qc.append(reinitialization(index_qubits, data_qubits, C1, C2, C_max), qr_index[:] + qr_data[:] + qr_f[:])

        ### mixing operator ###
        qc.append(mixing_operator(index_qubits, beta), qr_index)

    ### measure the index ###
    ### since the default measurement outcome is shown in big endian, it is necessary to reverse the classical bits in order to unify the endian ###
    qc.measure(qr_index, cr_index[::-1])

    return qc
```

검증 함수는 4개의 입력 인스턴스를 갖습니다
검증 함수의 출력은 최종 점수가 매겨지기 전에 8개의 입력에 대해 정밀도 임계값 0.80을 통과해야 합니다.

```python
# Execute your circuit with following prepare_ex4c() function.
# The prepare_ex4c() function works like the execute() function with only QuantumCircuit as an argument.
from qc_grader import prepare_ex4c
job = prepare_ex4c(solver_function)

result = job.result()
```

```python
# Check your answer and submit using the following code
from qc_grader import grade_ex4c
grade_ex4c(job)
```

### 참고자료
1. Edward Farhi and Jeffrey Goldstone and Sam Gutmann (2014). A Quantum Approximate Optimization Algorithm. (https://arxiv.org/abs/1411.4028)
2. Grand'rive, Pierre & Hullo, Jean-Francois (2019). Knapsack Problem variants of QAOA for battery revenue optimisation.  (https://arxiv.org/abs/1908.02210)
3. V. Vedral, A. Barenco, A. Ekert (1995). Quantum Networks for Elementary Arithmetic Operations. (https://arxiv.org/abs/quant-ph/9511018)
4. Steven A. Cuccaro, Thomas G. Draper, Samuel A. Kutin, David Petrie Moulton (2004). A new quantum ripple-carry addition circuit. (https://arxiv.org/abs/quant-ph/0410184)
5. Thomas G. Draper (2000). Addition on a Quantum Computer (https://arxiv.org/abs/quant-ph/0008033)
6. Lidia Ruiz-Perez, Juan Carlos Garcia-Escartin (2014). Quantum arithmetic with the Quantum Fourier Transform. (https://arxiv.org/abs/1411.5949)

## 추가 정보

**제작자:** Bo Yang, Hyungseok Chang, Sitong Liu, Kifumi Numata

**한글번역:** 신소영, 김정원

**Version:** 1.0.1