```python
!pip install qiskit
```

    Collecting qiskit
      Downloading https://files.pythonhosted.org/packages/ab/05/b9f82e569f1d4c39cd856c2fd04c716999b0e7a7a395a7fd2b1c48d40e68/qiskit-0.25.0.tar.gz
    Collecting qiskit-terra==0.17.0
      Downloading https://files.pythonhosted.org/packages/26/50/39921a9aa428e3bb0ca1d646ad72e40d87e82699054b33344d9cf4e54a17/qiskit_terra-0.17.0-cp37-cp37m-manylinux2010_x86_64.whl (6.0MB)
         |████████████████████████████████| 6.0MB 13.3MB/s
    Collecting qiskit-aer==0.8.0
      Downloading https://files.pythonhosted.org/packages/ed/a0/93b1c1efb6e55185c9af446c2ed44b050a0675045eedae52d1f5c0247cf2/qiskit_aer-0.8.0-cp37-cp37m-manylinux2010_x86_64.whl (17.9MB)
         |████████████████████████████████| 17.9MB 230kB/s
    Collecting qiskit-ibmq-provider==0.12.2
      Downloading https://files.pythonhosted.org/packages/08/ac/69bb35892303c3a4a52eaaf7ea95431dd3f57963b580a011ee92693a7fcc/qiskit_ibmq_provider-0.12.2-py3-none-any.whl (198kB)
         |████████████████████████████████| 204kB 44.3MB/s
    Collecting qiskit-ignis==0.6.0
      Downloading https://files.pythonhosted.org/packages/54/be/a13c828e457e09d979667a61bddbd8c7246aafa94e2501b6a9154429cbea/qiskit_ignis-0.6.0-py3-none-any.whl (207kB)
         |████████████████████████████████| 215kB 42.4MB/s
    Collecting qiskit-aqua==0.9.0
      Downloading https://files.pythonhosted.org/packages/86/26/8d89e8ecb8d21f555496a0db80a5299dd663443a971e9fb4089efc7c5aef/qiskit_aqua-0.9.0-py3-none-any.whl (2.1MB)
         |████████████████████████████████| 2.1MB 42.4MB/s
    Requirement already satisfied: psutil>=5 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra==0.17.0->qiskit) (5.4.8)
    Requirement already satisfied: sympy>=1.3 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra==0.17.0->qiskit) (1.7.1)
    Requirement already satisfied: jsonschema>=2.6 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra==0.17.0->qiskit) (2.6.0)
    Requirement already satisfied: numpy>=1.17 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra==0.17.0->qiskit) (1.19.5)
    Requirement already satisfied: python-dateutil>=2.8.0 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra==0.17.0->qiskit) (2.8.1)
    Collecting ply>=3.10
      Downloading https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl (49kB)
         |████████████████████████████████| 51kB 4.9MB/s
    Requirement already satisfied: scipy>=1.4 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra==0.17.0->qiskit) (1.4.1)
    Collecting fastjsonschema>=2.10
      Downloading https://files.pythonhosted.org/packages/89/1c/8be51fa42aadc1c1611a52b866e1a5a1032a504f24789cf140b4e6d7c940/fastjsonschema-2.15.0-py3-none-any.whl
    Collecting python-constraint>=1.4
      Downloading https://files.pythonhosted.org/packages/37/8b/5f1bc2734ca611943e1d6733ee244238679f6410a10cd45ede55a61a8402/python-constraint-1.4.0.tar.bz2
    Requirement already satisfied: dill>=0.3 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra==0.17.0->qiskit) (0.3.3)
    Collecting retworkx>=0.8.0
      Downloading https://files.pythonhosted.org/packages/1b/92/f007f8b9d88dcd5b90e363967e5d54431a68c5fe06d83400732e3b438084/retworkx-0.8.0-cp37-cp37m-manylinux2010_x86_64.whl (1.0MB)
         |████████████████████████████████| 1.0MB 36.4MB/s
    Collecting pybind11>=2.6
      Downloading https://files.pythonhosted.org/packages/8d/43/7339dbabbc2793718d59703aace4166f53c29ee1c202f6ff5bf8a26c4d91/pybind11-2.6.2-py2.py3-none-any.whl (191kB)
         |████████████████████████████████| 194kB 53.2MB/s
    Requirement already satisfied: nest-asyncio!=1.1.0,>=1.0.0 in /usr/local/lib/python3.7/dist-packages (from qiskit-ibmq-provider==0.12.2->qiskit) (1.5.1)
    Requirement already satisfied: requests>=2.19 in /usr/local/lib/python3.7/dist-packages (from qiskit-ibmq-provider==0.12.2->qiskit) (2.23.0)
    Collecting requests-ntlm>=1.1.0
      Downloading https://files.pythonhosted.org/packages/03/4b/8b9a1afde8072c4d5710d9fa91433d504325821b038e00237dc8d6d833dc/requests_ntlm-1.1.0-py2.py3-none-any.whl
    Collecting websockets>=8
      Downloading https://files.pythonhosted.org/packages/5a/0b/3ebc752392a368af14dd24ee041683416ac6d2463eead94b311b11e41c82/websockets-8.1-cp37-cp37m-manylinux2010_x86_64.whl (79kB)
         |████████████████████████████████| 81kB 8.0MB/s
    Requirement already satisfied: urllib3>=1.21.1 in /usr/local/lib/python3.7/dist-packages (from qiskit-ibmq-provider==0.12.2->qiskit) (1.24.3)
    Requirement already satisfied: setuptools>=40.1.0 in /usr/local/lib/python3.7/dist-packages (from qiskit-ignis==0.6.0->qiskit) (54.2.0)
    Collecting quandl<=3.6.0
      Downloading https://files.pythonhosted.org/packages/c2/58/9f0e69d836045e3865d263e9ed49f42b23a58526fdabb30f74c430baee3f/Quandl-3.6.0-py2.py3-none-any.whl
    Requirement already satisfied: scikit-learn<=0.24.1,>=0.20.0 in /usr/local/lib/python3.7/dist-packages (from qiskit-aqua==0.9.0->qiskit) (0.22.2.post1)
    Requirement already satisfied: pandas<=1.2.3 in /usr/local/lib/python3.7/dist-packages (from qiskit-aqua==0.9.0->qiskit) (1.1.5)
    Requirement already satisfied: fastdtw<=0.3.4 in /usr/local/lib/python3.7/dist-packages (from qiskit-aqua==0.9.0->qiskit) (0.3.4)
    Collecting yfinance<=0.1.55
      Downloading https://files.pythonhosted.org/packages/7a/e8/b9d7104d3a4bf39924799067592d9e59119fcfc900a425a12e80a3123ec8/yfinance-0.1.55.tar.gz
    Collecting dlx<=1.0.4
      Downloading https://files.pythonhosted.org/packages/54/c0/b8fb5bb727e983b6f5251433ef941b48f38c65bb0bd6ec509e9185bcd406/dlx-1.0.4.tar.gz
    Collecting docplex<=2.20.204; sys_platform != "darwin"
      Downloading https://files.pythonhosted.org/packages/87/99/6f7c219b39fd58c84688ad0713eb932bfcf6be81fc74519e43ea9c915b56/docplex-2.20.204.tar.gz (611kB)
         |████████████████████████████████| 614kB 40.9MB/s
    Requirement already satisfied: h5py<=3.1.0 in /usr/local/lib/python3.7/dist-packages (from qiskit-aqua==0.9.0->qiskit) (2.10.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) (1.2.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) (1.15.0)
    Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.7/dist-packages (from requests>=2.19->qiskit-ibmq-provider==0.12.2->qiskit) (2020.12.5)
    Requirement already satisfied: idna<3,>=2.5 in /usr/local/lib/python3.7/dist-packages (from requests>=2.19->qiskit-ibmq-provider==0.12.2->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.12.2->qiskit) (3.0.4)
    Collecting ntlm-auth>=1.0.2
      Downloading https://files.pythonhosted.org/packages/ff/84/97c550164b54942b0e908c31ef09d9469f3ba4cd7332a671e2125732f63b/ntlm_auth-1.5.0-py2.py3-none-any.whl
    Collecting cryptography>=1.3
      Downloading https://files.pythonhosted.org/packages/b2/26/7af637e6a7e87258b963f1731c5982fb31cd507f0d90d91836e446955d02/cryptography-3.4.7-cp36-abi3-manylinux2014_x86_64.whl (3.2MB)
         |████████████████████████████████| 3.2MB 37.9MB/s
    Collecting inflection>=0.3.1
      Downloading https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl
    Requirement already satisfied: more-itertools in /usr/local/lib/python3.7/dist-packages (from quandl<=3.6.0->qiskit-aqua==0.9.0->qiskit) (8.7.0)
    Requirement already satisfied: joblib>=0.11 in /usr/local/lib/python3.7/dist-packages (from scikit-learn<=0.24.1,>=0.20.0->qiskit-aqua==0.9.0->qiskit) (1.0.1)
    Requirement already satisfied: pytz>=2017.2 in /usr/local/lib/python3.7/dist-packages (from pandas<=1.2.3->qiskit-aqua==0.9.0->qiskit) (2018.9)
    Requirement already satisfied: multitasking>=0.0.7 in /usr/local/lib/python3.7/dist-packages (from yfinance<=0.1.55->qiskit-aqua==0.9.0->qiskit) (0.0.9)
    Collecting lxml>=4.5.1
      Downloading https://files.pythonhosted.org/packages/cf/4d/6537313bf58fe22b508f08cf3eb86b29b6f9edf68e00454224539421073b/lxml-4.6.3-cp37-cp37m-manylinux1_x86_64.whl (5.5MB)
         |████████████████████████████████| 5.5MB 43.8MB/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.12.2->qiskit) (1.14.5)
    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.12.2->qiskit) (2.20)
    Building wheels for collected packages: qiskit, python-constraint, yfinance, dlx, docplex
      Building wheel for qiskit (setup.py) ... done
      Created wheel for qiskit: filename=qiskit-0.25.0-cp37-none-any.whl size=3032 sha256=bd9ed1854661fc5bbedca5b27eaadb43274beef9a59100643f76bf8183563671
      Stored in directory: /root/.cache/pip/wheels/09/6e/f1/a7eaab6e3943d749f1b257f462f60fbc402b0c8e870b555169
      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=24079 sha256=c10ef798e8300679d3f6c19b7d35d72659e24b5725dfb91885537f3e456cee32
      Stored in directory: /root/.cache/pip/wheels/34/31/15/7b070b25d0a549d20ce2e9fe6d727471c2c61ef904720fd40c
      Building wheel for yfinance (setup.py) ... done
      Created wheel for yfinance: filename=yfinance-0.1.55-py2.py3-none-any.whl size=22616 sha256=238d00ee361319b34a584800ff492231426eed70b361a2c43bce46acf5b1f1f2
      Stored in directory: /root/.cache/pip/wheels/04/98/cc/2702a4242d60bdc14f48b4557c427ded1fe92aedf257d4565c
      Building wheel for dlx (setup.py) ... done
      Created wheel for dlx: filename=dlx-1.0.4-cp37-none-any.whl size=5712 sha256=31e657f5b05376f43942320f323e4b11d090fecf09f25c684f9da020b156d00b
      Stored in directory: /root/.cache/pip/wheels/bb/ba/15/fdd0deb104df3254912998150ba9245668db06b00af5912d1a
      Building wheel for docplex (setup.py) ... done
      Created wheel for docplex: filename=docplex-2.20.204-cp37-none-any.whl size=675362 sha256=fc57801d30a3be5977d6c15d001999799d2336a5720425151dbeb70ba2a4e966
      Stored in directory: /root/.cache/pip/wheels/ae/2c/e2/a099ebb6fda8adeba9c5fc2e25659d195ad2f5c6cc5fb75fd4
    Successfully built qiskit python-constraint yfinance dlx docplex
    Installing collected packages: ply, fastjsonschema, python-constraint, retworkx, qiskit-terra, pybind11, qiskit-aer, ntlm-auth, cryptography, requests-ntlm, websockets, qiskit-ibmq-provider, qiskit-ignis, inflection, quandl, lxml, yfinance, dlx, docplex, qiskit-aqua, qiskit
      Found existing installation: lxml 4.2.6
        Uninstalling lxml-4.2.6:
          Successfully uninstalled lxml-4.2.6
    Successfully installed cryptography-3.4.7 dlx-1.0.4 docplex-2.20.204 fastjsonschema-2.15.0 inflection-0.5.1 lxml-4.6.3 ntlm-auth-1.5.0 ply-3.11 pybind11-2.6.2 python-constraint-1.4.0 qiskit-0.25.0 qiskit-aer-0.8.0 qiskit-aqua-0.9.0 qiskit-ibmq-provider-0.12.2 qiskit-ignis-0.6.0 qiskit-terra-0.17.0 quandl-3.6.0 requests-ntlm-1.1.0 retworkx-0.8.0 websockets-8.1 yfinance-0.1.55

```python
import numpy as np

from qiskit import BasicAer
from qiskit.aqua import QuantumInstance, aqua_globals
from qiskit.aqua.algorithms import VQC
from qiskit.aqua.components.optimizers import SPSA
from qiskit.circuit.library import TwoLocal, ZFeatureMap,EfficientSU2
from qiskit.aqua.utils import split_dataset_to_data_and_labels, map_label_to_class_name

from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, MinMaxScaler
from sklearn.metrics import classification_report

import time

seed = 10599
aqua_globals.random_seed = seed
```

    /usr/local/lib/python3.7/dist-packages/ipykernel_launcher.py:18: DeprecationWarning: The variable qiskit.aqua.aqua_globals is deprecated. It was moved/refactored to qiskit.utils.aqua_globals (pip install qiskit-terra). For more information see <https://github.com/Qiskit/qiskit-aqua/blob/master/README.md#migration-guide>

```python
class circuit_result():
  '''
  class to store details of each circuit and later pickle it
  '''
  def details(self):
    print("Circuit type is: \t",self.circuit_type)
    print("feature_map used is :\t",self.feature_map)
    print("The paulis used were: \t",self.paulis)
    print("The entanglement layout is \t",self.entanglement)
    print("The repititions of feature map: \t",self.reps)
    print("running time is : \t",self.running_time)
    print("Accuracy report: \n",accuracy)
    print("Circuit Depth: \t", self.depth)
    print("Number of operations \t",self.count_ops)

  def __init__(self,circuit_type,feature_map,time,accuracy,depth,count_ops,paulis,reps,entanglement):
    self.circuit_type=circuit_type
    self.feature_map=feature_map
    self.running_time=time
    self.accuracy=accuracy
    self.depth=depth
    self.count_ops=count_ops
    self.paulis=paulis
    self.reps=reps
    self.entanglement=entanglement
```

```python
training_size=120
test_size=30
class_labels = [r'A', r'B', r'C']
data, target = datasets.load_iris(return_X_y=True)
sample_train, sample_test, label_train, label_test =train_test_split(data, target, test_size=30, random_state=42)
std_scale = StandardScaler().fit(sample_train)
sample_train = std_scale.transform(sample_train)
sample_test = std_scale.transform(sample_test)
samples = np.append(sample_train, sample_test, axis=0)
minmax_scale = MinMaxScaler((-1, 1)).fit(samples)
sample_train = minmax_scale.transform(sample_train)
sample_test = minmax_scale.transform(sample_test)
training_input = {key: (sample_train[label_train == k, :])[:training_size]
                      for k, key in enumerate(class_labels)}
test_input = {key: (sample_test[label_test == k, :])[:test_size]
                  for k, key in enumerate(class_labels)}
```

```python
feature_map = ZFeatureMap(feature_dimension=4, reps=1)
optimizer = SPSA(maxiter=80)
var_form = TwoLocal(4, ['ry','rz'], 'crz', reps=3,entanglement='circular')
vqc = VQC(optimizer, feature_map, var_form, training_input, test_input)

backend = BasicAer.get_backend('qasm_simulator')
quantum_instance = QuantumInstance(backend, shots=1024, seed_simulator=seed, seed_transpiler=seed)

start=time.time()
result = vqc.run(quantum_instance)
end= time.time()
print("training took ",end-start)
params=vqc.optimal_params
y_pred=vqc.predict(sample_test,quantum_instance,params=params)
accuracy=classification_report(y_pred[1],label_test)
print(accuracy)
```

    /usr/local/lib/python3.7/dist-packages/qiskit/aqua/components/optimizers/optimizer.py:50: DeprecationWarning: The package qiskit.aqua.components.optimizers is deprecated. It was moved/refactored to qiskit.algorithms.optimizers (pip install qiskit-terra). For more information see <https://github.com/Qiskit/qiskit-aqua/blob/master/README.md#migration-guide>
      'qiskit.algorithms.optimizers', 'qiskit-terra')
    /usr/local/lib/python3.7/dist-packages/qiskit/aqua/algorithms/classifiers/vqc.py:98: DeprecationWarning: The package qiskit.aqua.algorithms.classifiers is deprecated. It was moved/refactored to qiskit_machine_learning.algorithms.classifiers (pip install qiskit-machine-learning). For more information see <https://github.com/Qiskit/qiskit-aqua/blob/master/README.md#migration-guide>
      'qiskit-machine-learning')
    /usr/local/lib/python3.7/dist-packages/qiskit/aqua/algorithms/vq_algorithm.py:72: DeprecationWarning: The class qiskit.aqua.algorithms.VQAlgorithm is deprecated. It was moved/refactored to qiskit.algorithms.VariationalAlgorithm (pip install qiskit-terra). For more information see <https://github.com/Qiskit/qiskit-aqua/blob/master/README.md#migration-guide>
      'qiskit-terra')
    /usr/local/lib/python3.7/dist-packages/qiskit/aqua/quantum_instance.py:137: DeprecationWarning: The class qiskit.aqua.QuantumInstance is deprecated. It was moved/refactored to qiskit.utils.QuantumInstance (pip install qiskit-terra). For more information see <https://github.com/Qiskit/qiskit-aqua/blob/master/README.md#migration-guide>
      'qiskit-terra')
    /usr/local/lib/python3.7/dist-packages/qiskit/ml/__init__.py:40: DeprecationWarning: The package qiskit.ml is deprecated. It was moved/refactored to qiskit_machine_learning (pip install qiskit-machine-learning). For more information see <https://github.com/Qiskit/qiskit-aqua/blob/master/README.md#migration-guide>
      warn_package('ml', 'qiskit_machine_learning', 'qiskit-machine-learning')

    training took  543.2600898742676
                  precision    recall  f1-score   support

               0       1.00      1.00      1.00        10
               1       0.89      0.89      0.89         9
               2       0.91      0.91      0.91        11

        accuracy                           0.93        30
       macro avg       0.93      0.93      0.93        30
    weighted avg       0.93      0.93      0.93        30

```python

```

```python
feature_map = ZFeatureMap(feature_dimension=4, reps=1)
optimizer = SPSA(maxiter=80)
var_form = EfficientSU2(4,reps=3)
vqc = VQC(optimizer, feature_map, var_form, training_input, test_input)

backend = BasicAer.get_backend('qasm_simulator')
quantum_instance = QuantumInstance(backend, shots=1024, seed_simulator=seed, seed_transpiler=seed)

start=time.time()
result = vqc.run(quantum_instance)
end= time.time()
print("training took ",end-start)
params=vqc.optimal_params
y_pred=vqc.predict(sample_test,quantum_instance,params=params)
accuracy=classification_report(y_pred[1],label_test)
print(accuracy)
```

    training took  406.8485155105591
                  precision    recall  f1-score   support

               0       1.00      1.00      1.00        10
               1       0.89      0.73      0.80        11
               2       0.73      0.89      0.80         9

        accuracy                           0.87        30
       macro avg       0.87      0.87      0.87        30
    weighted avg       0.88      0.87      0.87        30

```python
TwoLocal(4, ['ry','rz'], 'crz', reps=3,entanglement='sca',insert_barriers=True).draw()
```

     ┌──────────┐┌──────────┐ ░ ┌──────────┐                         »
q_0: ┤ RY(θ[0]) ├┤ RZ(θ[4]) ├─░─┤ RZ(θ[8]) ├─────■───────────────────»
     ├──────────┤├──────────┤ ░ └────┬─────┘┌────┴─────┐             »
q_1: ┤ RY(θ[1]) ├┤ RZ(θ[5]) ├─░──────┼──────┤ RZ(θ[9]) ├──────■──────»
     ├──────────┤├──────────┤ ░      │      └──────────┘┌─────┴─────┐»
q_2: ┤ RY(θ[2]) ├┤ RZ(θ[6]) ├─░──────┼──────────────────┤ RZ(θ[10]) ├»
     ├──────────┤├──────────┤ ░      │                  └───────────┘»
q_3: ┤ RY(θ[3]) ├┤ RZ(θ[7]) ├─░──────■───────────────────────────────»
     └──────────┘└──────────┘ ░                                      »
«                   ░ ┌───────────┐┌───────────┐ ░                           »
«q_0: ──────────────░─┤ RY(θ[12]) ├┤ RZ(θ[16]) ├─░────────────────────■──────»
«                   ░ ├───────────┤├───────────┤ ░                    │      »
«q_1: ──────────────░─┤ RY(θ[13]) ├┤ RZ(θ[17]) ├─░────────────────────┼──────»
«                   ░ ├───────────┤├───────────┤ ░ ┌───────────┐      │      »
«q_2: ──────■───────░─┤ RY(θ[14]) ├┤ RZ(θ[18]) ├─░─┤ RZ(θ[20]) ├──────┼──────»
«     ┌─────┴─────┐ ░ ├───────────┤├───────────┤ ░ └─────┬─────┘┌─────┴─────┐»
«q_3: ┤ RZ(θ[11]) ├─░─┤ RY(θ[15]) ├┤ RZ(θ[19]) ├─░───────■──────┤ RZ(θ[21]) ├»
«     └───────────┘ ░ └───────────┘└───────────┘ ░              └───────────┘»
«     ┌───────────┐              ░ ┌───────────┐┌───────────┐ ░              »
«q_0: ┤ RZ(θ[22]) ├──────────────░─┤ RY(θ[24]) ├┤ RZ(θ[28]) ├─░──────────────»
«     └─────┬─────┘┌───────────┐ ░ ├───────────┤├───────────┤ ░              »
«q_1: ──────■──────┤ RZ(θ[23]) ├─░─┤ RY(θ[25]) ├┤ RZ(θ[29]) ├─░───────■──────»
«                  └─────┬─────┘ ░ ├───────────┤├───────────┤ ░ ┌─────┴─────┐»
«q_2: ───────────────────■───────░─┤ RY(θ[26]) ├┤ RZ(θ[30]) ├─░─┤ RZ(θ[32]) ├»
«                                ░ ├───────────┤├───────────┤ ░ └───────────┘»
«q_3: ───────────────────────────░─┤ RY(θ[27]) ├┤ RZ(θ[31]) ├─░──────────────»
«                                ░ └───────────┘└───────────┘ ░              »
«                  ┌───────────┐              ░ ┌───────────┐┌───────────┐
«q_0: ─────────────┤ RZ(θ[34]) ├──────■───────░─┤ RY(θ[36]) ├┤ RZ(θ[40]) ├
«                  └─────┬─────┘┌─────┴─────┐ ░ ├───────────┤├───────────┤
«q_1: ───────────────────┼──────┤ RZ(θ[35]) ├─░─┤ RY(θ[37]) ├┤ RZ(θ[41]) ├
«                        │      └───────────┘ ░ ├───────────┤├───────────┤
«q_2: ──────■────────────┼────────────────────░─┤ RY(θ[38]) ├┤ RZ(θ[42]) ├
«     ┌─────┴─────┐      │                    ░ ├───────────┤├───────────┤
«q_3: ┤ RZ(θ[33]) ├──────■────────────────────░─┤ RY(θ[39]) ├┤ RZ(θ[43]) ├
«     └───────────┘                           ░ └───────────┘└───────────┘

```python
feature_map = ZFeatureMap(feature_dimension=4, reps=1)
optimizer = SPSA(maxiter=100)
var_form = EfficientSU2(4,reps=3)
vqc = VQC(optimizer, feature_map, var_form, training_input, test_input)

backend = BasicAer.get_backend('qasm_simulator')
quantum_instance = QuantumInstance(backend, shots=1024, seed_simulator=seed, seed_transpiler=seed)

start=time.time()
result = vqc.run(quantum_instance)
end= time.time()
print("training took ",end-start)
params=vqc.optimal_params
y_pred=vqc.predict(sample_test,quantum_instance,params=params)
accuracy=classification_report(y_pred[1],label_test)
print(accuracy)
```

    training took  509.36247539520264
                  precision    recall  f1-score   support

               0       1.00      1.00      1.00        10
               1       1.00      0.82      0.90        11
               2       0.82      1.00      0.90         9

        accuracy                           0.93        30
       macro avg       0.94      0.94      0.93        30
    weighted avg       0.95      0.93      0.93        30

```python
feature_map = ZFeatureMap(feature_dimension=4, reps=1)
optimizer = SPSA(maxiter=120)
var_form = TwoLocal(4, ['ry','rz'], 'crz', reps=3,entanglement='circular')
vqc = VQC(optimizer, feature_map, var_form, training_input, test_input)

backend = BasicAer.get_backend('qasm_simulator')
quantum_instance = QuantumInstance(backend, shots=1024, seed_simulator=seed, seed_transpiler=seed)

start=time.time()
result = vqc.run(quantum_instance)
end= time.time()
print("training took ",end-start)
params=vqc.optimal_params
y_pred=vqc.predict(sample_test,quantum_instance,params=params)
accuracy=classification_report(y_pred[1],label_test)
print(accuracy)
```

    training took  815.8849918842316
                  precision    recall  f1-score   support

               0       1.00      1.00      1.00        10
               1       1.00      0.90      0.95        10
               2       0.91      1.00      0.95        10

        accuracy                           0.97        30
       macro avg       0.97      0.97      0.97        30
    weighted avg       0.97      0.97      0.97        30

```python
feature_map = ZFeatureMap(feature_dimension=4, reps=1)
optimizer = SPSA(maxiter=120)
var_form = TwoLocal(4, ['rz'], 'crz', reps=3,entanglement='circular')
vqc = VQC(optimizer, feature_map, var_form, training_input, test_input)

backend = BasicAer.get_backend('qasm_simulator')
quantum_instance = QuantumInstance(backend, shots=1024, seed_simulator=seed, seed_transpiler=seed)

start=time.time()
result = vqc.run(quantum_instance)
end= time.time()
print("training took ",end-start)
params=vqc.optimal_params
y_pred=vqc.predict(sample_test,quantum_instance,params=params)
accuracy=classification_report(y_pred[1],label_test)
print(accuracy)
```

    /usr/local/lib/python3.7/dist-packages/qiskit/aqua/components/optimizers/optimizer.py:50: DeprecationWarning: The package qiskit.aqua.components.optimizers is deprecated. It was moved/refactored to qiskit.algorithms.optimizers (pip install qiskit-terra). For more information see <https://github.com/Qiskit/qiskit-aqua/blob/master/README.md#migration-guide>
      'qiskit.algorithms.optimizers', 'qiskit-terra')
    /usr/local/lib/python3.7/dist-packages/qiskit/aqua/algorithms/classifiers/vqc.py:98: DeprecationWarning: The package qiskit.aqua.algorithms.classifiers is deprecated. It was moved/refactored to qiskit_machine_learning.algorithms.classifiers (pip install qiskit-machine-learning). For more information see <https://github.com/Qiskit/qiskit-aqua/blob/master/README.md#migration-guide>
      'qiskit-machine-learning')
    /usr/local/lib/python3.7/dist-packages/qiskit/aqua/algorithms/vq_algorithm.py:72: DeprecationWarning: The class qiskit.aqua.algorithms.VQAlgorithm is deprecated. It was moved/refactored to qiskit.algorithms.VariationalAlgorithm (pip install qiskit-terra). For more information see <https://github.com/Qiskit/qiskit-aqua/blob/master/README.md#migration-guide>
      'qiskit-terra')
    /usr/local/lib/python3.7/dist-packages/qiskit/aqua/quantum_instance.py:137: DeprecationWarning: The class qiskit.aqua.QuantumInstance is deprecated. It was moved/refactored to qiskit.utils.QuantumInstance (pip install qiskit-terra). For more information see <https://github.com/Qiskit/qiskit-aqua/blob/master/README.md#migration-guide>
      'qiskit-terra')
    /usr/local/lib/python3.7/dist-packages/qiskit/ml/__init__.py:40: DeprecationWarning: The package qiskit.ml is deprecated. It was moved/refactored to qiskit_machine_learning (pip install qiskit-machine-learning). For more information see <https://github.com/Qiskit/qiskit-aqua/blob/master/README.md#migration-guide>
      warn_package('ml', 'qiskit_machine_learning', 'qiskit-machine-learning')

    training took  661.326092004776
                  precision    recall  f1-score   support

               0       0.00      0.00      0.00         0
               1       1.00      0.30      0.46        30
               2       0.00      0.00      0.00         0

        accuracy                           0.30        30
       macro avg       0.33      0.10      0.15        30
    weighted avg       1.00      0.30      0.46        30

    /usr/local/lib/python3.7/dist-packages/sklearn/metrics/_classification.py:1272: UndefinedMetricWarning: Recall and F-score are ill-defined and being set to 0.0 in labels with no true samples. Use `zero_division` parameter to control this behavior.
      _warn_prf(average, modifier, msg_start, len(result))

```python
feature_map = ZFeatureMap(feature_dimension=4, reps=1)
optimizer = SPSA(maxiter=120)
var_form = TwoLocal(4, ['rx', 'rz'], 'crz', reps=3,entanglement='circular')
vqc = VQC(optimizer, feature_map, var_form, training_input, test_input)

backend = BasicAer.get_backend('qasm_simulator')
quantum_instance = QuantumInstance(backend, shots=1024, seed_simulator=seed, seed_transpiler=seed)

start=time.time()
result = vqc.run(quantum_instance)
end= time.time()
print("training took ",end-start)
params=vqc.optimal_params
y_pred=vqc.predict(sample_test,quantum_instance,params=params)
accuracy=classification_report(y_pred[1],label_test)
print(accuracy)
```

    training took  807.7532076835632
                  precision    recall  f1-score   support

               0       1.00      1.00      1.00        10
               1       1.00      0.69      0.82        13
               2       0.64      1.00      0.78         7

        accuracy                           0.87        30
       macro avg       0.88      0.90      0.87        30
    weighted avg       0.92      0.87      0.87        30

```python

```


#Lesson1 基本問題

##ex1

17より小さい数字を渡すと17との差を返すが、17より大きい数字を渡すと差の2乗を返す関数を作成せよ

```python
def func1(n):
    ans = 17 - n
    return ans if ans > 0 else ans ** 2
```

テストコード

```python
assert func1(0) == 17
assert func1(15) == 2
assert func1(17) == 0
assert func1(19) == 4
assert func1(100) == 6889
```

##ex2

1000か2000との差が100以下の数であることを判定する関数を作成せよ

```python
def func2(n):
    return abs(n - 1000) <= 100 or abs(n - 2000) <= 100
```

テストコード

```python
assert func2(899) == False
assert func2(900) == True
assert func2(1100) == True
assert func2(1101) == False
assert func2(1899) == False
assert func2(1900) == True
assert func2(2100) == True
assert func2(2101) == False
```

##ex3

アルファベットを1文字渡すとそのアルファベットが子音か母音かを判定する関数を作成せよ

```python
def func3(s):
    return "vowel" if s in "aiueo" else "consonant"
```

テストコード

```python
assert func3("a") == "vowel"
assert func3("b") == "consonant"
assert func3("c") == "consonant"
assert func3("d") == "consonant"
assert func3("e") == "vowel"
```

##ex4

文字列と数字を渡すと、文字列の先頭2文字を数字の回数だけ繰り返した文字列を返す関数を作成せよ

ただし、渡した文字列が1文字の場合、その文字を数字の回数だけ繰り返した文字列を返すこと

```python
# def func4(s, n):
#     return s[:2] * n if len(s) > 2 else s * n

def func4(s, n):
    return s[:2] * n
```

テストコード

```python
assert func4("ab", 8) == "abababababababab"
assert func4("abc", 10) == "abababababababababab"
assert func4("abcdefg", 20) == "abababababababababababababababababababab"
assert func4("a", 2) == "aa"
```

##ex5

数字3個をリストで渡すと合計を返すが、3個の数字がすべて同じであれば合計の3
倍を返す関数を作成せよ

```python
def func5(a):
    ans = sum(a)
    return ans * 3 if a[0] == a[1] == a[2] else ans
```

テストコード

```python
assert func5([1, 2, 3]) == 6
assert func5([2, 2, 2]) == 18
assert func5([2, 4, 6]) == 12
assert func5([5, 5, 5]) == 45
```

#Lesson2 応用問題

##ex6 BMI計算

BMIを計算する関数を作成せよ

>$BMI = 体重[kg] ÷ (身長[m])^2$

ただし、小数点以下２桁まで（未満は切り捨て）

```python
# 切り捨てはintが簡単
def func6(h, w):
    return (w*100 // h**2) / 100
```

テストコード

```python
assert func6(1.60, 60) == 23.43
assert func6(1.65, 65) == 23.87
assert func6(1.70, 70) == 24.22
assert func6(1.75, 75) == 24.48
```

##ex7 BMI判定

BMIをから肥満度や健康状態を確認する関数を作成せよ

なお、BMIの判定基準は次のようにする

* 18.5未満は「痩せすぎ」
* 18.5以上25未満が「標準」
* 25以上が「肥満」
* 22 $±$ 10%の人は「健康」
* 23以上24未満の人は「長生き」

ちなみに、BMIは次のようなことが知られている

* 健康診断の結果に異常がない人が最も多いのは22前後
* 最も長生きするのは23〜24の人

```python
# "="の付け方によってテストコードがうまく行かな場合があるらしい・・・その辺はいい感じでお願いします
def func7(h, w):
    bmi = func6(h, w)
    if 21.78 <= bmi < 22.22:
        chk = "健康"
    elif 23 <= bmi < 24:
        chk = "長生き"
    elif 18 <= bmi < 25:
        chk = "標準"
    elif bmi < 18:
        chk = "痩せすぎ"
    else:
        chk = "肥満"

    return f"あなたは「{chk}」です。"
```

テストコード

```python
assert func7(1.70, 50) == "あなたは「痩せすぎ」です。"
assert func7(1.70, 60) == "あなたは「標準」です。"
assert func7(1.70, 64) == "あなたは「健康」です。"
assert func7(1.70, 68) == "あなたは「長生き」です。"
assert func7(1.70, 80) == "あなたは「肥満」です。"
```

##ex8 温度変換

華氏と摂氏を相互に変換する関数を作成せよ

ただし、華氏は「F」を、摂氏は「C」を、それぞれ数字の後に付けた文字列で表すこととする

なお、華氏と摂氏の変換式は次のとおり

>$(F - 32) × 5 ÷ 9 = C$

変換後の数値は小数点1桁まで（未満は四捨五入）

※もし可能なら、変換できない不明な入力には「無効」を返すこと

```python
# 四捨五入ならフォーマットでも可能
def func8(s):
    try:
        if s[-1] in "fF":
            t = int(s[:-1])
            x = (t - 32) * 5 / 9
            return f"{x:.1f}C"
        elif s[-1] in "cC":
            t = int(s[:-1])
            x = 9 * t / 5 + 32
            return f"{x:.1f}F"
        else:
            raise
    except:
        return "無効"
```

```python
assert func8("32F") == "0.0C"
assert func8("4C") == "39.2F"
assert func8("211f") == "99.4C"
assert func8("-4c") == "24.8F"
assert func8("5X") == "無効"
assert func8("!5F") == "無効"
```

#lesson3 RSA暗号① 〜準備〜

##ex9 桁の大きい指数関数

$n^{n^n}$ の１の桁を計算する関数を作成せよ

```python
# この方法だとn=13でもなかなか終わらない・・・
# def func9(n):
#     return n ** (n ** n) % 10

# 標準関数
# def func9(n):
#     return pow(n, n**n, 10)

# 補足１参照
# def func9(n):
#     x = n ** n
#     y = n
#     ans = 1
#     while x > 0:
#         if x % 2 == 1:
#             ans *= y
#             ans = ans % 10
#         y = y * y % 10
#         x = x // 2
#     return ans

# 補足２参照
def func9(n):
    m = 4   #10より小さい10と互いに素な自然数の数（オイラーのトーシェント関数）
    return n ** m % 10 if n**n % m == 0 else n ** (n ** n % m) %10

```

テストコード

※処理が止められなくなった場合、右上のメニューから一旦セッションを切断すると止まる

```python
assert func9(3) == 7
assert func9(7) == 3
assert func9(13) == 3
assert func9(432) == 6
```

補足１：

$n^{2^m}$を考えるとき、$n$を$2^m$回乗算するのは効率が悪い

なぜなら、１ステップ目で$n^{2}$を計算したら、２ステップ目はその計算結果を２乗して$n^4$を計算する方が明らかに効率が良いから

あとはこれを$m$繰り返せば$2^m$が求まる

$n^{N}$の計算は指数部の$N$がどんな整数でも

>$N = Σ_{i=0}a_i 2^{m_i}$

と表せるので

>$n^N = 𝝥_{i=0}n^{a_i 2^{m_i}}$

と書けて、上の考え方が利用できる

補足２：

オイラーの公式

>$a^{\phi(b)} = 1\ mod\ b$

は$a$と$b$が互いに素の時しか成立しない

※$\phi(b)$はオイラーのトーシェント関数：$b$より小さい$b$と互いに素な自然数の数

しかし、$a$と$b$が互いに素ではなくても

>$a^{\phi(b)+1} = a\ mod\ b$

は成立する

つまり、

>$a^{\phi(b)} \times a = a\ mod\ b$

なのだから、$a^N\ mod\ b$を計算する上では

>$a^{\phi(b)}=1\ mod\ b$

とみなしても実質的に問題ない

よって、

>$N = α\phi(b) + β$

とすると

>$a^{N} = a^{α\phi(b) + β} = a^β\ mod\ b$

となる

ただし、$β=0$の場合だけは注意が必要で、$a$と$b$が互いに素であれば「１」で良いが、$a$と$b$が互いに素でなければ「$a^{\phi(b)} ~ mod ~ b$」を計算する必要がある

この方法を利用する場合、事前に$\phi(b)$を計算する必要があるため、$b$が大きな数になると必ずしも効率的とは言えなくなる

##ex10 16進ダンプ

文字列を16進ダンプする関数せよ

ただし、文字符号化形式は「UTF-8」とする

```python
def func10(s):
    return ''.join(f'{x:02X}' for x in s.encode('utf-8'))

# def func10(s):
#     return f"{int.from_bytes(s.encode('utf-8'), 'big'):X}"

# def func10(s):
#     return s.encode('utf-8').hex().upper()

# import codecs
# def func10(s):
#     return codecs.encode(s.encode('utf-8'), 'hex_codec').decode().upper()

```

テストコード

```python
assert func10("Caeleb Remel Dressel") == "4361656C65622052656D656C204472657373656C"
assert func10("Lamont Marcell Jacobs") == "4C616D6F6E74204D617263656C6C204A61636F6273"
assert func10("西矢椛") == "E8A5BFE79FA2E6A49B"
assert func10("堀米雄斗") == "E5A080E7B1B3E99B84E69697"
```

補足１：

```
s = "Caeleb Remel Dressel"
print(''.join(f'{ord(x):X}' for x in s))

4361656C65622052656D656C204472657373656C
```
としてもよさそうだけど・・・

```
s = "西矢椛"
print(''.join(f'{ord(x):X}' for x in s))

897F77E2691B

```

となってうまく行かない

これは、ord関数が与えられた文字のコードポイント（unicode）を返すから

UTF-８はユニコードの文字符号化形式のひとつであり、次の特徴がある

*   コードポイント0〜127：00〜7Fの１バイトで表す
*   コードポイント128〜：80〜FFを2〜４個組み合わせた２〜４バイトで表す

上の特徴があるため、UTF-8はASCIIと互換性がある

補足２：

"西矢椛"の各文字のコードポイントは次のとおり

```
for x in "西矢椛":
    print(ord(x))

35199 # "西"
30690 # "矢"
26907 # "椛"
```

一方、UTF-8で符号化したバイトの並びは次のとおり

```
for x in "西矢椛":
    print(''.join(f"{b:X}" for b in x.encode('utf-8')))

E8A5BF # "西"
E79FA2 # "矢"
E6A49B # "椛"
```

両者の関係について考える

まず、"西"のUTF-8符号化の値とコードポイントをそれぞれ２進数に変換

>$E8A5BF =111010001010010110111111\\
35199 =1000100101111111$

分かりやすくするため、1バイトずつ区切って表示

>$$
\begin{align}
1110~&1000&10~&100101&10~&111111\\
      &1000&   &100101&  &111111\\
\end{align}
$$

各バイトの先頭に付加されているビットは、それぞれ次の意味

*   1110：３バイト文字の１バイト目であること
*   10：２バイト以上の文字の２バイト目以降であること

##ex11 16進→10進変換

16進数を10進数に変換する関数

```python
def func11(s):
    return int(s, 16)
```

テストコード

```python
assert func11("4361656C65622052656D656C204472657373656C") == 384674389155590982046234764697110782161450132844
assert func11("4C616D6F6E74204D617263656C6C204A61636F6273") == 111630337030194331012420294446637731487233908630131
assert func11("E8A5BFE79FA2E6A49B") == 4291588144510244660379
assert func11("E5A080E7B1B3E99B84E69697") == 71066104117138884767124985495
```

##ex12 10進→文字列変換

10進数を文字列に変換する関数

ただし、文字符号化形式は「UTF-8」とする

```python
def func12(n):
    return n.to_bytes((n.bit_length() + 7) // 8, 'big').decode('utf-8')

# def func12(n):
#     return bytes.fromhex(hex(n)[2:]).decode('utf-8')

# import codecs
# def func12(n):
#     return codecs.decode(hex(n)[2:].encode(), 'hex_codec').decode('utf-8')

```

テストコード

```python
assert func12(384674389155590982046234764697110782161450132844) == "Caeleb Remel Dressel"
assert func12(111630337030194331012420294446637731487233908630131) =="Lamont Marcell Jacobs"
assert func12(4291588144510244660379) == "西矢椛"
assert func12(71066104117138884767124985495) == "堀米雄斗"
```

#Lesson4 RSA暗号② 〜暗号化と復号〜

##ex13 RSA暗号（暗号化）

次のパラメータを使って数値（func10 & func11で文字列を変換したもの）をRSAで暗号化する関数を作成せよ

p = 1223608816783989526977697142729
q = 1181419059721907087891844374533
N = 1445594777792376190871161299886642462542562458520846233720557
e = 65537

※$N = p × q$

ただし、暗号文Cと平文Mの関係は次のとおり

>$C = M^e ~ mod ~ N$

```python
p = 1223608816783989526977697142729
q = 1181419059721907087891844374533
N = 1445594777792376190871161299886642462542562458520846233720557
e = 65537
```

```python
def func13(m):
    return pow(m, e, N)
```

テストコード

```python
assert func13(384674389155590982046234764697110782161450132844) == 933724396887774009673956822287392949344951453324424927225843
assert func13(111630337030194331012420294446637731487233908630131) == 96166222195780378773068570820947104808959039543354452361988
assert func13(4291588144510244660379) == 259428661535051310743580900142038188754161976801949527257666
assert func13(71066104117138884767124985495) == 251132345489958486648793004527006641296516405032422636490685
```

作った暗号文を復号する関数を作りたいが、そのためには復号用の鍵、つまり秘密鍵$d$を計算する必要がある

RSAの秘密鍵$d$は公開鍵$e$と次の関係を満たす

>$e×d ≡ 1 ~ mod ~ \phi \\
\phi = (p-1)×(q-1)$

「$≡$」 や 「$mod \phi$」 は剰余計算の合同式（左辺と右辺の$\phi$で割った余りが等しいこと）を表す

※面倒なので、以降は「$≡$」を「$=$」と書く

##ex14 モジュラ逆数の計算（総当たり）

２つの素数$p,q$を与えた時、次の関係式を満たす$d$を**総当たりで**探す関数を作成せよ

>$e×d ≡ 1 ~ mod ~\phi \\ \phi = (p-1)×(q-1)$

ただし、$e = 65537, \quad d < (p-1)×(q-1)$

```python
e = 65537
```

```python
def func14(p, q):
    phi = (p-1)*(q-1)
    for i in range(phi):
        if e * i % phi == 1:
            return i
```

テストコード

```python
assert func14(659, 643) == 232937
assert func14(821, 769) == 197633
assert func14(691, 941) == 274673
```

テストコード２

※終了まで少し時間がかかります（30秒くらい）

```python
assert func14(17519, 24611) == 108290953
```

テストコード３

※終了までさらに時間がかかります（２〜３分）

```python
assert func14(36373, 64901) == 841967873
```

以上の結果から、$p,q$が大きくなると総当たりで$d$を計算するのはキツいことが分かる

そこで、別の方法を考える

---

２つの整数 $(a, b)$ に対する不定方程式

>$ax+by = gcd(a,b)$

の解$(x,y)$は高速に計算できる

---

ことが知られている

※$gcd(a,b)$は$a$と$b$の最大公約数

例えば

>$(a,b) = (e, \phi) \\ gcd(e, \phi) = 1$

とすると

>$ex+\phi y = 1$

両辺で$\phi$の剰余を計算すると

>$ex = 1 ~ mod ~ \phi$

となり、$d$ $(=x)$ が求まる

##ex15 ユークリッドの互除法

$gcd(e, \phi)=1$ を確認するため、２つの素数 $(p,q)$ から$gcd(e, \phi)$を計算する関数を作成せよ

ただし、
>$e = 65537 \\ \phi=(p-1)\times(q-1)$

※「ユークリッドの互除法」を実装するのが望ましいが、mathモジュールのmath.gcd(a, b)関数を使っても良い

```python
e = 65537
```

```python
def gcd(x, y):
    if y == 0:
        return x
    else:
        return gcd(y, x%y)

def func15(p, q):
    x = (p-1)*(q-1)
    y = e
    return gcd(x, y)

# import math
# def func15(p, q):
#     return math.gcd(e, (p-1)*(q-1))
```

テストコード

```python
assert func15(36373, 64901) == 1
assert func15(915349060633981, 879702975012439) == 1
assert func15(1223608816783989526977697142729, 1181419059721907087891844374533) == 1
```

※もしも$gcd(e, (p-1)×(q-1)) ≠ 1$だったら、$d$が決まらず復号できない

##ex16 拡張ユークリッドの互除法

２つの素数（a, b）から次の関係式を満たす$x$を計算する関数を作成せよ

>$ax = 1 ~ mod ~ b$

※不定方程式の解は「拡張ユークリッドの互除法」で実装するのが望ましいが、sympyモジュールのgcdex関数を使っても良い

※$x$は負の値の場合もあるので、最後に$b$の剰余を計算しておくと良い

```python
def func16(a, b):
    if a == 1:
        return 1
    else:
        return b + (-b * func16(b % a, a) + 1) // a

# import sympy
# def func16(a, b):
#     x, y, gcd = sympy.gcdex(a, b)
#     return x%b
```

テストコード

```python
assert func16(73571, 1072894853) == 192905535
assert func16(64901, 1207471893509091040314362866849) == 920734295894846096271513596362
assert func16(81119, 1375780239273875130104284840151075796433493450132783529632293) == 237152024627603840582948691673128278967067381417506528616580
```

今回は大きな数字でも $d$ が高速に計算できている

##ex17 RSA暗号（復号）

以上のことを踏まえて、復号関数を作成せよ

ただし、暗号文Cを平文Mに復号する関数は次のとおり

>$M = C^d ~ mod ~ N$

また、秘密鍵 $d$ はex16の結果を利用すると計算できて

```python
p = 1223608816783989526977697142729
q = 1181419059721907087891844374533
e = 65537

func16(e, (p-1)*(q-1))
```

    904012334605531925578889401627410272401142288068641359129345

だから

```python
N = 1445594777792376190871161299886642462542562458520846233720557
d = 904012334605531925578889401627410272401142288068641359129345

def func17(c):
    return pow(c, d, N)
```

テストコード

```python
assert func17(933724396887774009673956822287392949344951453324424927225843) == 384674389155590982046234764697110782161450132844
assert func17(96166222195780378773068570820947104808959039543354452361988) == 111630337030194331012420294446637731487233908630131
assert func17(259428661535051310743580900142038188754161976801949527257666) == 4291588144510244660379
assert func17(251132345489958486648793004527006641296516405032422636490685) == 71066104117138884767124985495
```

#Lesson5 RSA暗号③ 〜練習問題〜

##ex18 練習問題１

ある文字列を次のように暗号化した

```
print(func13(func11(func10("★★★★★★"))))
print(func13(func11(func10("■■■■■■"))))
print(func13(func11(func10("▶▶▶▶▶▶"))))

1003877171348949160374299553769977330726090328065100116050778
225647961907964819754026097296433612017266008485331217477479
159418251333432300444122281769765489025932051607611723427661
```

元の文字列を復号せよ

```python

```

##ex19 練習問題２

次のパラーメータでRSA暗号化した暗号文を復号する関数を作成せよ

```
p = 99280930794300920071382774999216354524467420720528123759974588765034074663876045012987508905100128519759656290036198160476599881414260590989088460633320720729553296458254250825593914337729366809693487561815522430135404840009054317649002737463174582770508303823068955868689982831714452911982071223716649802469
q = 100219261295560742240797249262241836429593803690430204413099950961916296187735281900162200626563247056460037491059010759261716848232442550563715171608821366962038603101580563641890422729098242769815241344092373705491169032776678296707049935771071818115217897562283195638162708850284681971687415194614231563837
e = 65537
```

```python
p = 99280930794300920071382774999216354524467420720528123759974588765034074663876045012987508905100128519759656290036198160476599881414260590989088460633320720729553296458254250825593914337729366809693487561815522430135404840009054317649002737463174582770508303823068955868689982831714452911982071223716649802469
q = 100219261295560742240797249262241836429593803690430204413099950961916296187735281900162200626563247056460037491059010759261716848232442550563715171608821366962038603101580563641890422729098242769815241344092373705491169032776678296707049935771071818115217897562283195638162708850284681971687415194614231563837
e = 65537
```

暗号文

```
c1 =  5993282892294268012337210646422460224942003682339814479302884022181417999114855972670712186545118220125662871519229929846646836121084304816649015386737099513475747449874780402119038224661422110585167929611841407240680162076463742265931238648499543704282525494309999633856049015070714887185020390388127522863038787024024971681295320738326637792964656596726338299837333044702246888328862024915305745845222013436536335920158802049683304981193846955698062770587725601229602815288132137856123708800724991975252889579775780660852492871832071628983023169299866211503370369088572285358877954624525621213978189430281298032646
c2 = 2336409511515301298547613713805669698248801677012087786057830501685650092538851986158769315867383717531880893251879509173724747927988791995516999608829600380697658493739580397309777848549361780394898290309951442037981802516497561444266938575272668862506762747187662132887820853363257749315976620660964024077325598852336723615518659537997457586921076734858772380546261905233601169670405081331396013237534887055495226043808789346604646225739916037582803663028904004003447242618912882161431435149901381963797730750982328072738593117026750663183875159808706807269624132799819971700339717155999455769309288344415057241725
c3 = 7600801810617642582567941203777532865788859636265801290463357246340886777897267579251637555777501840347042749808043518617765984847892272013790181195503979654412547622527341949081816441318181628937923304654746983239808392198887768652839410205513907360928586204587597343609040675266749862522460124733481516513231755377734833191373671270070488620526463738692447142845303160936163560592238301973610095029089056932271401012158701771413934357230110456848626470814363473199265058838669921844384882825396553078297447253066680649265731945526822596942367105937910408865091393058285705680803783025716020959286434428891496923594
```

ただし、平文の文字符号化形式はUTF-8である

```python
def func19(c):
    d = func16(e, (p-1)*(q-1))
    m = pow(c, d, p*q)
    return func12(m)
```

テストコード

```python
print(func19(5993282892294268012337210646422460224942003682339814479302884022181417999114855972670712186545118220125662871519229929846646836121084304816649015386737099513475747449874780402119038224661422110585167929611841407240680162076463742265931238648499543704282525494309999633856049015070714887185020390388127522863038787024024971681295320738326637792964656596726338299837333044702246888328862024915305745845222013436536335920158802049683304981193846955698062770587725601229602815288132137856123708800724991975252889579775780660852492871832071628983023169299866211503370369088572285358877954624525621213978189430281298032646))
print(func19(2336409511515301298547613713805669698248801677012087786057830501685650092538851986158769315867383717531880893251879509173724747927988791995516999608829600380697658493739580397309777848549361780394898290309951442037981802516497561444266938575272668862506762747187662132887820853363257749315976620660964024077325598852336723615518659537997457586921076734858772380546261905233601169670405081331396013237534887055495226043808789346604646225739916037582803663028904004003447242618912882161431435149901381963797730750982328072738593117026750663183875159808706807269624132799819971700339717155999455769309288344415057241725))
print(func19(7600801810617642582567941203777532865788859636265801290463357246340886777897267579251637555777501840347042749808043518617765984847892272013790181195503979654412547622527341949081816441318181628937923304654746983239808392198887768652839410205513907360928586204587597343609040675266749862522460124733481516513231755377734833191373671270070488620526463738692447142845303160936163560592238301973610095029089056932271401012158701771413934357230110456848626470814363473199265058838669921844384882825396553078297447253066680649265731945526822596942367105937910408865091393058285705680803783025716020959286434428891496923594))

```


```julia
using PyPlot, StatsBase, Printf, DelimitedFiles, Combinatorics;
using Revise;
using MDToolbox;
PyPlot.plt.style.use("seaborn-colorblind");
ENV["COLUMNS"] = 110; #display width for MDToolbox
```

    ┌ Warning: no Manifest.toml file found, static paths used
    └ @ Revise /Users/yasu/.julia/packages/Revise/Pcs5V/src/Revise.jl:1217

### Potential energy function

```julia
V(x; k=1.0) = sum(- (1.0 / 2.0) .* k .* x.^2 .+ (1.0 ./ 4.0) .* k .* x.^4)
V([0.0])
```

    0.0

### Proposal distribution

```julia
proposal_fun(x) = x .+ (rand(Float64, size(x)) .* 2.0 .- 1.0);
proposal_fun([0.0])
```

    1-element Array{Float64,1}:
     -0.5139476455371819

### Define functions for infinite swapping

温度Tにおけるxの分布は、以下のπに比例する

\begin{align}
\pi({\bf x},T) = e^{-V({\bf x})/T}
\end{align}

```julia
pai(x, T) = exp.(-V(x) ./ T)
```

    pai (generic function with 1 method)

ρ_n(X)はある温度における期待値を求める際の「重み」のようなもの

\begin{align}
\rho_n({\bf X}) = \frac{P_n \left[ \pi({\bf x}_1, T_1) \pi({\bf x}_2, T_2) \ldots \pi({\bf x}_N, T_N) \right]}{\mu({\bf X})}
\end{align}

ここでμ(X)はsymmetrized distribution

\begin{align}
\mu({\bf X})  = \sum_{n=1}^{N!} P_n \left[ \pi({\bf x}_1, T_1) \pi({\bf x}_2, T_2) \ldots \pi({\bf x}_N, T_N) \right]
\end{align}

物理量A(x)の温度T_kにおける期待値は以下のように求められる

\begin{align}
< A >_k = \frac{\int \mu({\bf X}) \left[ \sum_{n=1}^{N!}\rho_n({\bf X}) A({\bf x}_{indx(n,k)}) \right] d{\bf X}}{\int \mu({\bf X}) d{\bf X}}
\end{align}

これを時間平均でもとめると
\begin{align}
< A >_k = \frac{1}{M} \sum_{m=0}^{M} \sum_{n=1}^{N!}\rho_n({\bf X}_m) A({\bf x}_{indx(n,k),m})
\end{align}

```julia
function calc_rho!(rho, x, T, perm)
    nreplica = length(x)
    nperm = length(rho)
    mu = 0.0
    for n = 1:nperm
        rho[n] = 0.0
        rho[n] = pai(x[1], T[perm[n][1]])
        for i = 2:nreplica
            rho[n] *= pai(x[i], T[perm[n][i]])
        end
        mu += rho[n]
    end
    rho .= rho./mu
    return rho
end
```

    calc_rho! (generic function with 1 method)

```julia
function select_n(rho)
    rho_cumsum = cumsum(rho)
    r = rand()
    n = sum(r .> rho_cumsum ) + 1
    return n
end
```

    select_n (generic function with 1 method)

### Infinite swap MCMC

```julia
function flush_weight(io::IOStream, m, rho, perm)
    nperm = length(rho)
    nreplica = length(perm[1])
    weight = zeros(Float64, nreplica)

    for n = 1:nperm
        #id_replica = perm[n][m]
        id_replica = findall(iszero, perm[n] .- m)[1]
        weight[id_replica] += rho[n]
    end

    for i = 1:nreplica
        @printf(io, "%f ", weight[i])
    end
    @printf(io, "\n")
end
```

    flush_weight (generic function with 1 method)

```julia
nreplica = 4
temperature_replica = [0.01, 0.10, 0.30, 0.40];
nstep = 2000;
perm=collect(permutations(1:nreplica))
nperm=factorial(nreplica)

x_replica = []
for i = 1:nreplica
    x = [0.0]
    push!(x_replica, x)
end

io_replica = []
for i = 1:nreplica
    filename = "replica$(i).dat"
    io = open(filename, "w")
    push!(io_replica, io)
end

io_weight=[]
for m = 1:nreplica
    filename = "weight$(m).dat"
    io = open(filename, "w")
    push!(io_weight, io)
end
```

```julia
rho=ones(Float64, nperm)
calc_rho!(rho, x_replica, temperature_replica, perm)

icount = 0
for istep = 1:nstep
    n = select_n(rho)
    for i = 1:nreplica
        x_replica[i] = propagate_mcmc(V, proposal_fun, x_replica[i], temperature_replica[perm[n][i]], nstep=1, io=io_replica[i]);
    end
    calc_rho!(rho, x_replica, temperature_replica, perm)

    for m = 1:nreplica
        flush_weight(io_weight[m], m, rho, perm)
    end
end
```

```julia
for i = 1:nreplica
    close(io_replica[i])
    close(io_weight[i])
end
```

### Trajectory analysis

```julia
traj_replica = []
temp_replica = []

for i = 1:nreplica
    filename = "replica$(i).dat"
    data = readdlm(filename);
    push!(temp_replica, data[:, 1])
    push!(traj_replica, data[:, 2])
end
```

```julia
fig, ax = subplots(figsize=(8, 6))
for i = 1:nreplica
    ax.plot(traj_replica[i], linewidth=0.5)
end
xlabel("step",fontsize=20)
ylabel("x(step)",fontsize=20)

ax.legend(["replica 1", "replica 2", "replica 3", "replica 4"])
tight_layout()
```

```julia
filename = "weight1.dat"
weight_replica = readdlm(filename);
```

```julia
traj = traj_replica[1]
weight = weight_replica[:, 1]

for i = 2:nreplica
    traj = [traj; traj_replica[i]]
    weight = [weight; weight_replica[:, i]]
end
weight .= weight ./ sum(weight)
```

    8000-element Array{Float64,1}:
     3.555000095985003e-7
     3.7100001001700033e-7
     1.8700000504900013e-7
     2.5000000675000023e-9
     3.640650098297553e-5
     0.0
     0.00023313300629459118
     0.0001440115038883106
     0.00031263800844122624
     0.00023388850631498968
     9.676500261265507e-5
     0.0005000000135000004
     0.00010968650296153559
     ⋮
     0.0002474095066800567
     0.0002474720066817442
     4.235000114345004e-6
     4.235000114345004e-6
     5.035000135945004e-6
     4.916000132732004e-6
     5.1425001388475045e-6
     5.1600001393200035e-6
     0.00010980000296460008
     0.0001377040037180081
     0.0001371970037043191
     2.556300069020102e-5

```julia
x_grid = range(-1.3, 1.3, length=100);
pmf_theory = V.(x_grid) ./ temperature_replica[1]
pmf_theory .= pmf_theory .- minimum(pmf_theory);
```

```julia
#pmf_observed, _ = getpmf(traj_replica[1], weight=weight_replica[:, 1], grid_x = collect(x_grid), bandwidth=0.05);
pmf_observed, _ = getpmf(traj, weight=weight, grid_x = collect(x_grid), bandwidth=0.05);
```

```julia
fig, ax = subplots(figsize=(8, 6))
ax.plot(x_grid, pmf_theory, linewidth=3)
xlabel("x",fontsize=20)
ylabel("PMF (KBT)",fontsize=20)

ax.plot(x_grid, pmf_observed, linewidth=3)

ax.legend(["theory", "observed"])

ax.xaxis.set_tick_params(which="major",labelsize=15)
ax.yaxis.set_tick_params(which="major",labelsize=15)
ax.grid(linestyle="--", linewidth=0.5)
tight_layout()
savefig("mcmc_infinite_swap.png", dpi=350)
```

```julia

```


# Optimizing DiffEq Code
### Chris Rackauckas

In this notebook we will walk through some of the main tools for optimizing your code in order to efficiently solve DifferentialEquations.jl. User-side optimizations are important because, for sufficiently difficult problems, most of the time will be spent inside of your `f` function, the function you are trying to solve. "Efficient" integrators are those that reduce the required number of `f` calls to hit the error tolerance. The main ideas for optimizing your DiffEq code, or any Julia function, are the following:

- Make it non-allocating
- Use StaticArrays for small arrays
- Use broadcast fusion
- Make it type-stable
- Reduce redundant calculations
- Make use of BLAS calls
- Optimize algorithm choice

We'll discuss these strategies in the context of small and large systems. Let's start with small systems.

## Optimizing Small Systems (<100 DEs)

Let's take the classic Lorenz system from before. Let's start by naively writing the system in its out-of-place form:

```julia
function lorenz(u,p,t)
 dx = 10.0*(u[2]-u[1])
 dy = u[1]*(28.0-u[3]) - u[2]
 dz = u[1]*u[2] - (8/3)*u[3]
 [dx,dy,dz]
end
```

Here, `lorenz` returns an object, `[dx,dy,dz]`, which is created within the body of `lorenz`.

This is a common code pattern from high-level languages like MATLAB, SciPy, or R's deSolve. However, the issue with this form is that it allocates a vector, `[dx,dy,dz]`, at each step. Let's benchmark the solution process with this choice of function:

```julia
using DifferentialEquations, BenchmarkTools
u0 = [1.0;0.0;0.0]
tspan = (0.0,100.0)
prob = ODEProblem(lorenz,u0,tspan)
@benchmark solve(prob,Tsit5())
```

The BenchmarkTools package's `@benchmark` runs the code multiple times to get an accurate measurement. The minimum time is the time it takes when your OS and other background processes aren't getting in the way. Notice that in this case it takes about 5ms to solve and allocates around 11.11 MiB. However, if we were to use this inside of a real user code we'd see a lot of time spent doing garbage collection (GC) to clean up all of the arrays we made. Even if we turn off saving we have these allocations.

```julia
@benchmark solve(prob,Tsit5(),save_everystep=false)
```

The problem of course is that arrays are created every time our derivative function is called. This function is called multiple times per step and is thus the main source of memory usage. To fix this, we can use the in-place form to ***make our code non-allocating***:

```julia
function lorenz!(du,u,p,t)
 du[1] = 10.0*(u[2]-u[1])
 du[2] = u[1]*(28.0-u[3]) - u[2]
 du[3] = u[1]*u[2] - (8/3)*u[3]
end
```

Here, instead of creating an array each time, we utilized the cache array `du`. When the inplace form is used, DifferentialEquations.jl takes a different internal route that minimizes the internal allocations as well. When we benchmark this function, we will see quite a difference.

```julia
u0 = [1.0;0.0;0.0]
tspan = (0.0,100.0)
prob = ODEProblem(lorenz!,u0,tspan)
@benchmark solve(prob,Tsit5())
```

```julia
@benchmark solve(prob,Tsit5(),save_everystep=false)
```

There is a 4x time difference just from that change! Notice there are still some allocations and this is due to the construction of the integration cache. But this doesn't scale with the problem size:

```julia
tspan = (0.0,500.0) # 5x longer than before
prob = ODEProblem(lorenz!,u0,tspan)
@benchmark solve(prob,Tsit5(),save_everystep=false)
```

since that's all just setup allocations.

#### But if the system is small we can optimize even more.

Allocations are only expensive if they are "heap allocations". For a more in-depth definition of heap allocations, [there are a lot of sources online](http://net-informations.com/faq/net/stack-heap.htm). But a good working definition is that heap allocations are variable-sized slabs of memory which have to be pointed to, and this pointer indirection costs time. Additionally, the heap has to be managed and the garbage controllers has to actively keep track of what's on the heap.

However, there's an alternative to heap allocations, known as stack allocations. The stack is statically-sized (known at compile time) and thus its accesses are quick. Additionally, the exact block of memory is known in advance by the compiler, and thus re-using the memory is cheap. This means that allocating on the stack has essentially no cost!

Arrays have to be heap allocated because their size (and thus the amount of memory they take up) is determined at runtime. But there are structures in Julia which are stack-allocated. `struct`s for example are stack-allocated "value-type"s. `Tuple`s are a stack-allocated collection. The most useful data structure for DiffEq though is the `StaticArray` from the package [StaticArrays.jl](https://github.com/JuliaArrays/StaticArrays.jl). These arrays have their length determined at compile-time. They are created using macros attached to normal array expressions, for example:

```julia
using StaticArrays
A = @SVector [2.0,3.0,5.0]
```

Notice that the `3` after `SVector` gives the size of the `SVector`. It cannot be changed. Additionally, `SVector`s are immutable, so we have to create a new `SVector` to change values. But remember, we don't have to worry about allocations because this data structure is stack-allocated. `SArray`s have a lot of extra optimizations as well: they have fast matrix multiplication, fast QR factorizations, etc. which directly make use of the information about the size of the array. Thus, when possible they should be used.

Unfortunately static arrays can only be used for sufficiently small arrays. After a certain size, they are forced to heap allocate after some instructions and their compile time balloons. Thus static arrays shouldn't be used if your system has more than 100 variables. Additionally, only the native Julia algorithms can fully utilize static arrays.

Let's ***optimize `lorenz` using static arrays***. Note that in this case, we want to use the out-of-place allocating form, but this time we want to output a static array:

```julia
function lorenz_static(u,p,t)
 dx = 10.0*(u[2]-u[1])
 dy = u[1]*(28.0-u[3]) - u[2]
 dz = u[1]*u[2] - (8/3)*u[3]
 @SVector [dx,dy,dz]
end
```

To make the solver internally use static arrays, we simply give it a static array as the initial condition:

```julia
u0 = @SVector [1.0,0.0,0.0]
tspan = (0.0,100.0)
prob = ODEProblem(lorenz_static,u0,tspan)
@benchmark solve(prob,Tsit5())
```

```julia
@benchmark solve(prob,Tsit5(),save_everystep=false)
```

And that's pretty much all there is to it. With static arrays you don't have to worry about allocating, so use operations like `*` and don't worry about fusing operations (discussed in the next section). Do "the vectorized code" of R/MATLAB/Python and your code in this case will be fast, or directly use the numbers/values.

#### Exercise 1

Implement the out-of-place array, in-place array, and out-of-place static array forms for the [Henon-Heiles System](https://en.wikipedia.org/wiki/H%C3%A9non%E2%80%93Heiles_system) and time the results.

## Optimizing Large Systems

### Interlude: Managing Allocations with Broadcast Fusion

When your system is sufficiently large, or you have to make use of a non-native Julia algorithm, you have to make use of `Array`s. In order to use arrays in the most efficient manner, you need to be careful about temporary allocations. Vectorized calculations naturally have plenty of temporary array allocations. This is because a vectorized calculation outputs a vector. Thus:

```julia
A = rand(1000,1000); B = rand(1000,1000); C = rand(1000,1000)
test(A,B,C) = A + B + C
@benchmark test(A,B,C)
```

That expression `A + B + C` creates 2 arrays. It first creates one for the output of `A + B`, then uses that result array to `+ C` to get the final result. 2 arrays! We don't want that! The first thing to do to fix this is to use broadcast fusion. [Broadcast fusion](https://julialang.org/blog/2017/01/moredots) puts expressions together. For example, instead of doing the `+` operations separately, if we were to add them all at the same time, then we would only have a single array that's created. For example:

```julia
test2(A,B,C) = map((a,b,c)->a+b+c,A,B,C)
@benchmark test2(A,B,C)
```

Puts the whole expression into a single function call, and thus only one array is required to store output. This is the same as writing the loop:

```julia
function test3(A,B,C)
    D = similar(A)
    @inbounds for i in eachindex(A)
        D[i] = A[i] + B[i] + C[i]
    end
    D
end
@benchmark test3(A,B,C)
```

However, Julia's broadcast is syntactic sugar for this. If multiple expressions have a `.`, then it will put those vectorized operations together. Thus:

```julia
test4(A,B,C) = A .+ B .+ C
@benchmark test4(A,B,C)
```

is a version with only 1 array created (the output). Note that `.`s can be used with function calls as well:

```julia
sin.(A) .+ sin.(B)
```

Also, the `@.` macro applys a dot to every operator:

```julia
test5(A,B,C) = @. A + B + C #only one array allocated
@benchmark test5(A,B,C)
```

Using these tools we can get rid of our intermediate array allocations for many vectorized function calls. But we are still allocating the output array. To get rid of that allocation, we can instead use mutation. Mutating broadcast is done via `.=`. For example, if we pre-allocate the output:

```julia
D = zeros(1000,1000);
```

Then we can keep re-using this cache for subsequent calculations. The mutating broadcasting form is:

```julia
test6!(D,A,B,C) = D .= A .+ B .+ C #only one array allocated
@benchmark test6!(D,A,B,C)
```

If we use `@.` before the `=`, then it will turn it into `.=`:

```julia
test7!(D,A,B,C) = @. D = A + B + C #only one array allocated
@benchmark test7!(D,A,B,C)
```

Notice that in this case, there is no "output", and instead the values inside of `D` are what are changed (like with the DiffEq inplace function). Many Julia functions have a mutating form which is denoted with a `!`. For example, the mutating form of the `map` is `map!`:

```julia
test8!(D,A,B,C) = map!((a,b,c)->a+b+c,D,A,B,C)
@benchmark test8!(D,A,B,C)
```

Some operations require using an alternate mutating form in order to be fast. For example, matrix multiplication via `*` allocates a temporary:

```julia
@benchmark A*B
```

Instead, we can use the mutating form `mul!` into a cache array to avoid allocating the output:

```julia
using LinearAlgebra
@benchmark mul!(D,A,B) # same as D = A * B
```

For repeated calculations this reduced allocation can stop GC cycles and thus lead to more efficient code. Additionally, ***we can fuse together higher level linear algebra operations using BLAS***. The package [SugarBLAS.jl](https://github.com/lopezm94/SugarBLAS.jl) makes it easy to write higher level operations like `alpha*B*A + beta*C` as mutating BLAS calls.

### Example Optimization: Gierer-Meinhardt Reaction-Diffusion PDE Discretization

Let's optimize the solution of a Reaction-Diffusion PDE's discretization. In its discretized form, this is the ODE:

$$
\begin{align}
du &= D_1 (A_y u + u A_x) + \frac{au^2}{v} + \bar{u} - \alpha u\\
dv &= D_2 (A_y v + v A_x) + a u^2 + \beta v
\end{align}
$$

where $u$, $v$, and $A$ are matrices. Here, we will use the simplified version where $A$ is the tridiagonal stencil $[1,-2,1]$, i.e. it's the 2D discretization of the LaPlacian. The native code would be something along the lines of:

```julia
# Generate the constants
p = (1.0,1.0,1.0,10.0,0.001,100.0) # a,α,ubar,β,D1,D2
N = 100
Ax = Array(Tridiagonal([1.0 for i in 1:N-1],[-2.0 for i in 1:N],[1.0 for i in 1:N-1]))
Ay = copy(Ax)
Ax[2,1] = 2.0
Ax[end-1,end] = 2.0
Ay[1,2] = 2.0
Ay[end,end-1] = 2.0

function basic_version!(dr,r,p,t)
  a,α,ubar,β,D1,D2 = p
  u = r[:,:,1]
  v = r[:,:,2]
  Du = D1*(Ay*u + u*Ax)
  Dv = D2*(Ay*v + v*Ax)
  dr[:,:,1] = Du .+ a.*u.*u./v .+ ubar .- α*u
  dr[:,:,2] = Dv .+ a.*u.*u .- β*v
end

a,α,ubar,β,D1,D2 = p
uss = (ubar+β)/α
vss = (a/β)*uss^2
r0 = zeros(100,100,2)
r0[:,:,1] .= uss.+0.1.*rand.()
r0[:,:,2] .= vss

prob = ODEProblem(basic_version!,r0,(0.0,0.1),p)
```

In this version we have encoded our initial condition to be a 3-dimensional array, with `u[:,:,1]` being the `A` part and `u[:,:,2]` being the `B` part.

```julia
@benchmark solve(prob,Tsit5())
```

While this version isn't very efficient,

#### We recommend writing the "high-level" code first, and iteratively optimizing it!

The first thing that we can do is get rid of the slicing allocations. The operation `r[:,:,1]` creates a temporary array instead of a "view", i.e. a pointer to the already existing memory. To make it a view, add `@view`. Note that we have to be careful with views because they point to the same memory, and thus changing a view changes the original values:

```julia
A = rand(4)
@show A
B = @view A[1:3]
B[2] = 2
@show A
```

Notice that changing `B` changed `A`. This is something to be careful of, but at the same time we want to use this since we want to modify the output `dr`. Additionally, the last statement is a purely element-wise operation, and thus we can make use of broadcast fusion there. Let's rewrite `basic_version!` to ***avoid slicing allocations*** and to ***use broadcast fusion***:

```julia
function gm2!(dr,r,p,t)
  a,α,ubar,β,D1,D2 = p
  u = @view r[:,:,1]
  v = @view r[:,:,2]
  du = @view dr[:,:,1]
  dv = @view dr[:,:,2]
  Du = D1*(Ay*u + u*Ax)
  Dv = D2*(Ay*v + v*Ax)
  @. du = Du + a.*u.*u./v + ubar - α*u
  @. dv = Dv + a.*u.*u - β*v
end
prob = ODEProblem(gm2!,r0,(0.0,0.1),p)
@benchmark solve(prob,Tsit5())
```

Now, most of the allocations are taking place in `Du = D1*(Ay*u + u*Ax)` since those operations are vectorized and not mutating. We should instead replace the matrix multiplications with `mul!`. When doing so, we will need to have cache variables to write into. This looks like:

```julia
Ayu = zeros(N,N)
uAx = zeros(N,N)
Du = zeros(N,N)
Ayv = zeros(N,N)
vAx = zeros(N,N)
Dv = zeros(N,N)
function gm3!(dr,r,p,t)
  a,α,ubar,β,D1,D2 = p
  u = @view r[:,:,1]
  v = @view r[:,:,2]
  du = @view dr[:,:,1]
  dv = @view dr[:,:,2]
  mul!(Ayu,Ay,u)
  mul!(uAx,u,Ax)
  mul!(Ayv,Ay,v)
  mul!(vAx,v,Ax)
  @. Du = D1*(Ayu + uAx)
  @. Dv = D2*(Ayv + vAx)
  @. du = Du + a*u*u./v + ubar - α*u
  @. dv = Dv + a*u*u - β*v
end
prob = ODEProblem(gm3!,r0,(0.0,0.1),p)
@benchmark solve(prob,Tsit5())
```

But our temporary variables are global variables. We need to either declare the caches as `const` or localize them. We can localize them by adding them to the parameters, `p`. It's easier for the compiler to reason about local variables than global variables. ***Localizing variables helps to ensure type stability***.

```julia
p = (1.0,1.0,1.0,10.0,0.001,100.0,Ayu,uAx,Du,Ayv,vAx,Dv) # a,α,ubar,β,D1,D2
function gm4!(dr,r,p,t)
  a,α,ubar,β,D1,D2,Ayu,uAx,Du,Ayv,vAx,Dv = p
  u = @view r[:,:,1]
  v = @view r[:,:,2]
  du = @view dr[:,:,1]
  dv = @view dr[:,:,2]
  mul!(Ayu,Ay,u)
  mul!(uAx,u,Ax)
  mul!(Ayv,Ay,v)
  mul!(vAx,v,Ax)
  @. Du = D1*(Ayu + uAx)
  @. Dv = D2*(Ayv + vAx)
  @. du = Du + a*u*u./v + ubar - α*u
  @. dv = Dv + a*u*u - β*v
end
prob = ODEProblem(gm4!,r0,(0.0,0.1),p)
@benchmark solve(prob,Tsit5())
```

We could then use the BLAS `gemmv` to optimize the matrix multiplications some more, but instead let's devectorize the stencil.

```julia
p = (1.0,1.0,1.0,10.0,0.001,100.0,N)
function fast_gm!(du,u,p,t)
  a,α,ubar,β,D1,D2,N = p

  @inbounds for j in 2:N-1, i in 2:N-1
    du[i,j,1] = D1*(u[i-1,j,1] + u[i+1,j,1] + u[i,j+1,1] + u[i,j-1,1] - 4u[i,j,1]) +
              a*u[i,j,1]^2/u[i,j,2] + ubar - α*u[i,j,1]
  end

  @inbounds for j in 2:N-1, i in 2:N-1
    du[i,j,2] = D2*(u[i-1,j,2] + u[i+1,j,2] + u[i,j+1,2] + u[i,j-1,2] - 4u[i,j,2]) +
            a*u[i,j,1]^2 - β*u[i,j,2]
  end

  @inbounds for j in 2:N-1
    i = 1
    du[1,j,1] = D1*(2u[i+1,j,1] + u[i,j+1,1] + u[i,j-1,1] - 4u[i,j,1]) +
            a*u[i,j,1]^2/u[i,j,2] + ubar - α*u[i,j,1]
  end
  @inbounds for j in 2:N-1
    i = 1
    du[1,j,2] = D2*(2u[i+1,j,2] + u[i,j+1,2] + u[i,j-1,2] - 4u[i,j,2]) +
            a*u[i,j,1]^2 - β*u[i,j,2]
  end
  @inbounds for j in 2:N-1
    i = N
    du[end,j,1] = D1*(2u[i-1,j,1] + u[i,j+1,1] + u[i,j-1,1] - 4u[i,j,1]) +
           a*u[i,j,1]^2/u[i,j,2] + ubar - α*u[i,j,1]
  end
  @inbounds for j in 2:N-1
    i = N
    du[end,j,2] = D2*(2u[i-1,j,2] + u[i,j+1,2] + u[i,j-1,2] - 4u[i,j,2]) +
           a*u[i,j,1]^2 - β*u[i,j,2]
  end

  @inbounds for i in 2:N-1
    j = 1
    du[i,1,1] = D1*(u[i-1,j,1] + u[i+1,j,1] + 2u[i,j+1,1] - 4u[i,j,1]) +
              a*u[i,j,1]^2/u[i,j,2] + ubar - α*u[i,j,1]
  end
  @inbounds for i in 2:N-1
    j = 1
    du[i,1,2] = D2*(u[i-1,j,2] + u[i+1,j,2] + 2u[i,j+1,2] - 4u[i,j,2]) +
              a*u[i,j,1]^2 - β*u[i,j,2]
  end
  @inbounds for i in 2:N-1
    j = N
    du[i,end,1] = D1*(u[i-1,j,1] + u[i+1,j,1] + 2u[i,j-1,1] - 4u[i,j,1]) +
             a*u[i,j,1]^2/u[i,j,2] + ubar - α*u[i,j,1]
  end
  @inbounds for i in 2:N-1
    j = N
    du[i,end,2] = D2*(u[i-1,j,2] + u[i+1,j,2] + 2u[i,j-1,2] - 4u[i,j,2]) +
             a*u[i,j,1]^2 - β*u[i,j,2]
  end

  @inbounds begin
    i = 1; j = 1
    du[1,1,1] = D1*(2u[i+1,j,1] + 2u[i,j+1,1] - 4u[i,j,1]) +
              a*u[i,j,1]^2/u[i,j,2] + ubar - α*u[i,j,1]
    du[1,1,2] = D2*(2u[i+1,j,2] + 2u[i,j+1,2] - 4u[i,j,2]) +
              a*u[i,j,1]^2 - β*u[i,j,2]

    i = 1; j = N
    du[1,N,1] = D1*(2u[i+1,j,1] + 2u[i,j-1,1] - 4u[i,j,1]) +
             a*u[i,j,1]^2/u[i,j,2] + ubar - α*u[i,j,1]
    du[1,N,2] = D2*(2u[i+1,j,2] + 2u[i,j-1,2] - 4u[i,j,2]) +
             a*u[i,j,1]^2 - β*u[i,j,2]

    i = N; j = 1
    du[N,1,1] = D1*(2u[i-1,j,1] + 2u[i,j+1,1] - 4u[i,j,1]) +
             a*u[i,j,1]^2/u[i,j,2] + ubar - α*u[i,j,1]
    du[N,1,2] = D2*(2u[i-1,j,2] + 2u[i,j+1,2] - 4u[i,j,2]) +
             a*u[i,j,1]^2 - β*u[i,j,2]

    i = N; j = N
    du[end,end,1] = D1*(2u[i-1,j,1] + 2u[i,j-1,1] - 4u[i,j,1]) +
             a*u[i,j,1]^2/u[i,j,2] + ubar - α*u[i,j,1]
    du[end,end,2] = D2*(2u[i-1,j,2] + 2u[i,j-1,2] - 4u[i,j,2]) +
             a*u[i,j,1]^2 - β*u[i,j,2]
   end
end
prob = ODEProblem(fast_gm!,r0,(0.0,0.1),p)
@benchmark solve(prob,Tsit5())
```

Lastly, we can do other things like multithread the main loops, but these optimizations get the last 2x-3x out. The main optimizations which apply everywhere are the ones we just performed (though the last one only works if your matrix is a stencil. This is known as a matrix-free implementation of the PDE discretization).

This gets us to about 8x faster than our original MATLAB/SciPy/R vectorized style code!

The last thing to do is then ***optimize our algorithm choice***. We have been using `Tsit5()` as our test algorithm, but in reality this problem is a stiff PDE discretization and thus one recommendation is to use `CVODE_BDF()`. However, instead of using the default dense Jacobian, we should make use of the sparse Jacobian afforded by the problem. The Jacobian is the matrix $\frac{df_i}{dr_j}$, where $r$ is read by the linear index (i.e. down columns). But since the $u$ variables depend on the $v$, the band size here is large, and thus this will not do well with a Banded Jacobian solver. Instead, we utilize sparse Jacobian algorithms. `CVODE_BDF` allows us to use a sparse Newton-Krylov solver by setting `linear_solver = :GMRES` (see [the solver documentation](https://docs.juliadiffeq.org/dev/solvers/ode_solve/#ode_solve_sundials-1), and thus we can solve this problem efficiently. Let's see how this scales as we increase the integration time.

```julia
prob = ODEProblem(fast_gm!,r0,(0.0,10.0),p)
@benchmark solve(prob,Tsit5())
```

```julia
using Sundials
@benchmark solve(prob,CVODE_BDF(linear_solver=:GMRES))
```

```julia
prob = ODEProblem(fast_gm!,r0,(0.0,100.0),p)
# Will go out of memory if we don't turn off `save_everystep`!
@benchmark solve(prob,Tsit5(),save_everystep=false)
```

```julia
@benchmark solve(prob,CVODE_BDF(linear_solver=:GMRES))
```

Now let's check the allocation growth.

```julia
@benchmark solve(prob,CVODE_BDF(linear_solver=:GMRES),save_everystep=false)
```

```julia
prob = ODEProblem(fast_gm!,r0,(0.0,500.0),p)
@benchmark solve(prob,CVODE_BDF(linear_solver=:GMRES),save_everystep=false)
```

Notice that we've elimated almost all allocations, allowing the code to grow without hitting garbage collection and slowing down.

Why is `CVODE_BDF` doing well? What's happening is that, because the problem is stiff, the number of steps required by the explicit Runge-Kutta method grows rapidly, whereas `CVODE_BDF` is taking large steps. Additionally, the `GMRES` linear solver form is quite an efficient way to solve the implicit system in this case. This is problem-dependent, and in many cases using a Krylov method effectively requires a preconditioner, so you need to play around with testing other algorithms and linear solvers to find out what works best with your problem.

## Conclusion

Julia gives you the tools to optimize the solver "all the way", but you need to make use of it. The main thing to avoid is temporary allocations. For small systems, this is effectively done via static arrays. For large systems, this is done via in-place operations and cache arrays. Either way, the resulting solution can be immensely sped up over vectorized formulations by using these principles.


```python
import sys
sys.path.append('../../')
import os
import dill

import numpy as np
import scipy as sc
import random as rand
from sklearn import preprocessing, linear_model
import matplotlib.pyplot as plt

from core.controllers import ConstantController

from koopman_core.dynamics import LinearLiftedDynamics, BilinearLiftedDynamics
from koopman_core.learning import Edmd_aut, KoopDnn, KoopmanNetAut
```

## Autonomous system with analytic finite dimensional Koopman operator

Consider the continuous-time dynamics

\begin{equation}
    x = \begin{bmatrix} x_1\\x_2\\\dot{x}_1\\ \dot{x}_2 \end{bmatrix}, \qquad
    \begin{bmatrix} \dot{x}_1 \\ \dot{x}_2 \\ \dot{x}_3 \\ \dot{x}_4 \end{bmatrix}
    = \begin{bmatrix}
    x_3 \\
    x_4 \\
    \mu x_3\\
    -\lambda x_3^2 + \lambda x_4
    \end{bmatrix}
\end{equation}
by carefully choosing observables, the drift vector field of the dynamics can be reformulated as an equilvalent linear system (global linearization). Define the observables

\begin{equation}
\begin{bmatrix}
y_1 \\ y_2 \\ y_3 \\ y_4 \\ y_5
\end{bmatrix}
= \begin{bmatrix}
x_1 \\ x_2 \\ x_3 \\ x_4 \\ x_3^2
\end{bmatrix}
\end{equation}

then the system can be rewritten as

\begin{equation}
\begin{bmatrix}
\dot{y}_1 \\ \dot{y}_2 \\ \dot{y}_3 \\ \dot{y}_4 \\ \dot{y}_5 \end{bmatrix} =
\begin{bmatrix}
0 & 0 & 1 & 0 & 0\\
0 & 0 & 0 & 1 & 0\\
0 & 0 & \mu & 0 & 0\\
0 & 0 & 0 & \lambda & - \lambda \\
0 & 0 & 0 & 0 & \mu \end{bmatrix}
\begin{bmatrix}
y_1 \\ y_2 \\ y_3 \\ y_4 \\ y_5 \end{bmatrix}
\end{equation}

```python
from core.dynamics import SystemDynamics

class FiniteKoopSys(SystemDynamics):
    def __init__(self, mu, lambd):
        SystemDynamics.__init__(self, 4, 0)
        self.params = mu, lambd

    def drift(self, x, t):
        mu, lambd = self.params

        return np.array([x[2], x[3], mu*x[2], -lambd*x[2]**2 + lambd*x[3]])

    def eval_dot(self, x, u, t):
        return self.drift(x, t)

n, m = 4, 0
mu, lambd = -0.3, -0.6
system = FiniteKoopSys(mu, lambd)
sys_name = 'analytic_koop_sys'
```

### Collect data for learning

To collect data, a nominal controller is designed with LQR on the dynamics's linearization around hover. However, any
controller can be used and the method does not require the knowledge of model's linearization. In addition, a
exploratory white noise is added to the controller to ensure that the data is sufficiently excited. Note that the system
is underactuated and that trajectory optimization is necessary to control the position of the vehicle. We use a
simplified trajectory generator based on a model predictive controller for the linearized dynamics. More careful design
of the desired trajectory may be necessary for more demanding applications and this is readily compatible with our method.

4 data sets are collected to evaluate the impact of signal and process noise:
- Nominal data set (no signal or process noise)
- Data set with signal noise
- Data set with process noise
- Data set with signal and process noise

```python
# Data collection parameters:
collect_data = True
dt = 1.0e-2                                                         # Time step length
traj_length_dc = 4.                                                 # Trajectory length, data collection
n_pred_dc = int(traj_length_dc/dt)                                  # Number of time steps, data collection
t_eval = dt * np.arange(n_pred_dc + 1)                              # Simulation time points
n_traj_train = 100                                                     # Number of trajectories to execute, data collection
n_traj_val = int(0.2*n_traj_train)

xmax = np.array([1, 1, 1, 1])                                             # State constraints, trajectory generation
xmin = -xmax
x0_max = xmax                                                       # Initial value limits
sub_sample_rate = 1                                                 # Rate to subsample data for training
n_cols = 10                                                         # Number of columns in training data plot
folder_plots = 'figures/'                                           # Path to save plots
directory = os.path.abspath("")                                     # Path to save learned models
```

```python
from koopman_core.util import run_experiment
if collect_data:
    xs_train, t_train = run_experiment(system, n, n_traj_train, n_pred_dc, t_eval, x0_max, plot_experiment_data=True)
    xs_val, t_val = run_experiment(system, n, n_traj_val, n_pred_dc, t_eval, x0_max, plot_experiment_data=True)

    data_list = [xs_train, t_train, n_traj_train, xs_val, t_val, n_traj_val]
    outfile = open(directory + '/data/' + sys_name + '_data.pickle', 'wb')
    dill.dump(data_list, outfile)
    outfile.close()
else:
    infile = open(directory + '/data/' + sys_name + '_data.pickle', 'rb')
    xs_train, t_train, n_traj_train, xs_val, t_val, n_traj_val = dill.load(infile)
    infile.close()
```

### Learn a lifted linear model with Koopman DNN

```python
## import dill, os, torch

load_tuned_params = False

if load_tuned_params:
    infile = open(os.path.abspath('') + '/data/analytic_koop_sys_best_params.pickle', 'rb')
    net_params, val_loss, test_loss, open_loop_mse, open_loop_std = dill.load(infile)
    infile.close()

else:
    net_params = {}
    net_params['state_dim'] = 4
    net_params['encoder_hidden_width'] = 100
    net_params['encoder_hidden_depth'] = 1
    net_params['encoder_output_dim'] = 5
    net_params['optimizer'] = 'adam'
    net_params['lr'] = 1e-3
    net_params['epochs'] = 100
    net_params['batch_size'] = 256
    net_params['lin_loss_penalty'] = 0.5
    net_params['l2_reg'] = 0
    net_params['l1_reg'] = 0
    net_params['activation_type'] = 'relu'
    net_params['first_obs_const'] = True
    net_params['override_kinematics'] = False
    net_params['override_C'] = False
    net_params['dt'] = dt

print(net_params)
```

    {'state_dim': 4, 'encoder_hidden_width': 100, 'encoder_hidden_depth': 1, 'encoder_output_dim': 5, 'optimizer': 'adam', 'lr': 0.001, 'epochs': 100, 'batch_size': 256, 'lin_loss_penalty': 0.5, 'l2_reg': 0, 'l1_reg': 0, 'activation_type': 'relu', 'first_obs_const': True, 'override_kinematics': False, 'override_C': False, 'dt': 0.01}

```python
from sklearn import preprocessing
from koopman_core.util import fit_standardizer

standardizer_kdnn = fit_standardizer(xs_train, preprocessing.StandardScaler(with_mean=False))
#standardizer_kdnn = None

net = KoopmanNetAut(net_params, standardizer_x=standardizer_kdnn)
model_koop_dnn = KoopDnn(net)
model_koop_dnn.set_datasets(xs_train, t_train, x_val=xs_val, t_val=t_val)
model_koop_dnn.model_pipeline(net_params, early_stop=False)
model_koop_dnn.construct_koopman_model()
sys_koop_dnn = LinearLiftedDynamics(model_koop_dnn.A, None, model_koop_dnn.C, model_koop_dnn.basis_encode,
                                    continuous_mdl=False, dt=dt, standardizer_x=standardizer_kdnn)
```

    Epoch   1: train loss: 35.40992355, validation loss: 3.26882026
    Epoch   2: train loss: 1.56158423, validation loss: 1.02168860
    Epoch   3: train loss: 0.80655497, validation loss: 0.70167576
    Epoch   4: train loss: 0.56227654, validation loss: 0.54597931
    Epoch   5: train loss: 0.43881264, validation loss: 0.45166230
    Epoch   6: train loss: 0.36680955, validation loss: 0.39235637
    Epoch   7: train loss: 0.32103419, validation loss: 0.35118330
    Epoch   8: train loss: 0.28698248, validation loss: 0.31469982
    Epoch   9: train loss: 0.25550216, validation loss: 0.27901266
    Epoch  10: train loss: 0.21792085, validation loss: 0.23417141
    Epoch  11: train loss: 0.16807830, validation loss: 0.17641270
    Epoch  12: train loss: 0.11039381, validation loss: 0.12058679
    Epoch  13: train loss: 0.06818499, validation loss: 0.08527222
    Epoch  14: train loss: 0.04846516, validation loss: 0.06711107
    Epoch  15: train loss: 0.03977470, validation loss: 0.05708525
    Epoch  16: train loss: 0.03454907, validation loss: 0.05133140
    Epoch  17: train loss: 0.03082135, validation loss: 0.04741736
    Epoch  18: train loss: 0.02796370, validation loss: 0.04419232
    Epoch  19: train loss: 0.02556530, validation loss: 0.04153837
    Epoch  20: train loss: 0.02348147, validation loss: 0.03916235
    Epoch  21: train loss: 0.02168990, validation loss: 0.03651086
    Epoch  22: train loss: 0.02015839, validation loss: 0.03412674
    Epoch  23: train loss: 0.01867562, validation loss: 0.03235700
    Epoch  24: train loss: 0.01741048, validation loss: 0.03025500
    Epoch  25: train loss: 0.01628786, validation loss: 0.02892634
    Epoch  26: train loss: 0.01528085, validation loss: 0.02733441
    Epoch  27: train loss: 0.01429125, validation loss: 0.02676978
    Epoch  28: train loss: 0.01334786, validation loss: 0.02504714
    Epoch  29: train loss: 0.01245847, validation loss: 0.02357509
    Epoch  30: train loss: 0.01165975, validation loss: 0.02167478
    Epoch  31: train loss: 0.01093394, validation loss: 0.02121401
    Epoch  32: train loss: 0.01025109, validation loss: 0.01982087
    Epoch  33: train loss: 0.00969805, validation loss: 0.01902705
    Epoch  34: train loss: 0.00913114, validation loss: 0.01914659
    Epoch  35: train loss: 0.00857810, validation loss: 0.01741436
    Epoch  36: train loss: 0.00810924, validation loss: 0.01703940
    Epoch  37: train loss: 0.00768422, validation loss: 0.01696984
    Epoch  38: train loss: 0.00726226, validation loss: 0.01554432
    Epoch  39: train loss: 0.00688268, validation loss: 0.01502428
    Epoch  40: train loss: 0.00648427, validation loss: 0.01437470
    Epoch  41: train loss: 0.00614577, validation loss: 0.01302346
    Epoch  42: train loss: 0.00582628, validation loss: 0.01280891
    Epoch  43: train loss: 0.00553037, validation loss: 0.01219395
    Epoch  44: train loss: 0.00524774, validation loss: 0.01138638
    Epoch  45: train loss: 0.00500216, validation loss: 0.01159217
    Epoch  46: train loss: 0.00477072, validation loss: 0.01036951
    Epoch  47: train loss: 0.00455023, validation loss: 0.01053043
    Epoch  48: train loss: 0.00430899, validation loss: 0.01048411
    Epoch  49: train loss: 0.00402427, validation loss: 0.00970747
    Epoch  50: train loss: 0.00390059, validation loss: 0.00911932
    Epoch  51: train loss: 0.00379409, validation loss: 0.00915665
    Epoch  52: train loss: 0.00367303, validation loss: 0.00840364
    Epoch  53: train loss: 0.00360072, validation loss: 0.00877010
    Epoch  54: train loss: 0.00331664, validation loss: 0.00799079
    Epoch  55: train loss: 0.00320440, validation loss: 0.00826608
    Epoch  56: train loss: 0.00298477, validation loss: 0.00735942
    Epoch  57: train loss: 0.00287640, validation loss: 0.00743897
    Epoch  58: train loss: 0.00270988, validation loss: 0.00689802
    Epoch  59: train loss: 0.00263420, validation loss: 0.00682455
    Epoch  60: train loss: 0.00243270, validation loss: 0.00647521
    Epoch  61: train loss: 0.00252979, validation loss: 0.00636839
    Epoch  62: train loss: 0.00225471, validation loss: 0.00589013
    Epoch  63: train loss: 0.00211117, validation loss: 0.00594142
    Epoch  64: train loss: 0.00197710, validation loss: 0.00559949
    Epoch  65: train loss: 0.00188390, validation loss: 0.00575612
    Epoch  66: train loss: 0.00175198, validation loss: 0.00537109
    Epoch  67: train loss: 0.00175088, validation loss: 0.00530688
    Epoch  68: train loss: 0.00172046, validation loss: 0.00528915
    Epoch  69: train loss: 0.00156987, validation loss: 0.00472683
    Epoch  70: train loss: 0.00152552, validation loss: 0.00464872
    Epoch  71: train loss: 0.00151564, validation loss: 0.00447944
    Epoch  72: train loss: 0.00149311, validation loss: 0.00469044
    Epoch  73: train loss: 0.00132268, validation loss: 0.00433559
    Epoch  74: train loss: 0.00136190, validation loss: 0.00435884
    Epoch  75: train loss: 0.00140774, validation loss: 0.00481901
    Epoch  76: train loss: 0.00124581, validation loss: 0.00375236
    Epoch  77: train loss: 0.00120807, validation loss: 0.00375814
    Epoch  78: train loss: 0.00118956, validation loss: 0.00375160
    Epoch  79: train loss: 0.00114103, validation loss: 0.00367617
    Epoch  80: train loss: 0.00117073, validation loss: 0.00350704
    Epoch  81: train loss: 0.00108789, validation loss: 0.00342412
    Epoch  82: train loss: 0.00113225, validation loss: 0.00378752
    Epoch  83: train loss: 0.00117191, validation loss: 0.00353329
    Epoch  84: train loss: 0.00097163, validation loss: 0.00314240
    Epoch  85: train loss: 0.00095673, validation loss: 0.00310225
    Epoch  86: train loss: 0.00094214, validation loss: 0.00282312
    Epoch  87: train loss: 0.00108762, validation loss: 0.00289164
    Epoch  88: train loss: 0.00097944, validation loss: 0.00281272
    Epoch  89: train loss: 0.00112335, validation loss: 0.00269484
    Epoch  90: train loss: 0.00086054, validation loss: 0.00263395
    Epoch  91: train loss: 0.00087003, validation loss: 0.00270316
    Epoch  92: train loss: 0.00091664, validation loss: 0.00259079
    Epoch  93: train loss: 0.00094248, validation loss: 0.00274955
    Epoch  94: train loss: 0.00087256, validation loss: 0.00274653
    Epoch  95: train loss: 0.00089141, validation loss: 0.00270404
    Epoch  96: train loss: 0.00079337, validation loss: 0.00259480
    Epoch  97: train loss: 0.00095896, validation loss: 0.00221076
    Epoch  98: train loss: 0.00073273, validation loss: 0.00209249
    Epoch  99: train loss: 0.00077297, validation loss: 0.00223075
    Epoch 100: train loss: 0.00082369, validation loss: 0.00218396
    Finished Training

```python
print(model_koop_dnn.net.loss_scaler_x, model_koop_dnn.net.loss_scaler_z)
print(model_koop_dnn.A)
print(model_koop_dnn.C)
```

    tensor([0.0036, 0.0029, 0.0031, 0.0060]) 0.004201584007578855
    [[ 1.0000000e+00  0.0000000e+00  0.0000000e+00  0.0000000e+00
       0.0000000e+00  0.0000000e+00]
     [ 2.6348929e-04  9.9692374e-01  5.5848900e-03 -2.7076707e-03
      -3.7039605e-03  1.5713881e-03]
     [-1.9400148e-03  3.5841730e-03  9.9449873e-01  3.3811578e-03
      -2.3257606e-04 -8.1557891e-04]
     [ 9.0241717e-04 -1.3419160e-03  1.4360358e-03  9.9990189e-01
       1.0841308e-03 -1.8250997e-03]
     [-2.7835306e-03  4.0333881e-03 -5.8428380e-03  3.9965254e-03
       9.9795526e-01 -6.7787385e-04]
     [ 2.0386602e-03 -4.9210279e-03  6.5359259e-03 -1.3853668e-03
       1.7506970e-04  9.9604613e-01]]
    [[ 0.094743    0.38280538  0.49119934  0.3101947   0.17463663  0.21336399]
     [-0.28912294  0.37039348  0.07711031 -0.49160424  0.0597033  -0.209025  ]
     [-0.1822299  -0.05199938  0.07441478  0.29072827 -0.4426105  -0.3873678 ]
     [-0.3268824   0.3219118  -0.23678386 -0.04721108 -0.6225603   0.659237  ]]

```python
train_loss = [l[0] for l in model_koop_dnn.train_loss_hist]
train_pred_loss = [l[1] for l in model_koop_dnn.train_loss_hist]
train_lin_loss = [l[2] for l in model_koop_dnn.train_loss_hist]
val_loss = [l[0] for l in model_koop_dnn.val_loss_hist]
val_pred_loss = [l[1] for l in model_koop_dnn.val_loss_hist]
val_lin_loss = [l[2] for l in model_koop_dnn.val_loss_hist]
epochs = np.arange(0, net_params['epochs'])

plt.figure(figsize=(15,8))
plt.plot(epochs, train_loss, color='tab:orange', label='Training loss')
plt.plot(epochs, train_pred_loss, '--', color='tab:orange', label='Training prediction loss')
plt.plot(epochs, train_lin_loss, ':', color='tab:orange', label='Training linear loss')
plt.plot(epochs, val_loss, color='tab:blue', label='Validation loss')
plt.plot(epochs, val_pred_loss, '--', color='tab:blue', label='Validation prediction loss')
plt.plot(epochs, val_lin_loss, ':', color='tab:blue', label='Validation linear loss')
plt.legend()
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.yscale('log')
plt.show()
```

### Learn a linear model with dynamic mode decomposition (DMD)

To compare our method with existing techniques, we first learn a linear state space model from data. This is dubbed dynamic mode decomposition. I.e. we use linear regression with LASSO regularization to learn an approximate linear model with model structure

\begin{equation}
    \mathbf{x}_{k+1} = A_{dmd} \mathbf{x}_k
\end{equation}

```python
#DMD parameters:
alpha_dmd = 5.4e-4
tune_mdl_dmd = False
```

```python
basis = lambda x: x
C_dmd = np.eye(n)

cv_dmd = linear_model.MultiTaskLassoCV(fit_intercept=False, n_jobs=-1, cv=3, selection='random')
standardizer_dmd = preprocessing.StandardScaler(with_mean=False)

optimizer_dmd = linear_model.MultiTaskLasso(alpha=alpha_dmd, fit_intercept=False, selection='random')
model_dmd = Edmd_aut(n, basis, n, n_traj_train, optimizer_dmd, cv=cv_dmd, standardizer=standardizer_dmd, C=C_dmd, first_obs_const=False, continuous_mdl=False, dt=dt)
X_dmd, y_dmd = model_dmd.process(xs_train, np.tile(t_train,(n_traj_train,1)), downsample_rate=sub_sample_rate)
model_dmd.fit(X_dmd, y_dmd, cv=tune_mdl_dmd)
sys_dmd = LinearLiftedDynamics(model_dmd.A, None, model_dmd.C, model_dmd.basis, continuous_mdl=False, dt=dt)
if tune_mdl_dmd:
    print('$\\alpha$ DMD: ',model_dmd.cv.alpha_)
```

### Learn a lifted linear model with extended dynamic mode decomposition (EDMD)

In addition, we compare our method with the current state of the art of Koopman based learning, the extended dynamic mode
decomposition. We use a dictionary of nonlinear functions $\boldsymbol{\phi(x)}$ to lift the state variables and learn a lifted state space model
of the dynamics. I.e. we first lift and then use linear regression with LASSO regularization to learn an approximate
lifted linear model with model structure

\begin{equation}
    \mathbf{z}_{k+1} = A_{edmd}\mathbf{z}_k, \qquad \mathbf{z} = \boldsymbol{\phi(x)}
\end{equation}

```python
#EDMD parameters:
alpha_edmd = 1.2e-4
tune_mdl_edmd = False
```

```python
koop_features = preprocessing.PolynomialFeatures(degree=2)
koop_features.fit(np.zeros((1,n)))
basis = lambda x: koop_features.transform(x)
n_lift_edmd = basis(np.zeros((1,n))).shape[1]

C_edmd = np.zeros((n,n_lift_edmd))
C_edmd[:,1:n+1] = np.eye(n)

optimizer_edmd = linear_model.MultiTaskLasso(alpha=alpha_edmd, fit_intercept=False, selection='random', max_iter=2000)
cv_edmd = linear_model.MultiTaskLassoCV(fit_intercept=False, n_jobs=-1, cv=3, selection='random', max_iter=2000)
standardizer_edmd = preprocessing.StandardScaler(with_mean=False)

model_edmd = Edmd_aut(n, basis, n_lift_edmd, n_traj_train, optimizer_edmd, cv=cv_edmd, standardizer=standardizer_edmd, C=C_edmd, continuous_mdl=False, dt=dt)
X_edmd, y_edmd = model_edmd.process(xs_train, np.tile(t_train,(n_traj_train,1)), downsample_rate=sub_sample_rate)
model_edmd.fit(X_edmd, y_edmd, cv=tune_mdl_edmd)
sys_edmd = LinearLiftedDynamics(model_edmd.A, None, model_edmd.C, model_edmd.basis, continuous_mdl=False, dt=dt)
if tune_mdl_edmd:
    print('$\\alpha$ EDMD: ', model_edmd.cv.alpha_)
```

### Evaluate open loop prediction performance

We now evaluate the open loop prediction performance of the implemented methods.
This is done by generating a new data set in the same way as the training set, predicting the evolution of the system
with the control sequence of each trajectory executed in the data set with each of the models, and finally comparing
the mean and standard deviation of the error between the true and predicted evolution over the trajectories. All the models are evaluated on 2 test data sets. One nominal data set (no signal or process noise) and a data set with process noise. No test data with signal noise is used, as we would need to fix the signal noise sequence to do a fair comparison in open loop prediction, hence resulting in the same comparison as the 2 data sets used.

```python
# Prediction performance evaluation parameters:
n_traj_ol = n_traj_train                                                     # Number of trajectories to execute, open loop
```

```python
from koopman_core.util import evaluate_ol_pred
from tabulate import tabulate

t_eval = dt * np.arange(4./dt + 1)
xs_ol = np.empty((n_traj_ol, t_eval.shape[0], n))
ctrl = ConstantController(system, 0.)

for ii in range(n_traj_ol):
    x0 = np.asarray([rand.uniform(l, u) for l, u in zip(-x0_max, x0_max)])
    xs_ol[ii,:,:], _ = system.simulate(x0, ctrl, t_eval)

mdl_lst = [sys_koop_dnn, sys_dmd, sys_edmd]
mdl_names = ['Koopman DNN', 'DMD', 'EDMD']
error, mse, std = [], [], []

for sys in mdl_lst:
    err_tmp, mse_tmp, std_tmp = evaluate_ol_pred(sys, xs_ol, t_eval)
    error.append(err_tmp)
    mse.append(mse_tmp)
    std.append(std_tmp)

print('\nOpen loop performance statistics:')

table_data = []
for name, mse_mdl, std_mdl in zip(mdl_names, mse, std):
    table_data.append([name, "{:.5f}".format(mse_mdl), "{:.5f}".format(std_mdl)])

print(tabulate(table_data,
               headers=['Mean squared error', 'Standard deviation']))
```

    Open loop performance statistics:
                   Mean squared error    Standard deviation
    -----------  --------------------  --------------------
    Koopman DNN               6e-05                 0.00782
    DMD                       0.04008               0.18128
    EDMD                      0.00021               0.01445

```python
import matplotlib.pyplot as plt
import matplotlib

figwidth = 12
lw = 2
fs = 14
y_lim_gain = 1.2
row = 2
col = 2

#Plot open loop results:
plt.figure(figsize=(figwidth,8))
axs = [plt.subplot(row,col,jj+1) for jj in range(n)]

for ii, err in enumerate(error):
    err_mean = np.mean(err, axis=0)
    err_std = np.std(err, axis=0)

    for jj in range(n):
        axs[jj].plot(t_eval[1:], err_mean[:,jj], label=mdl_names[ii])
        axs[jj].fill_between(t_eval[1:], err_mean[:,jj]-err_std[:,jj], err_mean[:,jj]+err_std[:,jj], alpha=0.1)

for jj in range(n):
    axs[jj].set_xlabel('Time (sec)', fontsize=fs)
    axs[jj].set_ylabel('$x_'+ str(jj+1) + '$', fontsize=fs)

plt.legend(frameon=False, fontsize=fs)
stitle=plt.suptitle('Open loop prediction performance of learned models', fontsize=fs+2)
matplotlib.rcParams['pdf.fonttype'] = 42
matplotlib.rcParams['ps.fonttype'] = 42
plt.savefig(folder_plots + 'koop_sys_prediction.pdf', format='pdf', dpi=2400, bbox_extra_artists=(stitle,), bbox_inches="tight")
plt.show()
```
print(standardizer_kdnn.mean_)
print(standardizer_kdnn.scale_)

```python
analytic_koop = sc.linalg.expm(np.array([[mu, 0, 0], [0, lambd, -lambd], [0, 0, mu]])*dt)
eig_analytic = np.linalg.eigvals(analytic_koop)
eig_koop_dnn = np.sort(np.linalg.eigvals(sys_koop_dnn.A))[::-1]
eig_edmd = np.sort(np.linalg.eigvals(sys_edmd.A))[::-1]
n_eig = eig_analytic.size
eig_koop_dnn = eig_koop_dnn[:n_eig]
eig_edmd = eig_edmd[:n_eig]
ang = np.linspace(0,2*np.pi,100)
circle = np.array([np.cos(ang), np.sin(ang)])

plt.figure()
plt.plot(circle[0,:], circle[1,:])
plt.scatter(np.real(eig_analytic), np.imag(eig_analytic), marker='o', color='tab:blue', label='Analytic')
plt.scatter(np.real(eig_koop_dnn), np.imag(eig_koop_dnn), marker='*', color='tab:green', label='Koopman DNN')
plt.scatter(np.real(eig_edmd), np.imag(eig_edmd), marker='x', color='tab:orange', label='EDMD')
plt.legend(loc='upper right', fontsize=fs)
plt.grid()
plt.title('Learned VS analytic Koopman spectrum', fontsize=fs)
plt.xlabel('Real part', fontsize=fs)
plt.ylabel('Imaginary part', fontsize=fs)
plt.show()
```

```python
from koopman_core.controllers import OpenLoopController
ii = 0
x0 = xs_ol[ii, 0, :].reshape(1,-1)
ctrl = ConstantController(sys, 0.)
z0 = sys.basis(np.atleast_2d(x0)).squeeze()
zs_tmp, _ = sys.simulate(z0, ctrl, t_train)
xs_pred = standardizer_kdnn.inverse_transform(np.dot(sys.C, zs_tmp.T).T)
#xs_pred = np.dot(sys.C, zs_tmp.T).T

plt.figure(figsize=(15,6))
for ss in range(2):
    plt.subplot(1,2,ss+1)
    plt.plot(t_train, xs_ol[ii, :, ss], '--', color='tab:orange', label='True')
    plt.plot(t_train, xs_pred[:, ss], '--', color='tab:blue', label='Learned')
    plt.plot(t_train, xs_ol[ii, :, ss+2], color='tab:orange', label='True')
    plt.plot(t_train, xs_pred[:, ss+2], color='tab:blue', label='Learned')
plt.legend()
plt.show()
```

```python

```