## **Surface Crack Detection using  Quantum-classical Neural Networks enabled by QKA with PyTorch and Qiskit**

CNN is a type of neural network which uses a special operation called convolution and works best on image dataset.

There are a lot of preprocessing methods that can be used in this pipeline.

Convolution is a multiplication operation between an input and a filter. It's not a simple multiplication process, rather, it is computed by multiplying the input with the filter over a delayed period of time.

**Input * filter -> Answer**

As more and more ideas based on Hybrid Learning Environment come up with combining classical and quantum world to solve various problems.
Here is one approach that uses a machine learning framework, PyTorch to create a Hybrid Learning Environment.
 With this Hybrid approach, a system can be created  with classical neural network, but with some of the nodes replaced with quantum circuit.

**Quantum Kernel Alignment (QKA):** Quantum computers allow to evaluate kernels to be used e.g. in Support Vector Machines (SVM), Quantum Neural Network (QNN)  on exponentially large feature spaces. This may help to improve the performance of machine learning models and lead to higher accuracies. QKA extends this idea to parametrized kernels that are trained to best match the data.

**Implementation:**

>Step 0 : Installing & Importing Required Python Modules
Step 1 : Load the image dataset.
 Step 2 : Preprocessing the image dataset.
Step 3: Calculating the Kernel Matrix so can be used as weights for a custom convolutional layer
 Step 4 : Defining the model architecture and  adding a custom layer for training our model.
 Step 5 : Train our model for 20 epochs.
 Step 6 : Using the trained model to make inference of test dataset.
 Step 7 : Visualizing the results of data

##**Installing Required Modules**

```python
!pip install qiskit
!pip install qiskit-aer-gpu
!pip install qiskit_machine_learning
!pip install torch
!pip install torchvision
!pip install kaggle
```

    Collecting qiskit
      Downloading qiskit-0.30.0.tar.gz (12 kB)
    Collecting qiskit-terra==0.18.2
      Downloading qiskit_terra-0.18.2-cp37-cp37m-manylinux2010_x86_64.whl (6.1 MB)
         |████████████████████████████████| 6.1 MB 5.1 MB/s
    Collecting qiskit-aer==0.9.0
      Downloading qiskit_aer-0.9.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (17.9 MB)
         |████████████████████████████████| 17.9 MB 109 kB/s
    Collecting qiskit-ibmq-provider==0.16.0
      Downloading qiskit_ibmq_provider-0.16.0-py3-none-any.whl (235 kB)
         |████████████████████████████████| 235 kB 49.7 MB/s
    Collecting qiskit-ignis==0.6.0
      Downloading qiskit_ignis-0.6.0-py3-none-any.whl (207 kB)
         |████████████████████████████████| 207 kB 45.9 MB/s
    Collecting qiskit-aqua==0.9.5
      Downloading qiskit_aqua-0.9.5-py3-none-any.whl (2.1 MB)
         |████████████████████████████████| 2.1 MB 31.0 MB/s
    Requirement already satisfied: numpy>=1.16.3 in /usr/local/lib/python3.7/dist-packages (from qiskit-aer==0.9.0->qiskit) (1.19.5)
    Collecting pybind11>=2.6
      Downloading pybind11-2.7.1-py2.py3-none-any.whl (200 kB)
         |████████████████████████████████| 200 kB 19.8 MB/s
    Requirement already satisfied: scipy>=1.0 in /usr/local/lib/python3.7/dist-packages (from qiskit-aer==0.9.0->qiskit) (1.4.1)
    Collecting dlx<=1.0.4
      Downloading dlx-1.0.4.tar.gz (5.5 kB)
    Requirement already satisfied: sympy>=1.3 in /usr/local/lib/python3.7/dist-packages (from qiskit-aqua==0.9.5->qiskit) (1.7.1)
    Collecting docplex>=2.21.207
      Downloading docplex-2.22.213.tar.gz (634 kB)
         |████████████████████████████████| 634 kB 33.9 MB/s
    Requirement already satisfied: fastdtw<=0.3.4 in /usr/local/lib/python3.7/dist-packages (from qiskit-aqua==0.9.5->qiskit) (0.3.4)
    Requirement already satisfied: setuptools>=40.1.0 in /usr/local/lib/python3.7/dist-packages (from qiskit-aqua==0.9.5->qiskit) (57.4.0)
    Requirement already satisfied: pandas in /usr/local/lib/python3.7/dist-packages (from qiskit-aqua==0.9.5->qiskit) (1.1.5)
    Collecting yfinance>=0.1.62
      Downloading yfinance-0.1.63.tar.gz (26 kB)
    Collecting retworkx>=0.8.0
      Downloading retworkx-0.10.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl (1.4 MB)
         |████████████████████████████████| 1.4 MB 28.7 MB/s
    Requirement already satisfied: psutil>=5 in /usr/local/lib/python3.7/dist-packages (from qiskit-aqua==0.9.5->qiskit) (5.4.8)
    Collecting quandl
      Downloading Quandl-3.6.1-py2.py3-none-any.whl (26 kB)
    Requirement already satisfied: h5py<3.3.0 in /usr/local/lib/python3.7/dist-packages (from qiskit-aqua==0.9.5->qiskit) (3.1.0)
    Requirement already satisfied: scikit-learn>=0.20.0 in /usr/local/lib/python3.7/dist-packages (from qiskit-aqua==0.9.5->qiskit) (0.22.2.post1)
    Requirement already satisfied: python-dateutil>=2.8.0 in /usr/local/lib/python3.7/dist-packages (from qiskit-ibmq-provider==0.16.0->qiskit) (2.8.2)
    Requirement already satisfied: requests>=2.19 in /usr/local/lib/python3.7/dist-packages (from qiskit-ibmq-provider==0.16.0->qiskit) (2.23.0)
    Collecting requests-ntlm>=1.1.0
      Downloading requests_ntlm-1.1.0-py2.py3-none-any.whl (5.7 kB)
    Requirement already satisfied: urllib3>=1.21.1 in /usr/local/lib/python3.7/dist-packages (from qiskit-ibmq-provider==0.16.0->qiskit) (1.24.3)
    Collecting websocket-client>=1.0.1
      Downloading websocket_client-1.2.1-py2.py3-none-any.whl (52 kB)
         |████████████████████████████████| 52 kB 1.3 MB/s
    Requirement already satisfied: dill>=0.3 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra==0.18.2->qiskit) (0.3.4)
    Collecting python-constraint>=1.4
      Downloading python-constraint-1.4.0.tar.bz2 (18 kB)
    Collecting fastjsonschema>=2.10
      Downloading fastjsonschema-2.15.1-py3-none-any.whl (21 kB)
    Collecting tweedledum<2.0,>=1.1
      Downloading tweedledum-1.1.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (943 kB)
         |████████████████████████████████| 943 kB 46.3 MB/s
    Collecting ply>=3.10
      Downloading ply-3.11-py2.py3-none-any.whl (49 kB)
         |████████████████████████████████| 49 kB 5.7 MB/s
    Requirement already satisfied: jsonschema>=2.6 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra==0.18.2->qiskit) (2.6.0)
    Collecting symengine>0.7
      Downloading symengine-0.8.1-cp37-cp37m-manylinux2010_x86_64.whl (38.2 MB)
         |████████████████████████████████| 38.2 MB 24 kB/s
    Requirement already satisfied: six in /usr/local/lib/python3.7/dist-packages (from docplex>=2.21.207->qiskit-aqua==0.9.5->qiskit) (1.15.0)
    Requirement already satisfied: cached-property in /usr/local/lib/python3.7/dist-packages (from h5py<3.3.0->qiskit-aqua==0.9.5->qiskit) (1.5.2)
    Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.7/dist-packages (from requests>=2.19->qiskit-ibmq-provider==0.16.0->qiskit) (2021.5.30)
    Requirement already satisfied: idna<3,>=2.5 in /usr/local/lib/python3.7/dist-packages (from requests>=2.19->qiskit-ibmq-provider==0.16.0->qiskit) (2.10)
    Requirement already satisfied: chardet<4,>=3.0.2 in /usr/local/lib/python3.7/dist-packages (from requests>=2.19->qiskit-ibmq-provider==0.16.0->qiskit) (3.0.4)
    Collecting ntlm-auth>=1.0.2
      Downloading ntlm_auth-1.5.0-py2.py3-none-any.whl (29 kB)
    Collecting cryptography>=1.3
      Downloading cryptography-3.4.8-cp36-abi3-manylinux_2_24_x86_64.whl (3.0 MB)
         |████████████████████████████████| 3.0 MB 21.8 MB/s
    Requirement already satisfied: cffi>=1.12 in /usr/local/lib/python3.7/dist-packages (from cryptography>=1.3->requests-ntlm>=1.1.0->qiskit-ibmq-provider==0.16.0->qiskit) (1.14.6)
    Requirement already satisfied: pycparser in /usr/local/lib/python3.7/dist-packages (from cffi>=1.12->cryptography>=1.3->requests-ntlm>=1.1.0->qiskit-ibmq-provider==0.16.0->qiskit) (2.20)
    Requirement already satisfied: joblib>=0.11 in /usr/local/lib/python3.7/dist-packages (from scikit-learn>=0.20.0->qiskit-aqua==0.9.5->qiskit) (1.0.1)
    Requirement already satisfied: mpmath>=0.19 in /usr/local/lib/python3.7/dist-packages (from sympy>=1.3->qiskit-aqua==0.9.5->qiskit) (1.2.1)
    Requirement already satisfied: multitasking>=0.0.7 in /usr/local/lib/python3.7/dist-packages (from yfinance>=0.1.62->qiskit-aqua==0.9.5->qiskit) (0.0.9)
    Collecting lxml>=4.5.1
      Downloading lxml-4.6.3-cp37-cp37m-manylinux2014_x86_64.whl (6.3 MB)
         |████████████████████████████████| 6.3 MB 27.2 MB/s
    Requirement already satisfied: pytz>=2017.2 in /usr/local/lib/python3.7/dist-packages (from pandas->qiskit-aqua==0.9.5->qiskit) (2018.9)
    Collecting inflection>=0.3.1
      Downloading inflection-0.5.1-py2.py3-none-any.whl (9.5 kB)
    Requirement already satisfied: more-itertools in /usr/local/lib/python3.7/dist-packages (from quandl->qiskit-aqua==0.9.5->qiskit) (8.9.0)
    Building wheels for collected packages: qiskit, dlx, docplex, python-constraint, yfinance
      Building wheel for qiskit (setup.py) ... done
      Created wheel for qiskit: filename=qiskit-0.30.0-py3-none-any.whl size=11273 sha256=18ce3ea7040c87a50a7873662aa6e7475ffbd9a6534e402b8c606790dd8ce4b5
      Stored in directory: /root/.cache/pip/wheels/30/2b/0f/42f529b566315599b81731f2a58728da92883b00cccb78b6c1
      Building wheel for dlx (setup.py) ... done
      Created wheel for dlx: filename=dlx-1.0.4-py3-none-any.whl size=5719 sha256=c2d7b996f48f286fed4c700515b5d485ae4e927134dec2b26d5738e8dd76a3c8
      Stored in directory: /root/.cache/pip/wheels/78/55/c8/dc61e772445a566b7608a476d151e9dcaf4e092b01b0c4bc3c
      Building wheel for docplex (setup.py) ... done
      Created wheel for docplex: filename=docplex-2.22.213-py3-none-any.whl size=696881 sha256=5dd7157bda34ba1d1afd82023f3df380515564632b64797c08463f666d36f028
      Stored in directory: /root/.cache/pip/wheels/90/69/6b/1375c68a5b7ff94c40263b151c86f58bd72200bf0c465b5ba3
      Building wheel for python-constraint (setup.py) ... done
      Created wheel for python-constraint: filename=python_constraint-1.4.0-py2.py3-none-any.whl size=24081 sha256=362e7f8bad422b1fda7fe5e927dd362330fbf424b56bd9f5cb98c0e0e87ab963
      Stored in directory: /root/.cache/pip/wheels/07/27/db/1222c80eb1e431f3d2199c12569cb1cac60f562a451fe30479
      Building wheel for yfinance (setup.py) ... done
      Created wheel for yfinance: filename=yfinance-0.1.63-py2.py3-none-any.whl size=23918 sha256=4b0d0348c45645f81372f1c8bad4822fc29cf12ce933f5b7e13d9b5fa228088d
      Stored in directory: /root/.cache/pip/wheels/fe/87/8b/7ec24486e001d3926537f5f7801f57a74d181be25b11157983
    Successfully built qiskit dlx docplex python-constraint yfinance
    Installing collected packages: tweedledum, symengine, retworkx, python-constraint, ply, fastjsonschema, qiskit-terra, ntlm-auth, lxml, inflection, cryptography, yfinance, websocket-client, requests-ntlm, quandl, qiskit-ignis, pybind11, docplex, dlx, qiskit-ibmq-provider, qiskit-aqua, qiskit-aer, qiskit
      Attempting uninstall: lxml
        Found existing installation: lxml 4.2.6
        Uninstalling lxml-4.2.6:
          Successfully uninstalled lxml-4.2.6
    Successfully installed cryptography-3.4.8 dlx-1.0.4 docplex-2.22.213 fastjsonschema-2.15.1 inflection-0.5.1 lxml-4.6.3 ntlm-auth-1.5.0 ply-3.11 pybind11-2.7.1 python-constraint-1.4.0 qiskit-0.30.0 qiskit-aer-0.9.0 qiskit-aqua-0.9.5 qiskit-ibmq-provider-0.16.0 qiskit-ignis-0.6.0 qiskit-terra-0.18.2 quandl-3.6.1 requests-ntlm-1.1.0 retworkx-0.10.2 symengine-0.8.1 tweedledum-1.1.1 websocket-client-1.2.1 yfinance-0.1.63
    Collecting qiskit-aer-gpu
      Downloading qiskit_aer_gpu-0.9.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (26.7 MB)
         |████████████████████████████████| 26.7 MB 93 kB/s
    Requirement already satisfied: numpy>=1.16.3 in /usr/local/lib/python3.7/dist-packages (from qiskit-aer-gpu) (1.19.5)
    Requirement already satisfied: scipy>=1.0 in /usr/local/lib/python3.7/dist-packages (from qiskit-aer-gpu) (1.4.1)
    Requirement already satisfied: pybind11>=2.6 in /usr/local/lib/python3.7/dist-packages (from qiskit-aer-gpu) (2.7.1)
    Requirement already satisfied: qiskit-terra>=0.17.0 in /usr/local/lib/python3.7/dist-packages (from qiskit-aer-gpu) (0.18.2)
    Requirement already satisfied: jsonschema>=2.6 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra>=0.17.0->qiskit-aer-gpu) (2.6.0)
    Requirement already satisfied: python-dateutil>=2.8.0 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra>=0.17.0->qiskit-aer-gpu) (2.8.2)
    Requirement already satisfied: python-constraint>=1.4 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra>=0.17.0->qiskit-aer-gpu) (1.4.0)
    Requirement already satisfied: fastjsonschema>=2.10 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra>=0.17.0->qiskit-aer-gpu) (2.15.1)
    Requirement already satisfied: retworkx>=0.9.0 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra>=0.17.0->qiskit-aer-gpu) (0.10.2)
    Requirement already satisfied: tweedledum<2.0,>=1.1 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra>=0.17.0->qiskit-aer-gpu) (1.1.1)
    Requirement already satisfied: dill>=0.3 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra>=0.17.0->qiskit-aer-gpu) (0.3.4)
    Requirement already satisfied: ply>=3.10 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra>=0.17.0->qiskit-aer-gpu) (3.11)
    Requirement already satisfied: sympy>=1.3 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra>=0.17.0->qiskit-aer-gpu) (1.7.1)
    Requirement already satisfied: psutil>=5 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra>=0.17.0->qiskit-aer-gpu) (5.4.8)
    Requirement already satisfied: symengine>0.7 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra>=0.17.0->qiskit-aer-gpu) (0.8.1)
    Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.7/dist-packages (from python-dateutil>=2.8.0->qiskit-terra>=0.17.0->qiskit-aer-gpu) (1.15.0)
    Requirement already satisfied: mpmath>=0.19 in /usr/local/lib/python3.7/dist-packages (from sympy>=1.3->qiskit-terra>=0.17.0->qiskit-aer-gpu) (1.2.1)
    Installing collected packages: qiskit-aer-gpu
    Successfully installed qiskit-aer-gpu-0.9.0
    Collecting qiskit_machine_learning
      Downloading qiskit_machine_learning-0.2.1-py3-none-any.whl (96 kB)
         |████████████████████████████████| 96 kB 2.7 MB/s
    Requirement already satisfied: scipy>=1.4 in /usr/local/lib/python3.7/dist-packages (from qiskit_machine_learning) (1.4.1)
    Requirement already satisfied: scikit-learn>=0.20.0 in /usr/local/lib/python3.7/dist-packages (from qiskit_machine_learning) (0.22.2.post1)
    Requirement already satisfied: fastdtw in /usr/local/lib/python3.7/dist-packages (from qiskit_machine_learning) (0.3.4)
    Requirement already satisfied: psutil>=5 in /usr/local/lib/python3.7/dist-packages (from qiskit_machine_learning) (5.4.8)
    Requirement already satisfied: qiskit-terra>=0.18.0 in /usr/local/lib/python3.7/dist-packages (from qiskit_machine_learning) (0.18.2)
    Requirement already satisfied: numpy>=1.17 in /usr/local/lib/python3.7/dist-packages (from qiskit_machine_learning) (1.19.5)
    Requirement already satisfied: setuptools>=40.1.0 in /usr/local/lib/python3.7/dist-packages (from qiskit_machine_learning) (57.4.0)
    Requirement already satisfied: jsonschema>=2.6 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra>=0.18.0->qiskit_machine_learning) (2.6.0)
    Requirement already satisfied: python-constraint>=1.4 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra>=0.18.0->qiskit_machine_learning) (1.4.0)
    Requirement already satisfied: ply>=3.10 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra>=0.18.0->qiskit_machine_learning) (3.11)
    Requirement already satisfied: symengine>0.7 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra>=0.18.0->qiskit_machine_learning) (0.8.1)
    Requirement already satisfied: fastjsonschema>=2.10 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra>=0.18.0->qiskit_machine_learning) (2.15.1)
    Requirement already satisfied: sympy>=1.3 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra>=0.18.0->qiskit_machine_learning) (1.7.1)
    Requirement already satisfied: dill>=0.3 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra>=0.18.0->qiskit_machine_learning) (0.3.4)
    Requirement already satisfied: python-dateutil>=2.8.0 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra>=0.18.0->qiskit_machine_learning) (2.8.2)
    Requirement already satisfied: retworkx>=0.9.0 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra>=0.18.0->qiskit_machine_learning) (0.10.2)
    Requirement already satisfied: tweedledum<2.0,>=1.1 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra>=0.18.0->qiskit_machine_learning) (1.1.1)
    Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.7/dist-packages (from python-dateutil>=2.8.0->qiskit-terra>=0.18.0->qiskit_machine_learning) (1.15.0)
    Requirement already satisfied: joblib>=0.11 in /usr/local/lib/python3.7/dist-packages (from scikit-learn>=0.20.0->qiskit_machine_learning) (1.0.1)
    Requirement already satisfied: mpmath>=0.19 in /usr/local/lib/python3.7/dist-packages (from sympy>=1.3->qiskit-terra>=0.18.0->qiskit_machine_learning) (1.2.1)
    Installing collected packages: qiskit-machine-learning
    Successfully installed qiskit-machine-learning-0.2.1
    Requirement already satisfied: torch in /usr/local/lib/python3.7/dist-packages (1.9.0+cu102)
    Requirement already satisfied: typing-extensions in /usr/local/lib/python3.7/dist-packages (from torch) (3.7.4.3)
    Requirement already satisfied: torchvision in /usr/local/lib/python3.7/dist-packages (0.10.0+cu102)
    Requirement already satisfied: torch==1.9.0 in /usr/local/lib/python3.7/dist-packages (from torchvision) (1.9.0+cu102)
    Requirement already satisfied: numpy in /usr/local/lib/python3.7/dist-packages (from torchvision) (1.19.5)
    Requirement already satisfied: pillow>=5.3.0 in /usr/local/lib/python3.7/dist-packages (from torchvision) (7.1.2)
    Requirement already satisfied: typing-extensions in /usr/local/lib/python3.7/dist-packages (from torch==1.9.0->torchvision) (3.7.4.3)
    Requirement already satisfied: kaggle in /usr/local/lib/python3.7/dist-packages (1.5.12)
    Requirement already satisfied: urllib3 in /usr/local/lib/python3.7/dist-packages (from kaggle) (1.24.3)
    Requirement already satisfied: python-dateutil in /usr/local/lib/python3.7/dist-packages (from kaggle) (2.8.2)
    Requirement already satisfied: tqdm in /usr/local/lib/python3.7/dist-packages (from kaggle) (4.62.2)
    Requirement already satisfied: requests in /usr/local/lib/python3.7/dist-packages (from kaggle) (2.23.0)
    Requirement already satisfied: certifi in /usr/local/lib/python3.7/dist-packages (from kaggle) (2021.5.30)
    Requirement already satisfied: python-slugify in /usr/local/lib/python3.7/dist-packages (from kaggle) (5.0.2)
    Requirement already satisfied: six>=1.10 in /usr/local/lib/python3.7/dist-packages (from kaggle) (1.15.0)
    Requirement already satisfied: text-unidecode>=1.3 in /usr/local/lib/python3.7/dist-packages (from python-slugify->kaggle) (1.3)
    Requirement already satisfied: chardet<4,>=3.0.2 in /usr/local/lib/python3.7/dist-packages (from requests->kaggle) (3.0.4)
    Requirement already satisfied: idna<3,>=2.5 in /usr/local/lib/python3.7/dist-packages (from requests->kaggle) (2.10)

##**Importing Modules**

```python
# If local dataset is not available
! mkdir ~/.kaggle                           # Make directory named kaggle
! cp kaggle.json ~/.kaggle/                 # Copy kaggle.json file there
! chmod 600 ~/.kaggle/kaggle.json           # Change the permissions of the file.
```

```python
# Importing Required Python Modules
import numpy as np
import os
cwd = os.getcwd()
from PIL import Image
import time
import copy
import random
import cv2
import re
import shutil
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
%matplotlib inline

#Importing Pytorch Specific Modules
import torch
from torch.autograd import Function
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader,ConcatDataset
from torch.utils.data import sampler
from torch.optim import lr_scheduler
from torchvision import datasets, transforms,models
import torch.nn.functional as F
import torchvision
from torchvision.datasets import MNIST
from sklearn.model_selection import KFold

#Importing Qiskit Specific Modules
import qiskit
from qiskit import transpile, assemble
from qiskit.visualization import *
from qiskit  import Aer, QuantumCircuit
from qiskit.utils import QuantumInstance
from qiskit.circuit import Parameter
from qiskit.opflow import AerPauliExpectation
from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap

# # Qiskit Machine Learning
from qiskit_machine_learning.neural_networks import CircuitQNN, TwoLayerQNN
from qiskit_machine_learning.connectors import TorchConnector
from qiskit_machine_learning.kernels import QuantumKernel
## If local dataset is not available then uncomment below line
import kaggle
```

#**Checking for GPU Availability**

```python
simulator = qiskit.Aer.get_backend('aer_simulator')

use_cuda = torch.cuda.is_available()

print('CUDA available:', use_cuda)

if use_cuda:
    device = torch.device('cuda')
    print('Training on GPU...')
    simulator.set_options(device='GPU')
else:
    device = torch.device('cpu')
    print('Training on CPU...')
```

    CUDA available: True
    Training on GPU...

## **Step 1: Load the Dataset and Visualize it**
* Link to the cracks data set:  https://www.kaggle.com/arunrk7/surface-crack-detection/metadata

```python
!kaggle datasets download -d arunrk7/surface-crack-detection     # Downloading the zip file using Kaggle Credentials
```

    Downloading surface-crack-detection.zip to /content
     98% 228M/233M [00:01<00:00, 158MB/s]
    100% 233M/233M [00:01<00:00, 153MB/s]

```python
!unzip surface-crack-detection.zip                            # Unzip the data
from IPython.display import clear_output
clear_output()
```

```python
crack_images = os.listdir('Positive/')
print("Number of Crack Images: ", len(crack_images))
```

    Number of Crack Images:  20000

```python
no_crack_images = os.listdir('Negative/')
print("Number of No Crack Images: ", len(no_crack_images))
```

    Number of No Crack Images:  20000

Visualize Random images with cracks

```python
random_indices = np.random.randint(0, len(crack_images), size=4)
print("*****************Random Images with Cracks**************************")
random_images = np.array(crack_images)[random_indices.astype(int)]

f, axarr = plt.subplots(2,2)
axarr[0,0].imshow(mpimg.imread(os.path.join(cwd, 'Positive', random_images[0])))
axarr[0,1].imshow(mpimg.imread(os.path.join(cwd, 'Positive', random_images[1])))
axarr[1,0].imshow(mpimg.imread(os.path.join(cwd, 'Positive', random_images[2])))
axarr[1,1].imshow(mpimg.imread(os.path.join(cwd, 'Positive', random_images[3])))
```

Visualize Random images with no cracks

```python
random_indices = np.random.randint(0, len(no_crack_images), size=4)
print("*****************Random Images without Cracks**************************")
random_images = np.array(no_crack_images)[random_indices.astype(int)]

f, axarr = plt.subplots(2,2)
axarr[0,0].imshow(mpimg.imread(os.path.join(cwd, 'Negative', random_images[0])))
axarr[0,1].imshow(mpimg.imread(os.path.join(cwd, 'Negative', random_images[1])))
axarr[1,0].imshow(mpimg.imread(os.path.join(cwd, 'Negative', random_images[2])))
axarr[1,1].imshow(mpimg.imread(os.path.join(cwd, 'Negative', random_images[3])))
```

## **Create Train and Val Data sets**

### Create training folder

```python
!rm -rf train
base_dir = cwd
files = os.listdir(base_dir)
```

```python
##Considering only 1000 images from each class in order to restrict Compute resource utilization
def create_training_data(folder_name):
    files_list = os.listdir(os.path.join(base_dir,folder_name))
    files_list = files_list[0:1000]
    train_dir = f"{base_dir}/train/{folder_name}"
    os.makedirs(train_dir)
    for f in files:
          search_object = re.search(folder_name, f)
          if search_object:
            for file_name in files_list:
              shutil.copy(f'{base_dir}/{folder_name}/{file_name}', train_dir)
```

```python
create_training_data('Positive')
create_training_data('Negative')
!rm -rf val
```

### Move images randomly from training to val folders

```python
os.makedirs('val/Positive')
os.makedirs('val/Negative')
```

```python
positive_train = base_dir + "/train/Positive/"
positive_val = base_dir + "/val/Positive/"
negative_train = base_dir + "/train/Negative/"
negative_val = base_dir + "/val/Negative/"

positive_files = os.listdir(positive_train)
negative_files = os.listdir(negative_train)
```

```python
print(len(positive_files), len(negative_files))             # Checking for the total count of file to be unchanged. Expected output: 20000 20000
```

    1000 1000

```python
for f in positive_files:
    if random.random() > 0.80:
        shutil.move(f'{positive_train}/{f}', positive_val)   # moving files to positivr_val
```

```python
for f in negative_files:
    if random.random() > 0.80:
        shutil.move(f'{negative_train}/{f}', negative_val)   # moving files to negative_val
```

## **Step 2: Preprocessing the Image Dataset**
## Pytorch data loader and Transforms

### Compute mean and std deviation for the dataset

There are 3 channels in a RGB IMAGE.
> 1. Red
2. Green
3. Blue

```python
mean_nums = [0.5, 0.5, 0.5]
std_nums = [0.5, 0.5, 0.5]
```

### Define Data Augmentation and Transforms

```python
import torchvision.transforms
```

```python
data_path_train = os.path.join(cwd, 'train')
data_path_test = os.path.join(cwd, 'val')
chosen_transforms = {'train': transforms.Compose([
        transforms.RandomResizedCrop(size=8),
        transforms.RandomRotation(degrees=10),
        transforms.RandomHorizontalFlip(),
        transforms.RandomVerticalFlip(),
        transforms.ColorJitter(brightness=0.15, contrast=0.15),
        transforms.ToTensor(),
        transforms.Normalize(mean_nums, std_nums)
]), 'val': transforms.Compose([
        transforms.Resize(8),
        transforms.CenterCrop(8),
        transforms.ToTensor(),
        transforms.Normalize(mean_nums, std_nums)
]),
}
```

### Create the Data Loader

```python
def load_dataset(format, batch_size):
    data_path = os.path.join(cwd, format)
    dataset = datasets.ImageFolder(
        root=data_path,
        transform= chosen_transforms[format]
    )
    data_loader = DataLoader(
        dataset,
        batch_size=batch_size,
        num_workers=2,
        shuffle=True
    )
    return data_loader, len(dataset), dataset.classes
```

```python
train_loader, train_size, class_names = load_dataset('train', 8)
print("Train Data Set size is: ", train_size)
print("Class Names are: ", class_names)
inputs, classes = next(iter(train_loader))
print(inputs.shape, classes.shape)
```

    Train Data Set size is:  1608
    Class Names are:  ['Negative', 'Positive']
    torch.Size([8, 3, 8, 8]) torch.Size([8])

```python
test_loader, test_size, class_names = load_dataset('val', 8)
print("Test Data Set size is: ", test_size)
print("Class Names are: ", class_names)
inputs_test, classes_test = next(iter(test_loader))
print(inputs_test.shape, classes_test.shape)
```

    Test Data Set size is:  392
    Class Names are:  ['Negative', 'Positive']
    torch.Size([8, 3, 8, 8]) torch.Size([8])

### Visualize the Augmented Data Set

```python
# Visualize some images
def imshow(inp, title=None):
    inp = inp.numpy().transpose((1, 2, 0))
    mean = np.array([mean_nums])
    std = np.array([std_nums])
    inp = std * inp + mean
    inp = np.clip(inp, 0, 1)
    plt.imshow(inp)
    if title is not None:
        plt.title(title)
    plt.pause(0.001)  # Pause a bit so that plots are updated
```

```python
# Grab some of the training data to visualize
inputs, classes = next(iter(train_loader))         # class_names = chosen_datasets['train'].classes

out = torchvision.utils.make_grid(inputs)          # Now we construct a grid from batch

idx_to_class = {0:'Negative', 1:'Positive'}
plt.figure(figsize=(20,10))
imshow(out, title=[x.data.numpy() for x in classes])
```

## **Step 3: Define the architecture to train the model**

##Define Quantum Circuit and Layer

```python
class QuantumCircuit:
    """
    This class provides a simple interface for interaction
    with the quantum circuit
    """

    def __init__(self, n_qubits, backend, shots):
        # --- Circuit definition ---
        self._circuit = qiskit.QuantumCircuit(n_qubits)

        all_qubits = [i for i in range(n_qubits)]
        self.theta = qiskit.circuit.Parameter('theta')

        self._circuit.h(all_qubits)
        self._circuit.barrier()
        self._circuit.ry(self.theta, all_qubits)

        self._circuit.measure_all()
        # ---------------------------

        self.backend = backend
        self.shots = shots

    def run(self, thetas):
        t_qc = transpile(self._circuit,
                         self.backend)
        qobj = assemble(t_qc,
                        shots=self.shots,
                        parameter_binds = [{self.theta: theta} for theta in thetas])
        job = self.backend.run(qobj)
        result_circ = job.result().get_counts()
        y = list(map(int, result_circ.values()))
        # counts = np.array(list(result_circ.values()))
        counts= np.array(y)
        states = np.array(list(result_circ.keys())).astype(float)

        # Compute probabilities for each state
        probabilities = counts / self.shots
        # Get state expectation
        expectation = np.sum(states * probabilities)

        return np.array([expectation])
```

Let's test the implementation

```python
circuit = QuantumCircuit(1, simulator, 100)
print('Expected value for rotation pi {}'.format(circuit.run([np.pi])[0]))
circuit._circuit.draw()
```

    /usr/local/lib/python3.7/dist-packages/qiskit/utils/deprecation.py:62: DeprecationWarning: Using a qobj for run() is deprecated as of qiskit-aer 0.9.0 and will be removed no sooner than 3 months from that release date. Transpiled circuits should now be passed directly using `backend.run(circuits, **run_options).
      return func(*args, **kwargs)

    Expected value for rotation pi 0.61

        ┌───┐ ░ ┌───────────┐ ░ ┌─┐
   q_0: ┤ H ├─░─┤ Ry(theta) ├─░─┤M├
        └───┘ ░ └───────────┘ ░ └╥┘
meas: 1/═════════════════════════╩═
                                 0

### Creating a "Quantum-Classical Class" with PyTorch

```python
class HybridFunction(Function):
    """ Hybrid quantum - classical function definition """

    @staticmethod
    def forward(ctx, inputs, quantum_circuit, shift):
        """ Forward pass computation """
        ctx.shift = shift
        ctx.quantum_circuit = quantum_circuit

        expectation_z = []
        for input in inputs:
            expectation_z.append(ctx.quantum_circuit.run(input.tolist()))
        result = torch.tensor(expectation_z).to(device)

        ctx.save_for_backward(inputs, result)
        return result

    @staticmethod
    def backward(ctx, grad_output):
        """ Backward pass computation """
        input, expectation_z = ctx.saved_tensors
        input_list = np.array(input.tolist())

        shift_right = input_list + np.ones(input_list.shape) * ctx.shift
        shift_left = input_list - np.ones(input_list.shape) * ctx.shift

        gradients = []
        for i in range(len(input_list)):
            expectation_right = ctx.quantum_circuit.run(shift_right[i])
            expectation_left  = ctx.quantum_circuit.run(shift_left[i])

            gradient = torch.tensor([expectation_right]).to(device) - torch.tensor([expectation_left]).to(device)
            gradients.append(gradient)

        # gradients = np.array([gradients]).T
        gradients = torch.tensor([gradients]).to(device)
        gradients = torch.transpose(gradients, 0, 1)

        # return torch.tensor([gradients]).float() * grad_output.float(), None, None
        return gradients.float() * grad_output.float(), None, None

class Hybrid(nn.Module):
    """ Hybrid quantum - classical layer definition """

    def __init__(self, backend, shots, shift):
        super(Hybrid, self).__init__()
        self.quantum_circuit = QuantumCircuit(1, backend, shots)
        self.shift = shift

    def forward(self, input):
        return HybridFunction.apply(input, self.quantum_circuit, self.shift)
```

#**Step 3: Calculating the Kernel Matrix so can be used as weights for a custom convolutional layer**

```python
##Creating Quantum Kernel Circuit  Instance
seed = 12345
qka_feature_map = ZZFeatureMap(feature_dimension=8,
                                 reps=2, entanglement='linear') ###Defining Quantum Feature Map for Encoding

qka_backend = QuantumInstance(simulator, shots=1024,
                                seed_simulator=seed, seed_transpiler=seed) # Quantum Backend for Executing the QKA Circuit

qka_kernel = QuantumKernel(feature_map=qka_feature_map, quantum_instance=qka_backend) #Defining the Quantum Kenel Alignment Instance
```

```python
#Loading Sample Dataset for QKA evaluation
train_loader, train_size, class_names = load_dataset('train', 16)
print("Train Data Set size is: ", train_size)
print("Class Names are: ", class_names)
inputs, classes = next(iter(train_loader))
print(inputs.shape, classes.shape)
```

    Train Data Set size is:  1608
    Class Names are:  ['Negative', 'Positive']
    torch.Size([16, 3, 8, 8]) torch.Size([16])

```python
##Input for Kernel Matrix Evaluation
qka_inputs =torch.reshape(inputs,(48,64))

```

```python
##Evaluating Kernel Matrix
qka_matrix_train = qka_kernel.evaluate(x_vec=qka_inputs)
```

```python
weights = torch.tensor(qka_matrix_train)
print (weights.shape)
weights = torch.reshape(weights,(16,16,3,3))
print(weights.shape)
```

    torch.Size([48, 48])
    torch.Size([16, 16, 3, 3])

#**Step 4 : Defining the model architecture and adding a custom layer for training our model.**

```python
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(3, 16, kernel_size=3)
        self.conv3 = nn.Conv2d(16, 16, 3,1)
        print(self.conv3.weight.size())
        weights = torch.tensor(qka_matrix_train)
        weights = torch.reshape(weights,(16,16,3,3))
        print(weights.size())
        self.conv3.weight.data = self.conv3.weight.data + weights.float()
        self.dropout = nn.Dropout2d(p=0.5)
        self.fc3 = nn.Linear(16, 2)
        self.hybrid = [Hybrid(simulator, 100, np.pi / 2) for i in range(2)]

    def forward(self, x):
        x = F.relu(self.conv1(x))
        x = F.max_pool2d(x, 2)
        x = self.conv3(x.float())
        x = self.dropout(x)
        x = torch.flatten(x, start_dim=1)
        x = self.fc3(x)
        x = torch.chunk(x, 10, dim=1)
        x = tuple([hy(x_) for hy, x_ in zip(self.hybrid, x)])
        return torch.cat(x, -1)
```

```python
# Convert model to be used on GPU
model = Net().to(device)
from torchsummary import summary
print(summary(model, (3, 8, 8)))
```

    torch.Size([16, 16, 3, 3])
    torch.Size([16, 16, 3, 3])
    ----------------------------------------------------------------
            Layer (type)               Output Shape         Param #
    ================================================================
                Conv2d-1             [-1, 16, 6, 6]             448
                Conv2d-2             [-1, 16, 1, 1]           2,320
             Dropout2d-3             [-1, 16, 1, 1]               0
                Linear-4                    [-1, 2]              34
    ================================================================
    Total params: 2,802
    Trainable params: 2,802
    Non-trainable params: 0
    ----------------------------------------------------------------
    Input size (MB): 0.00
    Forward/backward pass size (MB): 0.00
    Params size (MB): 0.01
    Estimated Total Size (MB): 0.02
    ----------------------------------------------------------------
    None

    /usr/local/lib/python3.7/dist-packages/torch/nn/functional.py:718: UserWarning: Named tensors and all their associated APIs are an experimental feature and subject to change. Please do not use them for anything important until they are released as stable. (Triggered internally at  /pytorch/c10/core/TensorImpl.h:1156.)
      return torch.max_pool2d(input, kernel_size, stride, padding, dilation, ceil_mode)
    /usr/local/lib/python3.7/dist-packages/qiskit/utils/deprecation.py:62: DeprecationWarning: Using a qobj for run() is deprecated as of qiskit-aer 0.9.0 and will be removed no sooner than 3 months from that release date. Transpiled circuits should now be passed directly using `backend.run(circuits, **run_options).
      return func(*args, **kwargs)

# **Step 5: Training our Model**

```python
dataloaders = {}
dataset_sizes = {}
batch_size = 4
dataloaders['train'], dataset_sizes['train'], class_names = load_dataset('train', batch_size)
dataloaders['val'], dataset_sizes['val'], _ = load_dataset('val', batch_size)
idx_to_class = {0:'Negative', 1:'Positive'}

```

```python
def train_model(model, criterion, optimizer, scheduler, num_epochs=5):
    since = time.time()

    best_model_wts = copy.deepcopy(model.state_dict())
    best_acc = 0.0
    loss_list = []
    train_acc = []
    val_acc =[]
    for epoch in range(num_epochs):
        print('Epoch {}/{}'.format(epoch, num_epochs - 1))
        print('-' * 10)

        # Each epoch has a training and validation phase
        for phase in ['train', 'val']:
            if phase == 'train':
                scheduler.step()
                acc_hist = train_acc
                model.train()  # Set model to training mode
            else:
                model.eval()   # Set model to evaluate mode
                acc_hist = val_acc
            current_loss = 0.0
            current_corrects = 0

            # Here's where the training happens
            print('Iterating through data...')
            # training starts...
            total_loss = []
            for inputs, labels in dataloaders[phase]:
                inputs = inputs.to(device)
                labels = labels.to(device)

                # We need to zero the gradients, don't forget it
                # Re-setting all of model parameters

                optimizer.zero_grad()

                # Time to carry out the forward training poss
                # We only need to log the loss stats if we are in training phase
                with torch.set_grad_enabled(phase == 'train'):
                    outputs = model(inputs).to(device)
                    # this computes the class that the model belongs to...
                    _, preds = torch.max(outputs, 1)

                    # compute the loss here...
                    # we compute the difference between true label and the predicted output...
                    loss = criterion(outputs, labels)

                    # backward + optimize only if in training phase
                    if phase == 'train':
                        loss.backward() # this is the back-propragation step. we

                        # minimize our model loss and optimize model in this phase
                        optimizer.step()
                        # optimizer updates the model paramters...

                # We want variables to hold the loss statistics
                # combine our model losses and compute the wrongly classified examples
                current_loss += loss.item() * inputs.size(0)
                current_corrects += torch.sum(preds == labels.data)
                total_loss.append(loss.item())

            # compute the loss and accuracy for each of our epoch
            epoch_loss = current_loss / dataset_sizes[phase]
            epoch_acc = current_corrects.double() / dataset_sizes[phase]
            acc_hist.append(epoch_acc)
            loss_list.append(sum(total_loss)/len(total_loss))
            print('{} Loss: {:.4f} Acc: {:.4f}'.format(
                phase, epoch_loss, epoch_acc))

            # Make a copy of the model if the accuracy on the validation set has improved
            if phase == 'val' and epoch_acc > best_acc:
                best_acc = epoch_acc
                best_model_wts = copy.deepcopy(model.state_dict())

        print()

    time_since = time.time() - since
    print('Training complete in {:.0f}m {:.0f}s'.format(
        time_since // 60, time_since % 60))
    print('Best val Acc: {:4f}'.format(best_acc))

    #Plotting the training graph
    plt.figure(figsize = (15,10))
    plt.plot(loss_list)
    plt.title('Convolutional NN Training Convergence')
    plt.xlabel('Training Iterations')
    plt.ylabel('Neg Log Likelihood Loss')
    plt.show()

    #Plotting the learning curve - Accuracy
    plt.figure(figsize = (15,10))
    plt.title('Training Vs Validation Accuracy')
    plt.plot(train_acc, label='train_accuracy')
    plt.plot(val_acc, label='val_accuracy')
    plt.legend(loc='best')
    plt.show()
    # Now we'll load in the best model weights and return it
    model.load_state_dict(best_model_wts)
    return model

```

```python
def visualize_model(model, num_images=6):
    was_training = model.training
    model.eval()
    images_handeled = 0
    fig = plt.figure()

    with torch.no_grad():
        for i, (inputs, labels) in enumerate(dataloaders['val']):
            inputs = inputs.to(device)
            labels = labels.to(device)

            outputs = model(inputs).to(device)
            _, preds = torch.max(outputs, 1)

            for j in range(inputs.size()[0]):
                images_handeled += 1
                ax = plt.subplot(num_images//2, 2, images_handeled)
                ax.axis('off')
                ax.set_title('predicted: {}'.format(class_names[preds[j]]))
                imshow(inputs.cpu().data[j])

                if images_handeled == num_images:
                    model.train(mode=was_training)
                    return
        model.train(mode=was_training)
```

###**Define Optimizer and Loss Function**

Learning rate decay- As we train the model, we can decide to change the learning rate dynamically. The schedular helps us in doing that.

In the 1st epoch, the learning rate was 0.1, then the learning rate in epoch 4 would be 0.01.

```python
criterion = nn.CrossEntropyLoss().to(device)
optimizer = optim.Adam(model.parameters())
exp_lr_scheduler = lr_scheduler.StepLR(optimizer, step_size=3, gamma=0.1)
```

### **Start training**

```python
base_model = train_model(model, criterion, optimizer, exp_lr_scheduler, num_epochs=20)
visualize_model(base_model)
plt.show()
# # an epoch is the total time during which one training step completes. In other words, it is the total
# # time that our model takes to process the input data once...
```

# **Step 6: Using trained model to make inference of our test dataset**

```python
def predict(model, test_image, print_class = False):
     # it uses the model to predict on test_image...
    transform = chosen_transforms['val']

    test_image_tensor = transform(test_image)
    if torch.cuda.is_available(): # checks if we have a gpu available
        test_image_tensor = test_image_tensor.view(1, 3, 8, 8).to(device)
    else:
        test_image_tensor = test_image_tensor.view(1, 3, 8, 8)

    with torch.no_grad():
        model.eval()
        # Model outputs log probabilities
        # this computes the output of the model
        out = model(test_image_tensor).to(device)
        # this computes the probability of each classes.
        ps = torch.exp(out)
        # we choose the top class. That is, the class with highest probability
        topk, topclass = ps.topk(1, dim=1)
        class_name = idx_to_class[topclass.cpu().numpy()[0][0]]
        if print_class:
            print("Output class :  ", class_name)
    return class_name
```

```python
def predict_on_crops(input_image, height=32, width=32, save_crops = False):
    im = cv2.imread(input_image)
    imgheight, imgwidth, channels = im.shape
    k=0
    output_image = np.zeros_like(im)
    for i in range(0,imgheight,height):
        for j in range(0,imgwidth,width):
            a = im[i:i+height, j:j+width]
            ## discard image cropss that are not full size
            predicted_class = predict(base_model,Image.fromarray(a))
            ## save image
            file, ext = os.path.splitext(input_image)
            image_name = file.split('/')[-1]
            folder_name = 'out_' + image_name
            ## Put predicted class on the image
            if predicted_class == 'Positive':
                color = (0,0, 255)
            else:
                color = (0, 255, 0)
            cv2.putText(a, predicted_class, (50,50), cv2.FONT_HERSHEY_SIMPLEX , 0.7, color, 1, cv2.LINE_AA)
            b = np.zeros_like(a, dtype=np.uint8)
            b[:] = color
            add_img = cv2.addWeighted(a, 0.9, b, 0.1, 0)
            ## Save crops
            if save_crops:
                if not os.path.exists(os.path.join('real_images', folder_name)):
                    os.makedirs(os.path.join('real_images', folder_name))
                filename = os.path.join('real_images', folder_name,'img_{}.png'.format(k))
                cv2.imwrite(filename, add_img)
            output_image[i:i+height, j:j+width,:] = add_img
            k+=1
    ## Save output image
    cv2.imwrite(os.path.join('real_images','predictions', folder_name+ '.jpg'), output_image)
    return output_image
```

```python
# Specify a path
PATH = "qka_qnn_state_dict_model.pt"
PATH1 = "qka_qnn_model.pt"
# Save
torch.save(base_model.state_dict(), PATH)
torch.save(model, PATH1)

```

# **Step 7:  Visualize the results of our data**

```python
plt.figure()
output_image = predict_on_crops('/content/val/Negative/06878.jpg')
plt.imshow(cv2.cvtColor(output_image, cv2.COLOR_BGR2RGB))
```

```python
plt.figure()
output_image = predict_on_crops('/content/train/Positive/02125.jpg')
plt.imshow(cv2.cvtColor(output_image, cv2.COLOR_BGR2RGB))
```

```python
plt.figure()
output_image = predict_on_crops('/content/val/Negative/03175.jpg')
plt.imshow(cv2.cvtColor(output_image, cv2.COLOR_BGR2RGB))
```

```python

```


# 14. Построение решения с использованием суперпозиции для переменного дебита
Материалы курсов "Исследования скважин и пластов" и "Гидродинамические исследования скважин" в РГУ нефти и газа имени И.М.Губкина.

Версия 0.4 от 24.11.2021

Хабибуллин Ринат 2021 г.

---
[Открыть на google colab](https://colab.research.google.com/github/khabibullinra/welltest_examples/blob/master/jupyter/14_superposition_variable_rate.ipynb)

# Содержание

* [14.1 Расчет кривой восстановления давления ](#14.1)
* [14.2 Случай для произвольной истории дебитов (ступенчатое изменение дебита)](#14.2)
* [14.3 Случай для произвольной истории дебитов (линейное изменение дебита)](#14.3)

```python
# для того, чтобы скрипты гладко работали на https://colab.research.google.com/ пропишем
# установку библиотеки welltest
# библиотека welltest загружает определения функций для безразмерных переменных
# а также грузит anaflow которая обеспечивает работу с обратным преобразованием Лапласа

!pip install welltest
```

    WARNING: You are using pip version 21.3; however, version 21.3.1 is available.
    You should consider upgrading via the 'C:\venv\base\Scripts\python.exe -m pip install --upgrade pip' command.

    Requirement already satisfied: welltest in c:\venv\base\lib\site-packages (0.1)
    Requirement already satisfied: numpy in c:\venv\base\lib\site-packages (from welltest) (1.20.1)
    Requirement already satisfied: anaflow in c:\venv\base\lib\site-packages (from welltest) (1.0.1)
    Requirement already satisfied: pentapy>=1.1.0 in c:\venv\base\lib\site-packages (from anaflow->welltest) (1.1.2)
    Requirement already satisfied: scipy>=1.1.0 in c:\venv\base\lib\site-packages (from anaflow->welltest) (1.6.2)

```python
# импортируем библиотки, которые могут пригодиться для проведения расчетов
import numpy as np
import matplotlib.pyplot as plt
from anaflow import get_lap_inv
from scipy.special import kn, iv, expi

from welltest.functions import *
```

```python
# Решение линейного стока уравнения фильтрации
def pd_ei(td, rd=1):
    """
    Решение линейного стока уравнения фильтрации
    rd - безразмерное расстояние
    td - безразмерное время
    """
    # при расчете убедимся, что td=0 не повлияет на расчет, даже если td массив и нулевой только один элемент
    td = np.array(td, dtype = float)
    return np.multiply(-0.5,
                       expi(np.divide(-rd**2 / 4 ,
                                      td,
                                      out=np.zeros_like(td), where=td!=0)),
                       out=np.zeros_like(td), where=td!=0)
```

# 14.1 Расчет кривой восстановления давления

Один из самых простых примеров применения суперпозиции. Предполагаем, что добывающая скважина в однородном изотропном пласте запускается в момент времени `t=0` и работает `t_p_hr` часов, после чего останавливается. После остановки скважины забойное давление растет - и мы получим кривую восстановления давления.

Пусть решение задачи запуска скважины (падения давления) будет $P_D(t_D, r_D)$. Тогда решение для изменения давления при запуске и последующей остановки скважины можно представить в виде
$$P_{bu.D}(t_D, t_{prod.D}, r_D) = P_D(t_D) - P_D(t_D-t_{prod.D}, r_D) \cdot \mathcal{H}(t_D-t_{prod.D})  \tag{14.1}$$

где
* $t_D$ - безразмерное время после запуска скважины,
* $t_{prod.D}$ - безразмерное время работы скважины после запуска
* $\mathcal{H}$ - ступенчатая [функция Хевисайда](https://ru.wikipedia.org/wiki/%D0%A4%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D1%8F_%D0%A5%D0%B5%D0%B2%D0%B8%D1%81%D0%B0%D0%B9%D0%B4%D0%B0) (в некоторых книгах обозначается как $\theta$)
* $P_D(t_D, r_D)$ - безразмерное давление - решение задачи запуска скважины (падения давления)
* $P_{bu.D}(t_D, t_{prod.D}, r_D)$ - безразмерное давление- решение задачи запуска скважины и последующей остановки скважины

Для проведения векторных расчетов в python удобно выражение с использованием функции Хевисайда

$$ \mathcal{H} = \begin{cases}0 & x < 0\\1 & x = 0\\1 & x > 0\end{cases}$$

Применение функции Хевисайда позволяет избежать в расчетных функциях применение условных операторов в явном виде для отдельных элементов входных массивов. Это потенциально ускоряет расчет.

```python
x = np.arange(-2,2,0.01)
y = np.heaviside(x, 1)

plt.rcParams["figure.figsize"] = (8,3)
plt.plot(x,y)
plt.title('Функция Хевисайда $\mathcal{H}$ ')
plt.show()
```

```python
def pd_build_up(td, td_p):
    """
    расчет давления для запуска и последующей остановки скважины
    td - время после запуска
    td_p - время безразмерное - которое скважина работала до остановки
    """

    # применение функции Хевисайда здесь делает расчет корректным
    # для входных векторов td
    return pd_ei(td) - np.heaviside(td-td_p,1) * pd_ei(td-td_p)

```

```python
t_arr = np.logspace(-10, 2, 1000)
t_prod_hr = 24
k = 10   # проницаемость
q = 30   # дебит

# переведем размерный массив времени в безразмерные величины
td_arr = td_from_t(t_arr, k_mD=k)
td_prod = td_from_t(t_prod_hr, k_mD=k)
print('время работы скважины {:.2f} часа, что соответсвует безразмерному времени {:.2f}'.format(t_prod_hr, td_prod))
```

    время работы скважины 24.00 часа, что соответсвует безразмерному времени 4320000.00

```python
# для заданного массива безразмерных времен рассчитаем безразмерные давления
pd_arr = pd_build_up(td_arr, td_prod)
```

```python
plt.rcParams["figure.figsize"] = (8,3)
plt.plot(td_arr, pd_arr)
plt.xlabel('td')
plt.ylabel('pd')
plt.title('Решение в безразмерных координатах')
plt.show()
```

```python
# переведем безразмерные координаты в размерные
p_arr = p_from_pd_atma(pd_arr, k_mD=k, q_sm3day=q)
```

```python
plt.rcParams["figure.figsize"] = (8,3)
plt.plot(t_arr, p_arr)
plt.xlabel('t, hr')
plt.ylabel('p, atma')
plt.title('Решение в размерных координатах')
plt.show()
```

Нарисуем распределение давления в пласте при запуске и остановке скважины.
Начнем с запуска скважины - нарисуем график изменения давления в пласте для разных моментов времени

```python
r_arr = np.logspace(1, 3, 100)
t_arr = np.logspace(-1, 2, 10)
t_arr[0] = 0
t_prod = 100

tv, rv = np.meshgrid(td_from_t(t_arr),rd_from_r(r_arr))
pd_arr =pd_ei(tv, rd=rv)

fig, (ax1, ax2) = plt.subplots(1, 2, figsize = [12,5])
fig.suptitle('Изменение давления в пласте при запуске скважины')
ax1.plot(r_arr, p_from_pd_atma(pd_arr))
ax1.set_label(t_arr)
ax1.set_xlabel('r, m')
ax1.set_ylabel('$p_{wf}, atma$')

ax2.plot(r_arr,p_from_pd_atma(pd_arr))
ax2.set_xscale('log')
ax2.set_xlabel('r, m')
ax2.set_ylabel('$p_{wf}, atma$')
plt.show()
```

Дальше нарисуем графики изменения давления в пласте для различных моментов времени после остановки скважины

```python
pd_arr_bu =pd_ei(tv + td_from_t(t_prod), rd=rv) - pd_ei(tv, rd=rv)

fig, (ax1, ax2) = plt.subplots(1, 2, figsize = [12,5])
fig.suptitle('Изменение давления в пласте при запуске скважины')
ax1.plot(r_arr, p_from_pd_atma(pd_arr_bu))
ax1.set_label(t_arr)
ax1.set_xlabel('r, m')
ax1.set_ylabel('$p_{wf}, atma$')

ax2.plot(r_arr,p_from_pd_atma(pd_arr_bu))
ax2.set_xscale('log')
ax2.set_xlabel('r, m')
ax2.set_ylabel('$p_{wf}, atma$')
plt.show()
```

Ну и нарисуем все вместе, чтобы посмотреть что получится

```python
fig, (ax1, ax2) = plt.subplots(1, 2, figsize = [12,5])
fig.suptitle('Изменение давления в пласте при запуске скважины')
ax1.plot(r_arr,p_from_pd_atma(pd_arr))
ax1.plot(r_arr,p_from_pd_atma(pd_arr_bu))
ax1.set_label(t_arr)
ax1.set_xlabel('r, m')
ax1.set_ylabel('$p_{wf}, atma$')
ax2.plot(r_arr,p_from_pd_atma(pd_arr))
ax2.plot(r_arr,p_from_pd_atma(pd_arr_bu))

ax2.set_xscale('log')
ax2.set_xlabel('r, m')
ax2.set_ylabel('$p_{wf}, atma$')
plt.show()
```

# 14.2 Случай для произвольной истории дебитов (ступенчатое изменение дебита)

```python
# создадим историю изменения дебитов

t_history = np.array([ 0., 2., 24. ], dtype=np.float64)
q_history = np.array([10., 5., 0.], dtype=np.float64)

# массивы должны быть одной и той же длины
```

Для расчета изменения давления при переменном дебите введем произвольное референсное значение дебита q_{ref} (например первое не нулевое значение дебита при запуске скважины). Используем это значение для определения безразмерного давления.
$$ p_D = \frac{kh}{ 18.41 q_{ref} B \mu} \left( p_i - p \right) $$

и безразмерного дебита

$$q_D = \frac{q}{q_{ref}}$$

Тогда, используя принцип суперпозиции, можем выписать выражение для изменения давления на скважине и вокруг нее для произвольного момента времени

$$P_{mr.D}(t_D, r_D) = \sum_i \left[ q_{D(i)}-q_{D(i-1)} \right] \cdot p_D\left(t_D-t_{D(i)}, r_D\right)\cdot \mathcal{H}(t_D-t_{D(i)}) \tag{14.2} $$

где

* $i$ - индекс значения дебита в таблице изменения дебитов
* $q_{D(i)}$ - безразмерный дебит с номером $i$, который стартует в момент времени $t_i$. Для первого момента времени $i$ дебит следующий перед ним считается равным нулю
* $t_{D(i)}$ - безразмерный момент времени - включения дебита с номером $i$
* $t_{D}$ - безразмерный момент времени для которого проводится расчет
* $\mathcal{H}$ - ступенчатая [функция Хевисайда](https://ru.wikipedia.org/wiki/%D0%A4%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D1%8F_%D0%A5%D0%B5%D0%B2%D0%B8%D1%81%D0%B0%D0%B9%D0%B4%D0%B0#:~:text=%D0%A4%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D1%8F%20%D0%A5%D0%B5%D0%B2%D0%B8%D1%81%D0%B0%D0%B9%D0%B4%D0%B0%20%D1%88%D0%B8%D1%80%D0%BE%D0%BA%D0%BE%20%D0%B8%D1%81%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D1%83%D0%B5%D1%82%D1%81%D1%8F%20%D0%B2,%D0%B4%D0%BB%D1%8F%20%D0%B7%D0%B0%D0%BF%D0%B8%D1%81%D0%B8%20%D1%8D%D0%BC%D0%BF%D0%B8%D1%80%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B9%20%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B8%20%D1%80%D0%B0%D1%81%D0%BF%D1%80%D0%B5%D0%B4%D0%B5%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F.)
* $p_D\left(t\right)$ - зависимость безразмерного давление от времени - решение задачи запуска скважины с постоянным единичным дебитом
* $P_{mr.D} $ - безразмерное давление $P_{mr.D}(t_D, r_D)$ учитывающее историю изменения дебитов скважины

```python
def pd_superposition(td, td_hist, qd_hist):
    """
    расчет безразмерного давления для последовательности безразмерных дебитов
    td -  время расчета после запуска, безразмерное
    td_hist - массив времен изменения режимов работы скважин, безразмерное
    qd_hist - массив дебитов установленных после изменения режима работы, безразмерное
    """
    # принудительно добавим нули во входные массивы, чтобы учесть запуск скважины
    qdh = np.hstack([0, qd_hist])
    tdh = np.hstack([0, td_hist])
    # построим дебиты виртуальных скважин - разности реальных дебитов при переключении
    delta_qd = np.hstack([0, np.diff(qdh)])
    # референсный безразмерный дебит это 1

    # векторная магия - время может быть вектором и переключения дебитов тоже вектор
    # надо организовать сумму по временам, каждая из котороых сумма по переключениям
    # делаем при помощи расчета meshgrid и поиска накопленных сумм
    qd_v, td_v =np.meshgrid(delta_qd, td)
    # используем куммулятивную сумму numpy для того что суммировать результаты
    dpd = np.cumsum(qd_v * pd_ei((td_v - tdh)) * np.heaviside((td_v - tdh), 1),1 )
    # последний столбец - полная сумма, которая нужна в качестве результата
    return dpd[:,-1]

def q_superposition(t, t_hist, q_hist):
    """
    расчет давления для запуска и последующей остановки скважины
    t_hr - время после запуска в часах
    t_hist_hr - массив времен изменения режимов работы скважин
    q_hist_sm3day - массив дебитов установленных после изменения режима работы
    """
    def interpolate_constant(x, xp, yp):
        indices = np.searchsorted(xp, x, side='right')
        y = np.concatenate(([0], yp))
        return y[indices]

    q=[]
    for ti in t:
        q.append(interpolate_constant(ti, t_hist, q_hist))
    return q
```

```python
td_arr = np.linspace(1e-3, 9e4, 1000)

td_history = np.array([ 0., 2e4, 5e4 ], dtype=np.float64)
qd_history = np.array([10., 5., 0.], dtype=np.float64)

plt.rcParams["figure.figsize"] = (12,5)
fig, (ax1, ax2) = plt.subplots(2,1)

ax1.plot(td_arr, q_superposition(td_arr, td_history, qd_history))
ax2.plot(td_arr, pd_superposition(td_arr, td_history, qd_history), color='red')
ax2.set_xlabel('td')
ax1.set_ylabel('qd')
ax2.set_ylabel('pd')
plt.show()
```

```python
def p_superposition_atma(t_hr, t_hist_hr, q_hist_sm3day):
    """
    расчет давления для запуска и последующей остановки скважины
    t_hr - время после запуска в часах
    t_hist_hr - массив времен изменения режимов работы скважин
    q_hist_sm3day - массив дебитов установленных после изменения режима работы
    """
    q_ref=10.
    return p_from_pd_atma(pd_superposition(td_from_t(t_hr),
                                           td_from_t(t_hist_hr),
                                           q_hist_sm3day / q_ref),
                          q_sm3day=q_ref)

```

```python
t_arr = np.arange(1e-3, 50, 1e-2)
plt.rcParams["figure.figsize"] = (12,5)

fig, (ax1, ax2) = plt.subplots(2,1)
ax1.plot(t_arr, q_superposition(t_arr, t_history, q_history))
ax2.plot(t_arr, p_superposition_atma(t_arr, t_history, q_history), color='red')
ax2.set_xlabel('t, hr')
ax1.set_ylabel('q, m3/day')
ax2.set_ylabel('p, atma')
plt.show()
```

```python

t_history = np.array([0, 2, 10, 24, 24.1,24.2,24.3,24.4,24.5 ])
q_history = np.array([10, 11, 12, 11, 10, 9, 8, 7, 6])

plt.rcParams["figure.figsize"] = (12,5)

fig, (ax1, ax2) = plt.subplots(2,1)
ax1.plot(t_arr, q_superposition(t_arr, t_history, q_history))
ax2.plot(t_arr, p_superposition_atma(t_arr, t_history, q_history), color='red')
ax2.set_xlabel('t, hr')
ax1.set_ylabel('q, m3/day')
ax2.set_ylabel('p, atma')
plt.show()
```

# 14.3 Случай для произвольной истории дебитов (линейное изменение дебита)

Для линейно меняющегося дебита во времени (как и для любой другой зависимости) надо решение проинтегрировать по времени (надо бы подробнее расписать - сделать это позже, например как у Щелкачева в основах нестационарной фильтрации на стр 321).

Для линейной зависимости дебита от времени
$$Q_D = dQ_D \cdot t_D $$
можно получить выражение
$$p_D(r_D,t_D, dQ_D) =-\frac{dQ_D t_D }{2} \left[ \left( 1+ \frac{r_D^2}{4 t_D} \right) Ei \left(- \dfrac{r_D^2}{4t_D} \right) + e^{-\dfrac{r_D^2}{4t_D}} \right] \tag{14.3}$$

где $dQ_D$ - скорость изменения дебита.

Для таблично заданных дебитов и времен можно оценить

$$dQ_{D(i)} = \dfrac{Q_{D(i)}-Q_{D(i-1)}}{t_{D(i)} - t_{D(i-1)} } \tag{14.4}$$

Cравните формулу (14.3) с формулой (9.68) в книге Щелкачева "Основы неустановившейся фильтрации"

Тогда, используя принцип суперпозиции, можем выписать выражение для изменения давления на скважине и вокруг нее для произвольного момента времени

$$P_{mr.D}(t_D, r_D) = \sum_i  p_D\left(t_D-t_{D(i)}, r_D, dQ_{D(i+1)} - dQ_{D(i)}\right)\cdot \mathcal{H}(t_D-t_{D(i)}) \tag{14.5} $$

где

* $i$ - индекс значения дебита в таблице изменения дебитов
* $dQ_{D(i)}$ - изменение безразмерного дебита относительно безразмерного времени (14.4)
* $t_{D(i)}$ - безразмерный момент времени - включения дебита с номером $i$
* $t_{D}$ - безразмерный момент времени для которого проводится расчет
* $\mathcal{H}$ - ступенчатая [функция Хевисайда](https://ru.wikipedia.org/wiki/%D0%A4%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D1%8F_%D0%A5%D0%B5%D0%B2%D0%B8%D1%81%D0%B0%D0%B9%D0%B4%D0%B0#:~:text=%D0%A4%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D1%8F%20%D0%A5%D0%B5%D0%B2%D0%B8%D1%81%D0%B0%D0%B9%D0%B4%D0%B0%20%D1%88%D0%B8%D1%80%D0%BE%D0%BA%D0%BE%20%D0%B8%D1%81%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D1%83%D0%B5%D1%82%D1%81%D1%8F%20%D0%B2,%D0%B4%D0%BB%D1%8F%20%D0%B7%D0%B0%D0%BF%D0%B8%D1%81%D0%B8%20%D1%8D%D0%BC%D0%BF%D0%B8%D1%80%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B9%20%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B8%20%D1%80%D0%B0%D1%81%D0%BF%D1%80%D0%B5%D0%B4%D0%B5%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F.)
* $p_D\left(t\right)$ - зависимость безразмерного давление от времени - решение задачи запуска скважины с постоянным единичным дебитом
* $P_{mr.D} $ - безразмерное давление $P_{mr.D}(t_D, r_D)$ учитывающее историю изменения дебитов скважины

следует обратить внимание, при суперпозиции скорость изменения дебита вычисляется как $dQ_{D(i+1)} - dQ_{D(i)}$.  при реализации расчета необходимо предусмотреть, чтобы для первого и последнего шага расчет прошел корректно. Для этого можно, например, добавить к массивам дебитов и времени дополнительный значения в начале и в конце массивов соответствующие постоянным значениям дебита.

Также надо учитывать, что в приведенном выражении массивы должны начинаться со значений $Q_D=0$

```python
import sympy as sp
```

```python
x = sp.symbols('x')
rd, td = sp.symbols('r_d t_d')
```

```python
# запишем решение в символьном виде
e = sp.expint(1, rd **2 / 4 / td)
e
```

$\displaystyle \operatorname{E}_{1}\left(\frac{r_{d}^{2}}{4 t_{d}}\right)$

```python
# проинтегрируем решение по времени
sp.integrate(e, td)
```

$\displaystyle \frac{r_{d}^{2} \operatorname{E}_{1}\left(\frac{r_{d}^{2}}{4 t_{d}}\right)}{4} + t_{d} \operatorname{E}_{1}\left(\frac{r_{d}^{2}}{4 t_{d}}\right) - t_{d} e^{- \frac{r_{d}^{2}}{4 t_{d}}}$

```python
# Решение линейного стока уравнения фильтрации
def pd_lin_ei(td, rd=1, dqd_dtd=1):
    """
    Решение линейного стока уравнения фильтрации
    rd - безразмерное расстояние
    td - безразмерное время
    """
    # при расчете убедимся, что td=0 не повлияет на расчет,
    # даже если td массив и нулевой только один элемент
    td = np.array(td, dtype = float)
    pd =  (1 + rd**2/4/td) * (-expi(-rd**2 / 4 /td)) - np.exp(-rd**2 / 4 /td)
    return dqd_dtd * td * pd / 2
```

```python
def pd_superposition_lin(td, td_hist, qd_hist):
    """
    расчет безразмерного давления для последовательности безразмерных дебитов
    td -  время расчета после запуска, безразмерное
    td_hist - массив времен изменения режимов работы скважин, безразмерное
    qd_hist - массив дебитов установленных после изменения режима работы, безразмерное
    """
    # принудительно добавим нули во входные массивы, чтобы учесть запуск скважины
    qdh = np.hstack([qd_hist])
    tdh = np.hstack([td_hist])
    # построим дебиты виртуальных скважин - разности реальных дебитов при переключении
    delta_qd = np.hstack([np.diff(qdh),0])
    delta_td = np.hstack([np.diff(tdh),1])

    dq_dt = delta_qd / delta_td
    dq_dt = np.diff(np.hstack([0, delta_qd / delta_td]))

    # референсный безразмерный дебит это 1

    # векторная магия - время может быть вектором и переключения дебитов тоже вектор
    # надо организовать сумму по временам, каждая из котороых сумма по переключениям
    # делаем при помощи расчета meshgrid и поиска накопленных сумм
    qd_v, td_v =np.meshgrid(delta_qd, td)

    dpd = np.cumsum(pd_lin_ei((td_v - tdh), dqd_dtd=dq_dt) * np.heaviside((td_v - tdh), 1),1 )

    return dpd[:,-1]

def p_superposition_lin_atma(t_hr, t_hist_hr, q_hist_sm3day):
    """
    расчет давления для запуска и последующей остановки скважины
    t_hr - время после запуска в часах
    t_hist_hr - массив времен изменения режимов работы скважин
    q_hist_sm3day - массив дебитов установленных после изменения режима работы
    """
    q_ref=10
    return p_from_pd_atma(pd_superposition_lin(td_from_t(t_hr),
                                           td_from_t(t_hist_hr),
                                           q_hist_sm3day / q_ref),
                          q_sm3day=q_ref)

```

построим графики для безразмерных времен

```python
td_arr = np.linspace(1e-3, 2000, 2000)

td_history = np.array([0., 1, 50, 100, 300, 500 ], dtype=np.float64)
qd_history = np.array([0., 3, 1.4, 1.0, 1.01, 1], dtype=np.float64)

plt.rcParams["figure.figsize"] = (12,8)
fig, (ax1, ax2) = plt.subplots(2,1)

ax1.plot(td_arr, q_superposition(td_arr, td_history, qd_history))
ax1.plot(td_arr, np.interp(td_arr, td_history, qd_history))
ax2.plot(td_arr, pd_superposition_lin(td_arr, td_history, qd_history), color='red')
ax2.plot(td_arr, pd_superposition(td_arr, td_history, qd_history), color='blue')
ax2.set_xlabel('td')
ax1.set_ylabel('qd')
ax2.set_ylabel('pd')
plt.show()
```

```python

t_history = np.array([0, .02, 10, 24, 24.1,24.2,24.3,24.4,24.5 ])
q_history = np.array([0, 11, 12, 12, 10, 9, 8, 7, 6])

plt.rcParams["figure.figsize"] = (12,5)

fig, (ax1, ax2) = plt.subplots(2,1)
ax1.plot(t_arr, q_superposition(t_arr, t_history, q_history))
ax1.plot(t_arr, np.interp(t_arr, t_history, q_history))
ax2.plot(t_arr, p_superposition_atma(t_arr, t_history, q_history), color='red')
ax2.plot(t_arr, p_superposition_lin_atma(t_arr, t_history, q_history), color='green')
ax2.set_xlabel('t, hr')
ax1.set_ylabel('q, m3/day')
ax2.set_ylabel('p, atma')
plt.show()
```

```python
t_history = np.array([0, .02, 10, 24, 24.1,24.2,24.3,24.4,24.5 ])
q_history = np.array([0, 11, 12, 12, 10, 9, 8, 7, 6])

plt.rcParams["figure.figsize"] = (12,5)

fig, (ax1, ax2) = plt.subplots(2,1)
ax1.plot(t_arr, q_superposition(t_arr, t_history, q_history))
ax1.plot(t_arr, np.interp(t_arr, t_history, q_history))
ax2.plot(t_arr, p_superposition_atma(t_arr, t_history, q_history), color='red')
ax2.plot(t_arr, p_superposition_lin_atma(t_arr, t_history, q_history), color='green')
ax2.set_xlabel('t, hr')
ax1.set_ylabel('q, m3/day')
ax2.set_ylabel('p, atma')
ax1.set_xlim([23.9,24.6])
ax2.set_xlim([23.9,24.6])
plt.show()
```

```python

```