## Day 3: Cells in Silicon

Welcome to Day 3! Today, we start with our discussion of Hodgkin Huxley Neurons and how we can simulate them in Python using Tensorflow and Numerical Integration.

The electric potential measured across the membranes of excitable cells, such as neurons or heart cells, can undergo transient changes when perturbed by external inputs. When the inputs to a neuron are sufficiently large these transient changes can regeneratively build up into a large deviation from the resting state known as an action potential. Action potentials propagate undiminished along the axon and perturb post-synaptic neurons. The Hodgkin-Huxley model is a system of differential equations that describe the generation an action potential and its propagation along the axon. We provide only a brief overview of the Hodgkin-Huxley model. A number of classic references (Dayan 2005, Johnston 1995) and the original papers by Hodgkin and Huxley (Huxley 1952) chronicle the history and the details of the model. An excellent set of MOOCS and the accompanying textbooks (Gerstner 2014, Dayan 2005) give an accessible introduction to the topic

### What is the Hodgkin Huxley Neuron Model?

The cell membrane, a 5nm thick lipid bilayer, separates the inside from the outside of the neuron. The membrane is largely impermeable to charged ions present on either side. The concentration of $\text{Na}^{+}$ ions outside the cell is greater than its concentration inside, while $\text{K}^{+}$ ions are relatively abundant inside compared to the outside. In addition to these there are chloride ($\text{Cl}^{-}$), calcium ($\text{Ca}^{2+}$) and magnesium ions ($\text{Mg}^{+}$) that populate the cellular milieu. The differences in ionic abundances across the membrane cause a net accumulation of positive ions on one side of the membrane and negative ions on the other, and thus a potential difference across the membrane. Embedded on the membrane are ion channels that are highly selective to the ion species it lets across. In the squid axon, Hodgkin and Huxley found that there were only two types of ion channels ($\text{Na}^{+}$ and $\text{K}^{+}$), in addition to a non-specific leak channel. The Hodgkin-Huxley model of neurons can be understood with the help of an equivalent electrical circuit given below. The cell membrane acts as a capacitor. The total injected current ($I$) can be written as the sum of the capacitive current $I_{C}$, ionic currents $I_{Na}$ and $I_{K}$ and the leak current $I_L$.

\begin{equation}
I = I_{C}(t) + I_{Na}(t) + I_{K}(t)
\end{equation}
where,
\begin{eqnarray}
C_m = 1 \mu F/cm^2 \\
I_{Na} = g_{Na}(u-E_{Na})\\
I_{K} = g_{k}(u-E_K)\\
I_{L} = g_{L}(u-E_L)
\end{eqnarray}
The equation describing the membrane potential can thus be written as follows,
\begin{eqnarray}
\label{eq:HH}
C_m\frac{dV}{dt}=−I_{Na}(t)−I_{K}(t)−I_{L}(t)+I(t)
\end{eqnarray}
Hodgkin and Huxley discovered that the $Na$ and the $K$ channels do not act as Ohmic conductances, but are modulated by the potential across the membrane.
Changes in potential had a nonlinear effect on flow of ionic currents. Based in their experimental results they obtained a system of differential equations that described the temporal evolution of the membrane potential in terms of changes in ionic currents (chiefly $\text{Na}^{+}$ and $\text{K}^{+}$).

\begin{eqnarray}\label{d3_2}I_{Na} = g_{Na}m^3h(u−E_{Na})\end{eqnarray}
\begin{eqnarray}\label{d3_3}I_K = g_Kn^4(u−E_K)\end{eqnarray}
\begin{eqnarray}\label{d3_4}I_L = g_L(u−E_L)\end{eqnarray}

where $E_{Na}=50\ mV$, $E_K = -95\ mV$ and $E_L=-55\ mV$ are the reversal potentials; $g_{Na} = 100\ \mu S/cm^2$, $g_K = 10\ \mu S/cm^2$ and $g_L = 0.15\ \mu S/cm^2$ are the channel conductances; and m,h, and n are gating variables that follow the dynamics given by:

\begin{eqnarray}\label{d3_5}\frac{dm}{dt} = - \frac{1}{\tau_m}(m-m_0)\end{eqnarray}
\begin{eqnarray}\label{d3_6}\frac{dh}{dt} = - \frac{1}{\tau_h}(h-h_0)\end{eqnarray}
\begin{eqnarray}\label{d3_7}\frac{dn}{dt} = - \frac{1}{\tau_n}(n-n_0)\end{eqnarray}

where $\tau_m$, $\tau_h$ and $\tau_n$ are empirically determined voltage dependent time constants and $m_0$, $h_0$ and $n_0$ are voltage dependent asymptotic gating values.

On day 2, we had created a RK4 based numerical integrator. Recall this implementation:

```python
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
## OR for Tensorflow 2.0 and above ##
import tensorflow.compat.v1 as tf
tf.disable_eager_execution()

%matplotlib inline

def tf_check_type(t, y0): # Ensure Input is Correct
    if not (y0.dtype.is_floating and t.dtype.is_floating):
        raise TypeError('Error in Datatype')

class _Tf_Integrator():

    def integrate(self, func, y0, t):
        time_delta_grid = t[1:] - t[:-1]

        def scan_func(y, t_dt):
            t, dt = t_dt
            dy = self._step_func(func,t,dt,y) # Make code more modular.
            return y + dy

        y = tf.scan(scan_func, (t[:-1], time_delta_grid),y0)
        return tf.concat([[y0], y], axis=0)

    def _step_func(self, func, t, dt, y):
        k1 = func(y, t)
        half_step = t + dt / 2
        dt_cast = tf.cast(dt, y.dtype) # Failsafe

        k2 = func(y + dt_cast * k1 / 2, half_step)
        k3 = func(y + dt_cast * k2 / 2, half_step)
        k4 = func(y + dt_cast * k3, t + dt)
        return tf.add_n([k1, 2 * k2, 2 * k3, k4]) * (dt_cast / 6)

def odeint(func, y0, t):
    t = tf.convert_to_tensor(t, name='t')
    y0 = tf.convert_to_tensor(y0, name='y0')
    tf_check_type(y0,t)
    return _Tf_Integrator().integrate(func,y0,t)
```

#### Implementing the Hodgkin-Huxley neuron model

The variables of the Hodgkin Huxley neuron model that are updated at each integration time step are, the membrane potential, $V$, the sodium activation gating variable, $m$, the sodium inactivation gating variable, $h$, and the potassium channel gating variable, $n$. The dynamics are given by Equations above. In the following code, we define the parameters associated with the conductances, including the formulae for $\tau_{m}$, $\tau_{h}$, $\tau_{n}$ and the voltage dependent steady state values of the gating variables.

##### Step 1: Defining Parameters of the Neuron

```python
C_m = 1      # Membrane Capacitance

g_K = 10
E_K = -95

g_Na = 100
E_Na = 50

g_L = 0.15
E_L = -55
```

##### Step 2: Defining functions that calculate $\tau_m$, $\tau_h$, $\tau_n$, $m_0$, $h_0$, $n_0$

Note: Always use Tensorflow functions for all mathematical operations.

For our Hodgkin Huxley Model, we will determine the values of $\tau_m$, $\tau_h$, $\tau_n$, $m_0$, $h_0$, $n_0$  by the following equations:

```python
def K_prop(V):
    T = 22
    phi = 3.0**((T-36.0)/10)
    V_ = V-(-50)

    alpha_n = 0.02*(15.0 - V_)/(tf.exp((15.0 - V_)/5.0) - 1.0)
    beta_n = 0.5*tf.exp((10.0 - V_)/40.0)

    t_n = 1.0/((alpha_n+beta_n)*phi)
    n_0 = alpha_n/(alpha_n+beta_n)

    return n_0, t_n

def Na_prop(V):
    T = 22
    phi = 3.0**((T-36)/10)
    V_ = V-(-50)

    alpha_m = 0.32*(13.0 - V_)/(tf.exp((13.0 - V_)/4.0) - 1.0)
    beta_m = 0.28*(V_ - 40.0)/(tf.exp((V_ - 40.0)/5.0) - 1.0)

    alpha_h = 0.128*tf.exp((17.0 - V_)/18.0)
    beta_h = 4.0/(tf.exp((40.0 - V_)/5.0) + 1.0)

    t_m = 1.0/((alpha_m+beta_m)*phi)
    t_h = 1.0/((alpha_h+beta_h)*phi)

    m_0 = alpha_m/(alpha_m+beta_m)
    h_0 = alpha_h/(alpha_h+beta_h)

    return m_0, t_m, h_0, t_h
```

##### Step 3: Defining function that calculate Neuronal currents

```python
def I_K(V, n):
    return g_K  * n**4 * (V - E_K)

def I_Na(V, m, h):
    return g_Na * m**3 * h * (V - E_Na)

def I_L(V):
    return g_L * (V - E_L)
```

##### Step 4: Define the function dX/dt where X is the State Vector

```python
def dXdt(X, t):
    V = X[0:1]
    m = X[1:2]
    h = X[2:3]
    n = X[3:4]

    dVdt = (5 - I_Na(V, m, h) - I_K(V, n) - I_L(V)) / C_m
    # Here the current injection I_injected = 5 uA

    m0,tm,h0,th = Na_prop(V)
    n0,tn = K_prop(V)

    dmdt = - (1.0/tm)*(m-m0)
    dhdt = - (1.0/th)*(h-h0)
    dndt = - (1.0/tn)*(n-n0)

    out = tf.concat([dVdt,dmdt,dhdt,dndt],0)
    return out
```

##### Step 5: Define Initial Condition and Integrate

```python
y0 = tf.constant([-71,0,0,0], dtype=tf.float64)

epsilon = 0.01
t = np.arange(0,200,epsilon)

state = odeint(dXdt,y0,t)

with tf.Session() as sess:
    state = sess.run(state)
```

##### Step 6: Plot Output

```python
plt.plot(t,state.T[0,:])

plt.xlabel("Time (in ms)")
plt.ylabel("Voltage (in mV)")

plt.show()
```

#### Simulating Multiple Independent HH Neurons at the Same Time

Here we illustrate some simple steps that can be used to simulate populations of neurons efficiently. Key to setting up the equations is to order it in a manner that utilizes TensorFlow's algorithms that distribute vector, matrix and tensor computations over multiple cores. Consider a system of 20 independent HH neurons with different input currents that characterise the firing rates.

##### Methods of Parallelization
TensorFlow has built-in functions that speed up Tensor computations using available multi-cores, and GPU/TPU setups. There are two major parts of the code where such a speed-up can be effected

1. **RK4 iterations** Our implementation of the integrator utilizes Tensors as inputs.
2. **Functional Evaluations:** The form of the equations that describe the neuronal dynamics,  are common across neurons. Only the parameters for differ across neurons. This can be used to `vectorize' the equations.

Say $\vec{X}=[V,m,n,h]$ is the state vector of a single neuron and its dynamics are defined using parameters $C_m,g_K,...E_L$ equations of the form:

\begin{eqnarray}\frac{d\vec{X}}{dt} = [f_1(\vec{X},C_m,g_K,...E_L),f_2(\vec{X},C_m,g_K,...E_L)...f_m(\vec{X},C_m,g_K,...E_L)]\end{eqnarray}

We can convert these equations to a form in which all evaluations are done as vector calculations and NOT scalar calculations. Despite the parameters being different, the functional forms of the equations are similar for the same state variable of different neurons. Thus, the trick is to reorganize $\mathbf{X}$ as $\mathbf{X'}=[(V_1,V_2,...V_n),(m_1,m_2,...m_n),(h_1,h_2,...h_n),(n_1,n_2,...n_n)]=[\vec{V},\vec{m},\vec{h},\vec{n}]$. And the parameters as $[\vec{C_m},\vec{g_K}] = [C_{m_{1}}\dots C_{m_{n}},g_{K_{1}}\dots g_{K_{n}}]$ and so on.

The advantage of this re-ordering is that the differential equation of the form,
\begin{eqnarray}\frac{dV_i}{dt}=f(V_i,m_i,h_i,n_i,C_{m_i},g_{K_i}...)\end{eqnarray}

is now easily parallelizable using a vector computation of the form,

\begin{eqnarray}\frac{d\vec{V}}{dt}=f(\vec{V},\vec{m},\vec{h},\vec{n},\vec{C_m},\vec{g_K}...)\end{eqnarray}

The equations can now be written in the form,
\begin{eqnarray}\frac{d\mathbf{X'}}{dt}= \Big[\frac{d\vec{V}}{dt},\frac{d\vec{m}}{dt},\frac{d\vec{h}}{dt},\frac{d\vec{n}}{dt}\Big]\end{eqnarray}

```python
n_n = 20 # number of simultaneous neurons to simulate

# parameters will now become n_n-vectors

C_m = [1.0]*n_n
g_K = [10.0]*n_n
E_K = [-95.0]*n_n

g_Na = [100]*n_n
E_Na = [50]*n_n

g_L = [0.15]*n_n
E_L = [-55.0]*n_n

def K_prop(V):
    T = 22
    phi = 3.0**((T-36.0)/10)
    V_ = V-(-50)

    alpha_n = 0.02*(15.0 - V_)/(tf.exp((15.0 - V_)/5.0) - 1.0)
    beta_n = 0.5*tf.exp((10.0 - V_)/40.0)

    t_n = 1.0/((alpha_n+beta_n)*phi)
    n_0 = alpha_n/(alpha_n+beta_n)

    return n_0, t_n

def Na_prop(V):
    T = 22
    phi = 3.0**((T-36)/10)
    V_ = V-(-50)

    alpha_m = 0.32*(13.0 - V_)/(tf.exp((13.0 - V_)/4.0) - 1.0)
    beta_m = 0.28*(V_ - 40.0)/(tf.exp((V_ - 40.0)/5.0) - 1.0)

    alpha_h = 0.128*tf.exp((17.0 - V_)/18.0)
    beta_h = 4.0/(tf.exp((40.0 - V_)/5.0) + 1.0)

    t_m = 1.0/((alpha_m+beta_m)*phi)
    t_h = 1.0/((alpha_h+beta_h)*phi)

    m_0 = alpha_m/(alpha_m+beta_m)
    h_0 = alpha_h/(alpha_h+beta_h)

    return m_0, t_m, h_0, t_h

def I_K(V, n):
    return g_K  * n**4 * (V - E_K)

def I_Na(V, m, h):
    return g_Na * m**3 * h * (V - E_Na)

def I_L(V):
    return g_L * (V - E_L)

def dXdt(X, t):
    V = X[:1*n_n]       # First n_n values are Membrane Voltage
    m = X[1*n_n:2*n_n]  # Next n_n values are Sodium Activation Gating Variables
    h = X[2*n_n:3*n_n]  # Next n_n values are Sodium Inactivation Gating Variables
    n = X[3*n_n:]       # Last n_n values are Potassium Gating Variables

    dVdt = (np.linspace(0,10,n_n) - I_Na(V, m, h) - I_K(V, n) -I_L(V)) / C_m
    # Input current is linearly varied between 0 and 10

    m0,tm,h0,th = Na_prop(V)
    n0,tn = K_prop(V)

    dmdt = - (1.0/tm)*(m-m0)
    dhdt = - (1.0/th)*(h-h0)
    dndt = - (1.0/tn)*(n-n0)

    out = tf.concat([dVdt,dmdt,dhdt,dndt],0)
    return out

y0 = tf.constant([-71]*n_n+[0,0,0]*n_n, dtype=tf.float64)

epsilon = 0.01
t = np.arange(0,200,epsilon)

state = odeint(dXdt,y0,t)

with tf.Session() as sess:
    state = sess.run(state)
```

```python
plt.figure(figsize=(12,17))
for i in range(20):
    plt.subplot(10,2,i+1)
    plt.plot(t,state[:,i])
    plt.title("Injected Current = {:0.1f}".format(i/2))
    plt.ylim([-90,60])
    plt.xlabel("Time (in ms)")
    plt.ylabel("Voltage (in mV)")

plt.tight_layout()
plt.show()
```

#### Quantifying the Firing Rates against Input Current

The firing frequency as a function of the input is shown in the figure below. The code to generate the firing rate is below.

```python
plt.plot(np.linspace(0,10,20),np.bitwise_and(state[:-1,:20]<0,state[1:,:20]>0).sum(axis=0)/0.2,"o")
plt.xlabel("Injected Current(mA)")
plt.ylabel("Firing Rate (Hz)")
plt.show()
```

# References

(Dayan and Abbott, 2005) Peter Dayan and Larry F. Abbott, ``Theoretical Neuroscience - Computational and Mathematical Modeling of Neural Systems``,  2005.

(Johnston and Wu, 1995) D. Johnston and S. M.S. Wu, ``Foundations of cellular neurophysiology``,  1995.

(Huxley and Hodgkin, 1952) Huxley A. L. and Hodgkin A. F., ``Quantitative description of nerve current``, Journal of Physiology, vol. , number , pp. ,  1952.

(MOOC) , ``Neuronal dynamics``,  .  [online](https://www.edx.org/course/neuronal-dynamics)

(MOOC) , ``Computational Neuroscience``,  .  [online](https://www.coursera.org/learn/computational-neuroscience)

(Gerstner, Kistler et al., 2014) Wulfram Gerstner, Werner M. Kistler, Richard Naud et al., ``Neuronal dynamics: From single neurons to networks and models of cognition``,  2014.

Tutorial/Supplementary: Jupyter Notebooks/Day 3: Cells in Silicon/Day 3.ipynb


## 3.5 The Influence of Network Topologes

In Sec. 3.2 and 3.3, it is established that both ATC-DGD and AWC-DGD suffers from a limiting bias:

\begin{align}
\limsup_{k\to \infty} \frac{1}{n}\sum_{i=1}^n \|x_i^{(k)} - x^\star \|^2 = O\Big( \frac{\alpha^2 \rho^2 b^2}{(1-\rho)^2}\Big)
\end{align}

where $x^\star$ is the glboal solution to the optimization problem, $\alpha$ is the step-size, $\rho = \max\{|\lambda_2(W), \lambda_n(W)|\}$ and $b^2 = \frac{1}{n}\sum_{i=1}^n \|\nabla f_i(x^\star)\|^2$ denotes the data heterogeneity between nodes. Quantity $1-\rho$ measures the connectivity of the network topology. It is observed that $1-\rho$ can siginificantly affects the limiting bias especially when $1-\rho \to 0$. This section will examine how different network topologies will affect the limiting bias of ATC-DGD.

### 3.5.1 Connectivity and maximum degree of commonly-used topology

The following table, which we have discussed in Sec. 2.7, summarizes the connectivity (i.e., $1-\rho$) and the maximum degree of some common network topologes. The combination matrix $W$ of the undirected topologies, i.e., the ring, 2D-mesh, and fully-connected topology, are generated through the Matropolis-Hastings  rule (see Sec. 2.3), while the combination matrix of exponential-2 topology (which is a directed network) is genereted through the Averaging rule (see Sec. 2.4).

| Network topology | connectivity ($1-\rho$) | maximum degree |
| -: | :-: | :-: |
| Undirected Ring | $O(\frac{1}{n^2})$ | 2 |
| Undirected 2D-Mesh | $O(\frac{1}{n})$ | 4 |
| Exponential-2 | $O(\frac{1}{\ln(n)})$ | $\ln(n)$ |
| Fully connected| $1$ |$n-1$ |

- **Connectivity**: the larger $1-\rho$ is, the better the network connectivity is, and the smaller the limiting bias that DGD has. Generally speaking, dense network has better connectivity.

- **Maximum degree:** Maximum degree influences the communicaiton efficiency of *one step* of the average consensus, i.e., $x_i^{k+1} = \sum_{j=1}^n w_{ij} x_j^k$. Apparently, more non-zero $w_{ij}$'s imply more communications. In the extreme example of fully connected network, each agent will exchange information with $n-1$ neighbors to conduct avearging. In contrast, each agent in the undirected ring will only need to communicate with 2 neighbors to finish one update, which is more communication efficient. Generally speaking, sparse network has smaller maximum degree

- **Trade-off:** If the network topology can be designed freely, the exponential-2 graph empically reaches a nice trade-off between per-iteration communicaiton efficiency and the convergence rate, see the Table above.

### 3.5.2 Examine the influence of network topology on DGD

In this section, we examine the influence of different network topologies on the limiting bias of DGD. The code is  the same as in Sec.3.2. The different is we will test ATC-DGD's performance with different topologies.

#### 3.5.2.1 Set up BlueFog

In the following code, you should be able to see the id of your CPUs. We use 8 CPUs to conduct the following experiment.

```
import ipyparallel as ipp
import numpy as np
import torch
import networkx as nx
import matplotlib.pyplot as plt

%matplotlib inline

rc = ipp.Client(profile="bluefog")
print(rc.ids)
dview = rc[:]  # A DirectView of all engines
dview.block = True
```

    [0, 1, 2, 3, 4, 5, 6, 7]

```
%%px
import numpy as np
import bluefog.torch as bf
import torch
from bluefog.common import topology_util
import networkx as nx
import matplotlib.pyplot as plt

%matplotlib inline

bf.init()
print(f"Hello, I am {bf.rank()} among {bf.size()} processes")
```

    [stdout:0] Hello, I am 2 among 8 processes
    [stdout:1] Hello, I am 6 among 8 processes
    [stdout:2] Hello, I am 3 among 8 processes
    [stdout:3] Hello, I am 1 among 8 processes
    [stdout:4] Hello, I am 0 among 8 processes
    [stdout:5] Hello, I am 5 among 8 processes
    [stdout:6] Hello, I am 7 among 8 processes
    [stdout:7] Hello, I am 4 among 8 processes

#### 3.2.4.2 Generate local data $A_i$ and $b_i$

```
%%px

def generate_data(m, n, x_o):

    A = torch.randn(m, n).to(torch.double)
    #     x_o = torch.randn(n, 1).to(torch.double)
    ns = 0.1 * torch.randn(m, 1).to(torch.double)
    b = A.mm(x_o) + ns

    return A, b
```

#### 3.2.4.3 Distributed gradient descent method

```
%%px

def distributed_grad_descent(A, b, maxite=5000, alpha=1e-1):

    x_opt = torch.zeros(n, 1, dtype=torch.double)

    for _ in range(maxite):
        # calculate local gradient
        grad_local = A.t().mm(A.mm(x_opt) - b)

        # global gradient
        grad = bf.allreduce(grad_local, name="gradient")

        # distributed gradient descent
        x_opt = x_opt - alpha * grad

    grad_local = A.t().mm(A.mm(x_opt) - b)
    grad = bf.allreduce(grad_local, name="gradient")  # global gradient

    # evaluate the convergence of distributed gradient descent
    # the norm of global gradient is expected to 0 (optimality condition)
    global_grad_norm = torch.norm(grad, p=2)
    if bf.rank() == 0:
        print(
            "[Distributed Grad Descent] Rank {}: global gradient norm: {}".format(
                bf.rank(), global_grad_norm
            )
        )

    return x_opt
```

In the following code we run distributed gradient descent to achieve the global solution $x^\star$ to the optimization problem. To validate whether $x^\star$ is optimal, it is enough to examine $\frac{1}{n}\sum_{i=1}^n \nabla f_i(x^\star) = 0$.

```
m, n = 20, 5

# jupyter engine generate a reference solution x_o and push it to each worker
x_o = torch.randn(n, 1).to(torch.double)
dview.push({"x_o": x_o}, block=True)
```

    [None, None, None, None, None, None, None, None]

```
%%px

m, n = 20, 5
A, b = generate_data(m, n, x_o)
x_opt = distributed_grad_descent(A, b, maxite=300, alpha=1e-2)
```

    [stdout:4] [Distributed Grad Descent] Rank 0: global gradient norm: 1.2691123609414696e-14

#### 3.2.4.3 Decentralized gradient descent method

In this section, we depict the convergence curve of the decentralied gradient descent (the ATC version). We will utilize the $x^\star$ achieved by distributed gradient descent as the optimal solution. First, we define one step of the ATC-DGD method.

```
%%px

def ATC_DGD_one_step(x, x_opt, A, b, alpha=1e-2):

    # one-step ATC-DGD.
    # The combination weights have been determined by the associated combination matrix.

    grad_local = A.t().mm(A.mm(x) - b)  # compute local grad
    y = x - alpha * grad_local  # adapte
    x_new = bf.neighbor_allreduce(y)  # combination

    # the relative error: |x^k-x_gloval_average|/|x_gloval_average|
    rel_error = torch.norm(x_new - x_opt, p=2) / torch.norm(x_opt, p=2)

    return x_new, rel_error
```

Next we run ATC-DGD algorithm.

```
%%px

# uncomment the code if you want to use that topology
G = topology_util.RingGraph(bf.size())  # Set topology as ring topology.
# G = topology_util.ExponentialTwoGraph(bf.size()) # Set topology as exponential-two topology.
bf.set_topology(G)

maxite = 200
x = torch.zeros(n, 1, dtype=torch.double)  # Initialize x
rel_error = torch.zeros((maxite, 1))
for ite in range(maxite):

    if bf.rank() == 0:
        if ite % 10 == 0:
            print("Progress {}/{}".format(ite, maxite))

    x, rel_error[ite] = ATC_DGD_one_step(
        x, x_opt, A, b, alpha=3e-3
    )  # you can adjust alpha to different values
```

    [stdout:4]
    Progress 0/200
    Progress 10/200
    Progress 20/200
    Progress 30/200
    Progress 40/200
    Progress 50/200
    Progress 60/200
    Progress 70/200
    Progress 80/200
    Progress 90/200
    Progress 100/200
    Progress 110/200
    Progress 120/200
    Progress 130/200
    Progress 140/200
    Progress 150/200
    Progress 160/200
    Progress 170/200
    Progress 180/200
    Progress 190/200

In the following, we adjust to differnt topologies to examine its influence on the convergence rate and limiting bias.

```
# uncomment one of the following code when corresponding graph is activated

rel_error_ring = dview.pull("rel_error", block=True, targets=0)
rel_error_exp2 = dview.pull('rel_error', block=True, targets=0)
```

```
import matplotlib.pyplot as plt

%matplotlib inline

plt.semilogy(rel_error_ring)
plt.semilogy(rel_error_exp2)

plt.legend(["Ring", "Expo"], fontsize=16)

plt.xlabel("Iteration", fontsize=16)
plt.ylabel("Relative error", fontsize=16)
```

It is observed that exponential-two graph enables DGD to converge to a more accurate solution due to its better-connected topology.


```python
from decodes.core import *
from decodes.io.jupyter_out import JupyterOut
out = JupyterOut.unit_square( )
```

    http://decod.es/	v0.2.3
    io loaded

# Cross Product

There is another important way by which two vectors can be multiplied. Given two vectors $\vec{v_{1}}$ and $\vec{v_{2}}$, their **cross product** is denoted using $\times$, and is defined by the vector quantity:

\begin{align}
\vec{v_{1}} \times \vec{v_{2}} = (y_{1}z_{2} - z_{1}y_{2} \ ,\ z_{1}x_{2} - x_{1}z_{2} \ ,\ x_{1}y_{2} - y_{1}x_{2})
\end{align}

This quantity may be expressed in code as a method of the Vec class, as shown below.

In contrast to the dot product of two vectors, which results in a scalar, the cross product of two vectors results in a vector. This vector exhibits some useful properties:

* the direction of the cross product vector is **perpendicular to both given vectors**
* the **length is equal to the area** of the parallelogram formed between the two vectors.

```python
"""
Cross Product
"""
def cross(self, other):
    return Vec(
        self.y * other.z - self.z * other.y,
        self.z * other.x - self.x * other.z,
        self.x * other.y - self.y * other.x
    )

```

### Cross Product Direction

The definition of the cross product operation requires the vectors to be in $\mathbb{R}^3$. Before taking a closer look at the vector that it produces, let’s take a look at two special cases.

#### Special Cases
First, we can see that if the two vectors lie in the x-y plane, their cross product is a vector that only has a nonzero z-coordinate:

\begin{align}
( x_{1}, y_{1},0 ) \times ( x_{2}, y_{2},0 ) = ( 0, 0, x_{1}y_{2} -  y_{1}x_{2} )
\end{align}

Notice this cross product vector is aligned with the z-axis, perpendicular to both original vectors.

The other special case is when the two given vectors are parallel. This means that the vectors are scalar multiples of each other, and it can be readily checked that the cross product of $(x_{1} , y_{1} , z_{1})$ and $(cx_{1} , cy_{1} , cz_{1})$, ie, ***when two vectors are zero, the cross product is the zero vector***. This gives another simple check for when two vectors are parallel.

#### General Case

Now for the general case.

***The cross product of any two vectors that are not parallel will result in a vector perpendicular to both.*** But which perpendicular direction?

The **right-hand-rule** is one of the world’s only literal rules of thumb, and is explained as such:

> Using your right hand, curl your fingers from v1 to v2, making sure to pass through the smaller of the two angles. With your hand in this orientation, your thumb is pointing in the direction of the cross product.

### Cross Product Length

The length of the cross product is used less often than its direction, but the significance of the length is nonetheless worth mentioning.

The length of $\vec{v_{1}} \times \vec{v_{2}}$ is equal to the area of the parallelogram formed by $\vec{v_{1}}$ and $\vec{v_{2}}$.

### Cross Product in the Construction of Frames

A **frame** is an origin point in space bundled with a set of three **mutually perpendicular** unit vectors, also called an **orthonormal set of vectors**.

To demonstrate, imagine that we wish to generate an array of frames at points lying in the x-y plane, each oriented toward an attractor point. The cross product is invaluable in ensuring that a set of three mutually perpendicular unit vectors is produced, no matter the arguments provided to the generation function.

We show here a common construction in which the cross product is invoked twice.

```python
"""
Array of Frames
Constructs a frame of three mutually perpendicular unit vectors at a given Point in the plane, each oriented toward a given attractor Point
"""
for pt in pts:
    vec_x = Vec(pt,attr_pt).normalized()
    vec_y = vec_x.cross(Vec(0,0,1)).normalized()
    vec_z = vec_x.cross(vec_y).normalized()

```

Two points allow us to construct the first vector directly, while the x-y plane provided the normal direction which acted as a reference vector with which a cross product with the first vector could be performed.

### Cross Product in the Application of Guide Curves

```python
"""
Curve Function
Returns a Ray that describes a point on an ellipse along with a related
tangent vector.
"""
# a function that returns a point on an arbitrary curve
def pt_on_curve(t):
    c = math.cos(2*math.pi*t)
    s = math.sin(2*math.pi*t)
    return Point(len_x*c, len_y*s)

def ray_on_curve(t):
    # the EPSILON constant contains a very very small number
    nudge = EPSILON
    # a point at parameter t
    pt_a = pt_on_curve(t)
    # a point just past parameter t along the curve
    pt_b = pt_on_curve(t+nudge)
    vec = Vec(pt_a,pt_b).normalized()
    return Ray(pt_a,vec)

```

```python
"""
Frame Construction By Fixed Vector
Given a Ray on a curve, constructs the components of a frame using a
fixed vector as a reference
"""
tangent_ray = ray_on_curve(t)
# a vector tangent to the curve
frame_x = tangent_ray.vec

# a fixed reference vector
e_vec = Vec(0,1)
frame_z = frame_x.cross(e_vec)
frame_y = frame_x.cross(frame_z)

```

```python
"""
Frame Construction By Relative Vector
Given a Ray on a curve, constructs the components of a frame using a
relative vector oriented toward the center of the curve as a reference
"""
tangent_ray = ray_on_curve(t)
frame_x = tangent_ray.vec

# a relative reference vector
e_vec = Vec(tangent_ray.spt, Point())

frame_z = frame_x.cross(e_vec)
frame_y = frame_x.cross(frame_z)

```

```python

```

103 - Points, Vectors, and Coordinate Systems/092 - Cross Product.ipynb


# Usage of Taylor Series

1. **Theoretical applications:** Often when we try to understand a too complex function, using Taylor series enables us to turn it into a polynomial that we can work with directly.
2. **Numerical applications:** Some functions like $e^x$ or $\cos(x)$ are difficult for machines to compute. They can store tables of values at a fixed precision (and this is often done), but it still leaves open questions like “What is the 1000-th digit of cos(1)?” Taylor series are often helpful to answer such questions.

# 泰勒（Taylor)中值定理  1 (佩亚诺(Peano)余项)

**[定理]** 如果函数$f(x)$在$x_0$处具有$n$阶导数， 那么存在$x_0$的一个领域，对于该领域内的任一$x$, 有：
\begin{equation}
    f(x) = p_n(x) + R_n(x)
    \label{eq:taylor_1}
\end{equation}
其中:
\begin{equation}    \begin{aligned}
n次泰勒多项式: p_n(x) &= f(x_0) + f^{'}(x_0)(x-x_0) + \frac{f^{``}(x_0)}{2!}(x-x_0)^2 + \ldots + \frac{f^{(n)}(x_0)}{n!}(x-x_0)^n\\
佩亚诺(Peano)余项: R_n(x) &= O((x-x_0)^n)
\end{aligned}\end{equation}

\begin{proof} Assume R_n(x) = f(x) - p_n(x), then
\begin{align}
R_n(x_0) = R^{'}_n(x_0) = R^{''}_n(x_0) = \ldots = R^{(n)}_n(x_0) = 0
\end{align}

\end{proof}

# 泰勒（Taylor)中值定理  2 (拉格朗日(Lagrange)余项)

**[定理]** 如果函数$f(x)$在$x_0$的某个邻域$U(x_0)$具有$n+1$阶导数，对于任一$x \in U(x_0)$, 有：
\begin{equation}
    f(x) = p_n(x) + R_n(x)
    \label{eq:taylor_2}
\end{equation}
其中:
\begin{equation}    \begin{aligned}
多项式: p_n(x) &= f(x_0) + f^{'}(x_0)(x-x_0) + \frac{f^{``}(x_0)}{2!}(x-x_0)^2 + \ldots + \frac{f^{(n)}(x_0)}{n!}(x-x_0)^n\\
拉格朗日余项: R_n(x) &= \frac{f^{n+1}(\xi)}{(n+1)!}(x-x_0)^{n + 1}
    \end{aligned}\end{equation}
这里$\xi$是$x_0$与$x$之间的某一个值。

当$n=0$时， 泰勒公式变成拉格朗日中值公式：
\begin{equation}
f(x) = f(x_0) + f^{'}(x_0)(x-x_0) \; (\xi在x_0与x之间)
\end{equation}

在Taylor公式(\ref{eq:taylor_2})中, 是以多项式$p_n(x)$近似表达函数$f(x)$, 其误差为$|R_n(x)|$. 如果对于某一个固定的n, 当$x\in U(x_0)$时， $f^{(n+1)}(x) \leq M$, 那么有估计式：
\begin{equation}
|R_n(x)| = |\frac{f^{(n+1)}(\xi)}{(n+1)!}(x-x_0)^{n+1}| \leq |\frac{M}{(n+1)!}(x-x_0)^{n+1}|
\label{eq:taylor_diff}
\end{equation}

# 麦克劳林(Maclaurin)公式

在Taylor公式(\ref{eq:taylor_1})中, 如果取$x_0=0$， 那么带有**佩亚诺余项**的麦克劳林(Maclaurin)公式：
\begin{equation}
    f(x) = f(0) + f^{'}(0)(x) + \frac{f^{``}(0)}{2!}(x)^2 + \ldots + \frac{f^{(n)}(0)}{n!}(x)^n  + O(x^n)
    \label{eq:mac_taylor_0}
\end{equation}

在Taylor公式(\ref{eq:taylor_2})中, 如果取$x_0=0$， 那么$\xi \in (0, x)$, 因此$\xi=\theta x$ where $0<\theta<1$, 从而Taylor公式(\ref{eq:taylor_2})变成较简单的形式，即所谓带有**拉格朗日余项**的麦克劳林(Maclaurin)公式：
\begin{equation}
    f(x) = f(0) + f^{'}(0)(x) + \frac{f^{``}(0)}{2!}(x)^2 + \ldots + \frac{f^{(n)}(0)}{n!}(x)^n  + \frac{f^{(n+1)}(\theta x)}{(n+1)!}x^{n+1} \; where \; 0<\theta<1
    \label{eq:lag_taylor_0}
\end{equation}

由公式\ref{eq:mac_taylor_0}或是\ref{eq:lag_taylor_0}, 可得近似公式：
\begin{equation}
    f(x) = f(0) + f^{'}(0)(x) + \frac{f^{``}(0)}{2!}(x)^2 + \ldots + \frac{f^{(n)}(0)}{n!}(x)^n
\end{equation}
误差估计公式\ref{eq:taylor_diff}相应地变成
\begin{equation}
|R_n(x)| = \leq |\frac{M}{(n+1)!}(x)^{n+1}|
\label{eq:taylor_diff_updated}
\end{equation}

# Example

## 写出函数$f(x) = e^x$的带有拉格朗日余项的n阶麦克劳林公式
\begin{equation}\begin{aligned}
f^{'}(x) = f^{''}(x) = \ldots = f^{n}(x) = e^x \\
f^{'}(0) = f^{''}(0) = \ldots = f^{n}(0) = 1 \\
e^x = 1 + x + \frac{x^2}{2!} + \ldots + \frac{x^n}{n!} + \frac{e^{\theta x}}{(n+1)!} x^{n+1}
\end{aligned}\end{equation}

# 多元函数的泰勒(Taylor)展开式

## 一元函数在点$x_0$处的泰勒展开式为：

\begin{align}
\begin{split}
    f(x) = {}& f(x_0) \;+ \frac{f^{'}(x_0)}{1!}(x-x_0) \;+ \frac{f^{''}(x_0)}{2!}(x-x_0)^2 \;+ & o^n
\end{split}\\
\end{align}

## 二元函数在点$x_0 = (x^1_0,x^2_0)$处的泰勒展开式为：

\begin{align}
\begin{split}
    f(x^1, x^2) ={}& f(x^1_0, x^2_0) \;+ \\
         & \frac{f^{'}_{x^1}(x^1_0, x^2_0)}{1!}(x^1-x^1_0) + \frac{f^{'}_{x^2}(x^1_0, x^2_0)}{1!}(x^2-x^2_0)\;+\\
         & \frac{f^{''}_{x^1x^1}(x^1_0, x^2_0)}{2!}(x^1-x^1_0)^2 + \frac{f^{''}_{x^1x^2}(x^1_0, x^2_0)}{2!}(x^1-x^1_0)(x^2-x^2_0)\;+ \frac{f^{''}_{x^2x^1}(x^1_0, x^2_0)}{2!}(x^1-x^1_0)(x^2-x^2_0) + \frac{f^{''}_{x^2x^2}(x^1_0, x^2_0)}{2!}(x^2-x^2_0)^2\;+\\
         & o^n
\end{split}\\
\end{align}

上面可能还有些抽象，下面给出二元函数泰勒展开的具体推导帮助理，用偏导函数表达：
\begin{align}
\begin{split}
    f(x^1, x^2) = {}& f(x^1_0, x^2_0) \;+ \\
         & \frac{\frac{\partial f}{\partial x^1}|_{x_0}}{1!}(x^1-x^1_0) + \frac{\frac{\partial f}{\partial x^2}|_{x_0}}{1!}(x^2-x^2_0)\;+\\
         & \frac{\frac{\partial^2 f}{\partial x^1 \partial x^1}|_{x_0}}{2!}(x^1-x^1_0)^2 + \frac{\frac{\partial^2 f}{\partial x^1 \partial x^2}|_{x_0}}{2!}(x^1-x^1_0)(x^2-x^2_0)\;+ \frac{\frac{\partial^2 f}{\partial x^1 \partial x^1}|_{x_0}}{2!}(x^1-x^1_0)(x^2-x^2_0) + \frac{\frac{\partial^2 f}{\partial x^2 \partial x^2}|_{x_0}}{2!}(x^2-x^2_0)^2\;+ o^n \\
         = {}& f(x^1_0, x^2_0) \;+ \\
         & \frac{1}{1!}[\frac{\partial f}{\partial x^1}|_{x_0}(x^1-x^1_0) + \frac{\partial f}{\partial x^2}|_{x_0}(x^2-x^2_0)]\;+\\
         & \frac{1}{2!}[ \frac{\partial^2 f}{\partial x^1 \partial x^1}|_{x_0}(x^1-x^1_0)^2 + 2\frac{\partial^2 f}{\partial x^1 \partial x^2}|_{x_0}(x^1-x^1_0)(x^2-x^2_0) + \frac{\partial^2 f}{\partial x^2 \partial x^2}|_{x_0}(x^2-x^2_0)^2\ ]\;+ o^n \\
                  = {}& f(x^1_0, x^2_0) \;+ \frac{1}{1!}\begin{pmatrix} \frac{\partial f}{\partial x^1} &  \frac{\partial f}{\partial x^2} \end{pmatrix}_{x_0} \times \begin{pmatrix} x-x^1_0 \\ x-x^2_0 \end{pmatrix} + \frac{1}{2!} \times \begin{pmatrix} x-x^1_0 & x-x^2_0 \end{pmatrix} \times \begin{pmatrix} \frac{\partial^2 f}{\partial x^1 \partial x^1} & \frac{\partial^2 f}{\partial x^1 \partial x^2} \\ \frac{\partial^2 f}{\partial x^1 \partial x^2}  & \frac{\partial^2 f}{\partial x^2 \partial x^2} \end{pmatrix}_{x_0} \times \begin{pmatrix} x-x^1_0 \\ x-x^2_0 \end{pmatrix} \;+ o^n
\end{split}
\end{align}

**把多变量情形下($x_0 = (x^1_0, x^2_0, \ldots, x^n_0)$)的aylor二阶的形式**

\begin{align}
f(x) = f(x_0) + [\nabla f(x_0)]^T(x-x_0) + \frac{1}{2!}[x-x_0]^T H^2(x_0) [x-x_0] + o^n
\end{align}

其中：

\begin{align}
H(x_0) = \begin{bmatrix}
\frac{\partial^2f(x_0)}{\partial_{x_1}\partial_{x_1}} & \frac{\partial^2f(x_0)}{\partial_{x_1}\partial_{x_2}} & \ldots &\frac{\partial^2f(x_0)}{\partial_{x_1}\partial_{x_n}}\\
\frac{\partial^2f(x_0)}{\partial_{x_2}\partial_{x_1}} & \frac{\partial^2f(x_0)}{\partial_{x_2}\partial_{x_2}} & \ldots &\frac{\partial^2f(x_0)}{\partial_{x_2}\partial_{x_n}}\\
\vdots & \vdots & \ddots &\vdots\\
\frac{\partial^2f(x_0)}{\partial_{x_n}\partial_{x_1}} & \frac{\partial^2f(x_0)}{\partial_{x_n}\partial_{x_2}} & \ldots &\frac{\partial^2f(x_0)}{\partial_{x_n}\partial_{x_n}}\\
\end{bmatrix}
\end{align}

## 多元函数(n)在点$x_0 = (x^1_0, x^2_0, \ldots, x^n_0)$处的二阶泰勒展开式为：

\begin{align}
\begin{split}
    f(x^1, x^2, \ldots, x^n) = {}& f(x^1_0, x^2_0, \ldots, x^n_0) \;+ \\
         & \sum_{i=1}^{n}\frac{f^{'}_{x^i}(x^1_0, x^2_0, \ldots, x^n_0)}{1!}(x^i - x^i_0)\;+\\
         & \sum_{i=1}^{n}\sum_{j=1}^{n}\frac{f^{''}_{x^ix^j}(x^1_0, x^2_0, \ldots, x^n_0)}{2!}(x^i - x^i_0)(x^j - x^j_0)\;+\\
         & o^n
\end{split}\\
\end{align}

\begin{align}
\end{align}

## 多元函数(n)在点$x_0 = (x^1_0, x^2_0, \ldots, x^n_0)$处的二阶泰勒展开式为（矩阵的形式）

\begin{align}
f(x) = f(x_0) + [\nabla f(x_0)]^T(x-x_0) + \frac{1}{2!}[x-x_0]^T H(x_0) [x-x_0] + o^n
\end{align}

其中黑塞矩阵（Hessian Matrix， $H(x_0)$）为
\begin{align}
H(x_0) = \begin{bmatrix}
\frac{\partial^2f(x_0)}{\partial_{x_1}\partial_{x_1}} & \frac{\partial^2f(x_0)}{\partial_{x_1}\partial_{x_2}} & \ldots &\frac{\partial^2f(x_0)}{\partial_{x_1}\partial_{x_n}}\\
\frac{\partial^2f(x_0)}{\partial_{x_2}\partial_{x_1}} & \frac{\partial^2f(x_0)}{\partial_{x_2}\partial_{x_2}} & \ldots &\frac{\partial^2f(x_0)}{\partial_{x_2}\partial_{x_n}}\\
\vdots & \vdots & \ddots &\vdots\\
\frac{\partial^2f(x_0)}{\partial_{x_n}\partial_{x_1}} & \frac{\partial^2f(x_0)}{\partial_{x_n}\partial_{x_2}} & \ldots &\frac{\partial^2f(x_0)}{\partial_{x_n}\partial_{x_n}}\\
\end{bmatrix}
\end{align}

# 黑塞矩阵（Hessian Matrix）

其中黑塞矩阵（Hessian Matrix， $H(x_0)$）是一个多元函数的二阶偏导数构成的方阵，描述了函数的局部曲率。黑塞矩阵常用于牛顿法解决优化问题，利用黑塞矩阵可判定多元函数的极值问题。在工程实际问题的优化设计中，所列的目标函数往往很复杂，为了使问题简化，常常将目标函数在某点邻域展开成泰勒多项式来逼近原函数，此时函数在某点泰勒展开式的矩阵形式中会涉及到黑塞矩阵. 对于一个多元函数$f(x^1, x^2, \ldots, x^n)$, 如果函数$f$的二阶偏导数，由定义$f$的黑塞矩阵为：
\begin{align}
H_{i,j}(x) = D_iD_jf(x)
\end{align}
其中$D_i$表示对第$i$个变量微分算子， $x=(x^1, x^2, \ldots, x^n)$. 那么， $f$的黑塞矩阵为
\begin{align}
H(x_0) = \begin{bmatrix}
\frac{\partial^2f(x_0)}{\partial_{x_1}\partial_{x_1}} & \frac{\partial^2f(x_0)}{\partial_{x_1}\partial_{x_2}} & \ldots &\frac{\partial^2f(x_0)}{\partial_{x_1}\partial_{x_n}}\\
\frac{\partial^2f(x_0)}{\partial_{x_2}\partial_{x_1}} & \frac{\partial^2f(x_0)}{\partial_{x_2}\partial_{x_2}} & \ldots &\frac{\partial^2f(x_0)}{\partial_{x_2}\partial_{x_n}}\\
\vdots & \vdots & \ddots &\vdots\\
\frac{\partial^2f(x_0)}{\partial_{x_n}\partial_{x_1}} & \frac{\partial^2f(x_0)}{\partial_{x_n}\partial_{x_2}} & \ldots &\frac{\partial^2f(x_0)}{\partial_{x_n}\partial_{x_n}}\\
\end{bmatrix}
\end{align}

\begin{align}
f(x) = f(x_0) + [\nabla f(x_0)]^T(x-x_0) + \frac{1}{2!}[x-x_0]^T H(x_0) [x-x_0] + o^n
\end{align}

**对称性**

如果函数$f$在$D$区域内二阶连续可导， 那么$f$黑塞矩阵$H(f)$在D内对称矩阵。 原因是：如果函数$f$的二阶偏导连续，则二阶偏导数的求导顺序没有区别， 即：
\begin{equation}
\frac{\partial}{\partial x}(\frac{\partial f}{\partial y}) = \frac{\partial}{\partial y}(\frac{\partial f}{\partial x})
\end{equation}
则对于矩阵$H(f)$， 则$H_{i,j}(f) = H_{j,i}(f)$, 所以$H(f)$为对称矩阵。

\begin{equation}
\frac{d^2 f}{dx_i dx_j} = \frac{d^2 f}{dx_j dx_i}
\end{equation}

This follows by considering first perturbing a function in the direction of $x_i$, and then perturbing it in $x_j$ and then comparing the result of that with what happens if we perturb first $x_j$ and then $x_i$, with the knowledge that both of these orders lead to the same final change in the output of f.

**多元函数极值的判定**

如果实值多元函数$f(x^1, x^2, \ldots, x^n)$二阶连续可导，并且在临界点$M(x_i)$(其中$i=1,2,\ldots,n$, 并且$x_i$已知)处梯度（一阶导数）等于0， 即$\nabla f(M) = 0$, $M$为驻点。 仅通过一阶导数无法判断临界点$M$处是极大值或是极小值。

记$f$在$M$点处的黑塞矩阵$H(M)$, 由于$f$在$M$点连续，所以$H(M)$是一个$n \times n$的对称矩阵。对于$M(M)$，有如下结论：

1. 如果$H(M)$是正定矩阵， 则临界点$M$处是一个局部的极小值
2. 如果$H(M)$是负定矩阵， 则临界点$M$处是一个局部的极大值
3. 如果$H(M)$是不定矩阵， 则临界点$M$处不是极值

Hessian Matrix，它有着广泛的应用，如在牛顿方法、求极值以及边缘检测、消除边缘响应等方面的应用，图像处理里，可以抽取图像特征，在金融里可以用来作量化分析。
1. [用Hessian矩阵提出图片的关键特征](https://blog.csdn.net/jia20003/article/details/16874237)
2. [用Hessian矩阵进行量化分析](http://ookiddy.iteye.com/blog/2204127)
3. [边缘检测以及边缘响应消除](https://blog.csdn.net/lwzkiller/article/details/55050275)

既然检测到的对应点确认为边缘点，那么我们就有理由消除这个边缘点，所以边缘检测与边缘响应消除的应用是一回事。边缘到底有什么特征呢？如下图所示，一个二维平面上的一条直线，图像的特征具体可以描述为：沿着直线方向，亮度变化极小，垂直于直线方向，亮度由暗变亮，再由亮变暗，沿着这个方向，亮度变化很大。我们可以将边缘图像分布特征与二次型函数图形进行类比，是不是发现很相似，我们可以找到两个方向，一个方向图像梯度变化最慢，另一个方向图像梯度变化最快。那么图像中的边缘特征就与二次型函数的图像对应起来了，其实二次型函数中的hessian矩阵，也是通过对二次型函数进行二阶偏导得到的(可以自己求偏导测试下)，这就是我们为什么可以使用hessian矩阵来对边缘进行检测以及进行边缘响应消除，我想大家应该明白其中的缘由了。还是那句话，数学模型其实就是一种反映图像特征的模型。

所以Hessian matrix实际上就是多变量情形下的二阶导数，他描述了各方向上灰度梯度变化，这句话应该很好理解了吧。我们在使用对应点的hessian矩阵求取的特征向量以及对应的特征值，较大特征值所对应的特征向量是垂直于直线的，较小特征值对应的特征向量是沿着直线方向的。对于SIFT算法中的边缘响应的消除可以根据hessian矩阵进行判定。

## Example of f(x_1, x_2)

Suppose that $y = f(x_1, x_2) = a + b_1x_1 + b_2x_2 + c_{11}x^2_1 + c_{12}x_1x_2 + c_{22}x^2_2$

\begin{equation}
f(0, 0) = 0 \\
\nabla f(0, 0) = \begin{bmatrix} b_1 \\ b_2 \end{bmatrix} \\
Hf(0, 0) = \begin{bmatrix} 2c_{11} & c_{12} \\ c_{12} & 2c_{22} \end{bmatrix}
\end{equation}

we can get our original polynomial back by saying
\begin{align}
f(x) = f(x_0) + [\nabla f(x_0)]^T(x-x_0) + \frac{1}{2!}[x-x_0]^T H(x_0) [x-x_0] + o^n
\end{align}

```python

```


# Anomaly Detection with Adaptive Fourier Features on Quantum Computer Simulator

```python
!pip install qiskit
!pip install pylatexenc
```

    Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/
    Collecting qiskit
      Downloading qiskit-0.36.2.tar.gz (13 kB)
    Collecting qiskit-terra==0.20.2
      Downloading qiskit_terra-0.20.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (6.5 MB)
         |████████████████████████████████| 6.5 MB 10.1 MB/s
    Collecting qiskit-aer==0.10.4
      Downloading qiskit_aer-0.10.4-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (18.0 MB)
         |████████████████████████████████| 18.0 MB 136 kB/s
    Collecting qiskit-ibmq-provider==0.19.1
      Downloading qiskit_ibmq_provider-0.19.1-py3-none-any.whl (240 kB)
         |████████████████████████████████| 240 kB 46.9 MB/s
    Collecting qiskit-ignis==0.7.1
      Downloading qiskit_ignis-0.7.1-py3-none-any.whl (198 kB)
         |████████████████████████████████| 198 kB 14.0 MB/s
    Requirement already satisfied: scipy>=1.0 in /usr/local/lib/python3.7/dist-packages (from qiskit-aer==0.10.4->qiskit) (1.4.1)
    Requirement already satisfied: numpy>=1.16.3 in /usr/local/lib/python3.7/dist-packages (from qiskit-aer==0.10.4->qiskit) (1.21.6)
    Collecting websocket-client>=1.0.1
      Downloading websocket_client-1.3.2-py3-none-any.whl (54 kB)
         |████████████████████████████████| 54 kB 2.0 MB/s
    Requirement already satisfied: requests>=2.19 in /usr/local/lib/python3.7/dist-packages (from qiskit-ibmq-provider==0.19.1->qiskit) (2.23.0)
    Requirement already satisfied: urllib3>=1.21.1 in /usr/local/lib/python3.7/dist-packages (from qiskit-ibmq-provider==0.19.1->qiskit) (1.24.3)
    Collecting websockets>=10.0
      Downloading websockets-10.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl (112 kB)
         |████████████████████████████████| 112 kB 29.9 MB/s
    Collecting requests-ntlm>=1.1.0
      Downloading requests_ntlm-1.1.0-py2.py3-none-any.whl (5.7 kB)
    Requirement already satisfied: python-dateutil>=2.8.0 in /usr/local/lib/python3.7/dist-packages (from qiskit-ibmq-provider==0.19.1->qiskit) (2.8.2)
    Requirement already satisfied: setuptools>=40.1.0 in /usr/local/lib/python3.7/dist-packages (from qiskit-ignis==0.7.1->qiskit) (57.4.0)
    Collecting retworkx>=0.8.0
      Downloading retworkx-0.11.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl (1.6 MB)
         |████████████████████████████████| 1.6 MB 44.2 MB/s
    Collecting python-constraint>=1.4
      Downloading python-constraint-1.4.0.tar.bz2 (18 kB)
    Collecting ply>=3.10
      Downloading ply-3.11-py2.py3-none-any.whl (49 kB)
         |████████████████████████████████| 49 kB 4.1 MB/s
    Requirement already satisfied: dill>=0.3 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra==0.20.2->qiskit) (0.3.5.1)
    Requirement already satisfied: sympy>=1.3 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra==0.20.2->qiskit) (1.7.1)
    Collecting tweedledum<2.0,>=1.1
      Downloading tweedledum-1.1.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (943 kB)
         |████████████████████████████████| 943 kB 37.6 MB/s
    Requirement already satisfied: psutil>=5 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra==0.20.2->qiskit) (5.4.8)
    Collecting stevedore>=3.0.0
      Downloading stevedore-3.5.0-py3-none-any.whl (49 kB)
         |████████████████████████████████| 49 kB 4.4 MB/s
    Collecting scipy>=1.0
      Downloading scipy-1.7.3-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (38.1 MB)
         |████████████████████████████████| 38.1 MB 1.4 MB/s
    Collecting symengine>=0.9
      Downloading symengine-0.9.2-cp37-cp37m-manylinux2010_x86_64.whl (37.5 MB)
         |████████████████████████████████| 37.5 MB 2.3 MB/s
    Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.7/dist-packages (from python-dateutil>=2.8.0->qiskit-ibmq-provider==0.19.1->qiskit) (1.15.0)
    Requirement already satisfied: chardet<4,>=3.0.2 in /usr/local/lib/python3.7/dist-packages (from requests>=2.19->qiskit-ibmq-provider==0.19.1->qiskit) (3.0.4)
    Requirement already satisfied: idna<3,>=2.5 in /usr/local/lib/python3.7/dist-packages (from requests>=2.19->qiskit-ibmq-provider==0.19.1->qiskit) (2.10)
    Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.7/dist-packages (from requests>=2.19->qiskit-ibmq-provider==0.19.1->qiskit) (2022.5.18.1)
    Collecting cryptography>=1.3
      Downloading cryptography-37.0.2-cp36-abi3-manylinux_2_24_x86_64.whl (4.0 MB)
         |████████████████████████████████| 4.0 MB 30.8 MB/s
    Collecting ntlm-auth>=1.0.2
      Downloading ntlm_auth-1.5.0-py2.py3-none-any.whl (29 kB)
    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.19.1->qiskit) (1.15.0)
    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.19.1->qiskit) (2.21)
    Requirement already satisfied: importlib-metadata>=1.7.0 in /usr/local/lib/python3.7/dist-packages (from stevedore>=3.0.0->qiskit-terra==0.20.2->qiskit) (4.11.4)
    Collecting pbr!=2.1.0,>=2.0.0
      Downloading pbr-5.9.0-py2.py3-none-any.whl (112 kB)
         |████████████████████████████████| 112 kB 23.2 MB/s
    Requirement already satisfied: zipp>=0.5 in /usr/local/lib/python3.7/dist-packages (from importlib-metadata>=1.7.0->stevedore>=3.0.0->qiskit-terra==0.20.2->qiskit) (3.8.0)
    Requirement already satisfied: typing-extensions>=3.6.4 in /usr/local/lib/python3.7/dist-packages (from importlib-metadata>=1.7.0->stevedore>=3.0.0->qiskit-terra==0.20.2->qiskit) (4.2.0)
    Requirement already satisfied: mpmath>=0.19 in /usr/local/lib/python3.7/dist-packages (from sympy>=1.3->qiskit-terra==0.20.2->qiskit) (1.2.1)
    Building wheels for collected packages: qiskit, python-constraint
      Building wheel for qiskit (setup.py) ... done
      Created wheel for qiskit: filename=qiskit-0.36.2-py3-none-any.whl size=11933 sha256=f328a0a9daac65747c9017451e44f3d6e38699a8c51d97d27dd61c76a135e017
      Stored in directory: /root/.cache/pip/wheels/36/f7/83/e2755ad17aa35bc145fce34e184aaf394a265a978d95caaabf
      Building wheel for python-constraint (setup.py) ... done
      Created wheel for python-constraint: filename=python_constraint-1.4.0-py2.py3-none-any.whl size=24081 sha256=9c5d3724a5c23ee297b81fd58e0dd810d1a7e0a2f487509c28d0e023e127485b
      Stored in directory: /root/.cache/pip/wheels/07/27/db/1222c80eb1e431f3d2199c12569cb1cac60f562a451fe30479
    Successfully built qiskit python-constraint
    Installing collected packages: pbr, tweedledum, symengine, stevedore, scipy, retworkx, python-constraint, ply, ntlm-auth, cryptography, websockets, websocket-client, requests-ntlm, qiskit-terra, qiskit-ignis, qiskit-ibmq-provider, qiskit-aer, qiskit
      Attempting uninstall: scipy
        Found existing installation: scipy 1.4.1
        Uninstalling scipy-1.4.1:
          Successfully uninstalled scipy-1.4.1
    ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
    albumentations 0.1.12 requires imgaug<0.2.7,>=0.2.5, but you have imgaug 0.2.9 which is incompatible.
    Successfully installed cryptography-37.0.2 ntlm-auth-1.5.0 pbr-5.9.0 ply-3.11 python-constraint-1.4.0 qiskit-0.36.2 qiskit-aer-0.10.4 qiskit-ibmq-provider-0.19.1 qiskit-ignis-0.7.1 qiskit-terra-0.20.2 requests-ntlm-1.1.0 retworkx-0.11.0 scipy-1.7.3 stevedore-3.5.0 symengine-0.9.2 tweedledum-1.1.1 websocket-client-1.3.2 websockets-10.3
    Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/
    Collecting pylatexenc
      Downloading pylatexenc-2.10.tar.gz (162 kB)
         |████████████████████████████████| 162 kB 9.5 MB/s
    Building wheels for collected packages: pylatexenc
      Building wheel for pylatexenc (setup.py) ... done
      Created wheel for pylatexenc: filename=pylatexenc-2.10-py3-none-any.whl size=136835 sha256=72c407dbd89b799f918d13f84eeaac5cdf4b20bdf3f3af40e1608b1f9ccc0002
      Stored in directory: /root/.cache/pip/wheels/f1/8a/f5/33ee79d4473eb201b519fa40f989b842e373237395a3421f52
    Successfully built pylatexenc
    Installing collected packages: pylatexenc
    Successfully installed pylatexenc-2.10

gamma = 1, percentile = 9.54, test val train

## Mount Google Drive

```python
from google.colab import drive
drive.mount('/content/drive')
```

    Mounted at /content/drive

Load from Drive

Load from drive .mat file

```python
!pip install --upgrade --no-cache-dir gdown
```

    Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/
    Requirement already satisfied: gdown in /usr/local/lib/python3.7/dist-packages (4.4.0)
    Requirement already satisfied: tqdm in /usr/local/lib/python3.7/dist-packages (from gdown) (4.64.0)
    Requirement already satisfied: filelock in /usr/local/lib/python3.7/dist-packages (from gdown) (3.7.0)
    Requirement already satisfied: requests[socks] in /usr/local/lib/python3.7/dist-packages (from gdown) (2.23.0)
    Requirement already satisfied: six in /usr/local/lib/python3.7/dist-packages (from gdown) (1.15.0)
    Requirement already satisfied: beautifulsoup4 in /usr/local/lib/python3.7/dist-packages (from gdown) (4.6.3)
    Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.7/dist-packages (from requests[socks]->gdown) (2022.5.18.1)
    Requirement already satisfied: urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1 in /usr/local/lib/python3.7/dist-packages (from requests[socks]->gdown) (1.24.3)
    Requirement already satisfied: chardet<4,>=3.0.2 in /usr/local/lib/python3.7/dist-packages (from requests[socks]->gdown) (3.0.4)
    Requirement already satisfied: idna<3,>=2.5 in /usr/local/lib/python3.7/dist-packages (from requests[socks]->gdown) (2.10)
    Requirement already satisfied: PySocks!=1.5.7,>=1.5.6 in /usr/local/lib/python3.7/dist-packages (from requests[socks]->gdown) (1.7.1)

```python
#Loading .mat Cardiotocography dataset file
!gdown --id 1j4qIus2Bl44Om0UiOu4o4f__wVwUeDfP
```

    /usr/local/lib/python3.7/dist-packages/gdown/cli.py:131: FutureWarning: Option `--id` was deprecated in version 4.3.1 and will be removed in 5.0. You don't need to pass it anymore to use a file ID.
      category=FutureWarning,
    Downloading...
    From: https://drive.google.com/uc?id=1j4qIus2Bl44Om0UiOu4o4f__wVwUeDfP
    To: /content/cardio.mat
    100% 68.3k/68.3k [00:00<00:00, 64.3MB/s]

```python
import numpy as np
from time import time
from sklearn.kernel_approximation import RBFSampler
from sklearn.metrics import roc_auc_score
import matplotlib.pyplot as plt

from scipy import io

cardio = io.loadmat("cardio.mat")

cardio["X"].shape, cardio["y"].shape
```

    ((1831, 21), (1831, 1))

```python
from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit
from qiskit import Aer, execute
```

Preprocessing

np.load object --> X, y (scaled)

normal: '1'
anomalies: '0'

```python
from sklearn.preprocessing import MinMaxScaler
from scipy.stats import zscore

def preprocessing_cardio(data):
  features, labels = cardio["X"], cardio["y"]
  labels = 1 - labels
  return features, labels

cardio_X, cardio_y = preprocessing_cardio(cardio)

cardio_X.shape, cardio_y.shape
```

    ((1831, 21), (1831, 1))

Random fourier features

parameters: gamma, dimensions, random_state

X --> rff(X)

```python
from sklearn.kernel_approximation import RBFSampler
"""
  Code from https://arxiv.org/abs/2004.01227
"""
class QFeatureMap:

  def get_dim(self, num_features):
    pass

  def batch2wf(self, X):
    pass

  def batch2dm(self, X):
    psi = self.batch2wf(X)
    rho = np.einsum('...i,...j', psi, np.conj(psi))
    return rho

class QFeatureMap_rff(QFeatureMap):

  def __init__(self, rbf_sampler):
    self.rbf_sampler = rbf_sampler
    self.weights = np.array(rbf_sampler.random_weights_)
    self.offset = np.array(rbf_sampler.random_offset_)
    self.dim = rbf_sampler.get_params()['n_components']

  def get_dim(self, num_features):
    return self.dim

  def batch2wf(self, X):
    vals = np.dot(X, self.weights) + self.offset
    vals = np.cos(vals)
    vals *= np.sqrt(2.) / np.sqrt(self.dim)
    norms = np.linalg.norm(vals, axis=1)
    psi = vals / norms[:, np.newaxis]
    return psi
```

```python
# Create the RandomFourierFeature map
def rff(X, dim, gamma):
    feature_map_fourier = RBFSampler(gamma=gamma, n_components=dim, random_state=None)
    X_feat_train = feature_map_fourier.fit(cardio_X)

    rffmap = QFeatureMap_rff(rbf_sampler=feature_map_fourier)
    Crff = rffmap.batch2wf(cardio_X)

    return Crff
```

Train test split

```python
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(cardio_X, cardio_y, test_size=0.2, stratify=cardio_y, random_state=42)
X_train, X_val, y_train, y_val = train_test_split(X_train, y_train, test_size=0.25, stratify=y_train, random_state=42)

print(f"shape of X_train: {X_train.shape} X_test: {X_test.shape} X_val {X_val.shape}")
n_classes = np.bincount(y_test.ravel().astype(np.int64))

print(f"classes: 0: {n_classes[0]} 1: {n_classes[1]} %-anomalies: {n_classes[0] / (n_classes[0] + n_classes[1])}")
#print(f"classes: 0: {n_classes[0]} 1: {n_classes[1]} %-anomalies: {n_classes[1] / (n_classes[0] + n_classes[1])}")
```

    shape of X_train: (1098, 21) X_test: (367, 21) X_val (366, 21)
    classes: 0: 35 1: 332 %-anomalies: 0.09536784741144415

## Quantum Prediction

Density Matrix Build

Pure State: x_train --> U (matrix)

Mixed State: X_train --> lambda (vec) , U (matrix)

```python
def pure_state(Ctrain):
    phi_train = np.sum(Ctrain, axis=0)
    phi_train = phi_train / np.linalg.norm(phi_train)

    size_U = len(phi_train)
    U_train = np.zeros((size_U, size_U))
    x_1 = phi_train
    U_train[:, 0] = x_1
    for i in range(1, size_U):
        x_i = np.random.randn(size_U)
        for j in range(0, i):
            x_i -= x_i.dot(U_train[:, j]) * U_train[:, j]
        x_i = x_i / np.linalg.norm(x_i)
        U_train[:, i] = x_i

    return U_train
```

```python
import copy

def mixed_state(Ctrain):

    Z_train = np.outer(Ctrain[0], Ctrain[0])
    for i in range(1, len(Ctrain)):
        Z_train += np.outer(Ctrain[i], Ctrain[i])

    Z_train *= 1/len(Ctrain)

    lambda_P1_temp, U_train = np.linalg.eigh(Z_train)

    return lambda_P1_temp, U_train
```

Running the Quantum Circuits

parameters: eigvals, U, n_shots

X_test --> preds

```python
from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit
from qiskit import Aer, execute
```

```python
backend2 = Aer.get_backend('qasm_simulator')
# np.random.seed(1234)
```

```python
def quantum_circuit_mixed_four(Ctest, eigvals, U_train, n_shots=10000):

    Cpred = []
    for i in range(len(Ctest)):
        qc = QuantumCircuit(4, 2)

        qc.initialize(Ctest[i], [0, 1])
        qc.initialize(np.sqrt(eigvals), [2, 3])
        qc.isometry(U_train.T, [], [0, 1])

        qc.cnot(3, 1)
        qc.cnot(2, 0)

        qc.measure(0, 0)
        qc.measure(1, 1)

        counts = execute(qc, backend2, shots=n_shots).result().get_counts()
        try:
            Cpred.append(counts['00'] / n_shots)
        except:
            Cpred.append(0)

    return Cpred
```

```python
def quantum_circuit_pure_four(Ctest, U_train, n_shots=10000):

    Cpred = []
    for i in range(len(Ctest)):
        qc = QuantumCircuit(2, 2)

        qc.initialize(Ctest[i], [0, 1])
        qc.isometry(U_train.T, [], [0, 1]) # ArbRot as a isometry

        qc.measure(0, 0)
        qc.measure(1, 1)

        counts = execute(qc, backend2, shots=n_shots).result().get_counts()
        try:
            Cpred.append(np.sqrt(counts['00'] / n_shots))
        except:
            Cpred.append(0)

    return Cpred
```

Classification

The threshold is calculated using weighted f1_score

X_test --> classification_report, threshold

```python
from sklearn.metrics import roc_curve, f1_score
from sklearn.metrics import classification_report

def classification(preds_val, preds_test, y_test):

    thredhold = np.percentile(preds_val, q = 9.54)
    y_pred = preds_test > thredhold
    return classification_report(y_test, y_pred, digits=4)
```

```python
gammas = [0.0078125]
dim = 4
num_exps = 5
exp_time = time()
for gamma in gammas:
    for i in range(num_exps):
        print("4x4 Pure, experiment", i)
        print("Gamma:", gamma)

        feature_map_fourier = RBFSampler(gamma=gamma, n_components=dim) # original gamma 2
        X_feat_train = feature_map_fourier.fit(X_train)
        rffmap = QFeatureMap_rff(rbf_sampler=feature_map_fourier)
        X_feat_train = rffmap.batch2wf(X_train)
        X_feat_val = rffmap.batch2wf(X_val)
        X_feat_test = rffmap.batch2wf(X_test)

        U = pure_state(X_feat_train)

        preds_val = quantum_circuit_pure_four(X_feat_val, U)
        preds_test = quantum_circuit_pure_four(X_feat_test, U)

        print(classification(preds_val, preds_test, y_test))
        print(f"AUC = {round(roc_auc_score(y_test, preds_test), 4)}")
        print(time() - exp_time)
        exp_time = time()

        print("4x4 Mixed, experiment", i)
        print("Gamma:", gamma)

        eigvals, U = mixed_state(X_feat_train)

        preds_val = quantum_circuit_mixed_four(X_feat_val, eigvals, U)
        preds_test = quantum_circuit_mixed_four(X_feat_test, eigvals, U)

        print(classification(preds_val, preds_test, y_test))
        print(f"AUC = {round(roc_auc_score(y_test, preds_test), 4)}")
        print(time() - exp_time)
        exp_time = time()
```

    4x4 Pure, experiment 0
    Gamma: 0.0282
                  precision    recall  f1-score   support

             0.0     0.2222    0.1143    0.1509        35
             1.0     0.9112    0.9578    0.9339       332

        accuracy                         0.8774       367
       macro avg     0.5667    0.5361    0.5424       367
    weighted avg     0.8455    0.8774    0.8592       367

    AUC = 0.6635
    67.05944108963013
    4x4 Mixed, experiment 0
    Gamma: 0.0282
                  precision    recall  f1-score   support

             0.0     0.1538    0.1143    0.1311        35
             1.0     0.9091    0.9337    0.9212       332

        accuracy                         0.8556       367
       macro avg     0.5315    0.5240    0.5262       367
    weighted avg     0.8371    0.8556    0.8459       367

    AUC = 0.7065
    345.49100279808044
    4x4 Pure, experiment 1
    Gamma: 0.0282
                  precision    recall  f1-score   support

             0.0     0.3429    0.3429    0.3429        35
             1.0     0.9307    0.9307    0.9307       332

        accuracy                         0.8747       367
       macro avg     0.6368    0.6368    0.6368       367
    weighted avg     0.8747    0.8747    0.8747       367

    AUC = 0.719
    66.59816336631775
    4x4 Mixed, experiment 1
    Gamma: 0.0282
                  precision    recall  f1-score   support

             0.0     0.3438    0.3143    0.3284        35
             1.0     0.9284    0.9367    0.9325       332

        accuracy                         0.8774       367
       macro avg     0.6361    0.6255    0.6304       367
    weighted avg     0.8726    0.8774    0.8749       367

    AUC = 0.7412
    342.6536514759064
    4x4 Pure, experiment 2
    Gamma: 0.0282
                  precision    recall  f1-score   support

             0.0     0.2051    0.2286    0.2162        35
             1.0     0.9177    0.9066    0.9121       332

        accuracy                         0.8420       367
       macro avg     0.5614    0.5676    0.5642       367
    weighted avg     0.8497    0.8420    0.8458       367

    AUC = 0.7031
    66.87918972969055
    4x4 Mixed, experiment 2
    Gamma: 0.0282
                  precision    recall  f1-score   support

             0.0     0.2368    0.2571    0.2466        35
             1.0     0.9210    0.9127    0.9168       332

        accuracy                         0.8501       367
       macro avg     0.5789    0.5849    0.5817       367
    weighted avg     0.8557    0.8501    0.8529       367

    AUC = 0.701
    338.1283895969391
    4x4 Pure, experiment 3
    Gamma: 0.0282
                  precision    recall  f1-score   support

             0.0     0.4194    0.3714    0.3939        35
             1.0     0.9345    0.9458    0.9401       332

        accuracy                         0.8910       367
       macro avg     0.6769    0.6586    0.6670       367
    weighted avg     0.8854    0.8910    0.8880       367

    AUC = 0.792
    63.4641752243042
    4x4 Mixed, experiment 3
    Gamma: 0.0282
                  precision    recall  f1-score   support

             0.0     0.4194    0.3714    0.3939        35
             1.0     0.9345    0.9458    0.9401       332

        accuracy                         0.8910       367
       macro avg     0.6769    0.6586    0.6670       367
    weighted avg     0.8854    0.8910    0.8880       367

    AUC = 0.7773
    335.48702812194824
    4x4 Pure, experiment 4
    Gamma: 0.0282
                  precision    recall  f1-score   support

             0.0     0.1765    0.1714    0.1739        35
             1.0     0.9129    0.9157    0.9143       332

        accuracy                         0.8447       367
       macro avg     0.5447    0.5435    0.5441       367
    weighted avg     0.8427    0.8447    0.8437       367

    AUC = 0.6912
    64.79746794700623
    4x4 Mixed, experiment 4
    Gamma: 0.0282
                  precision    recall  f1-score   support

             0.0     0.2308    0.1714    0.1967        35
             1.0     0.9150    0.9398    0.9272       332

        accuracy                         0.8665       367
       macro avg     0.5729    0.5556    0.5620       367
    weighted avg     0.8497    0.8665    0.8575       367

    AUC = 0.7059
    332.08904814720154

# Quantum Prediction with Adaptive RFF

## Clone the QMC from GitHUB

```python
!pip install git+https://github.com/fagonzalezo/qmc.git
```

    Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/
    Collecting git+https://github.com/fagonzalezo/qmc.git
      Cloning https://github.com/fagonzalezo/qmc.git to /tmp/pip-req-build-9ojels8a
      Running command git clone -q https://github.com/fagonzalezo/qmc.git /tmp/pip-req-build-9ojels8a
    Requirement already satisfied: scipy in /usr/local/lib/python3.7/dist-packages (from qmc==0.0.1) (1.7.3)
    Requirement already satisfied: numpy>=1.19.2 in /usr/local/lib/python3.7/dist-packages (from qmc==0.0.1) (1.21.6)
    Requirement already satisfied: scikit-learn in /usr/local/lib/python3.7/dist-packages (from qmc==0.0.1) (1.0.2)
    Requirement already satisfied: tensorflow>=2.2.0 in /usr/local/lib/python3.7/dist-packages (from qmc==0.0.1) (2.8.2+zzzcolab20220527125636)
    Requirement already satisfied: typeguard in /usr/local/lib/python3.7/dist-packages (from qmc==0.0.1) (2.7.1)
    Requirement already satisfied: protobuf<3.20,>=3.9.2 in /usr/local/lib/python3.7/dist-packages (from tensorflow>=2.2.0->qmc==0.0.1) (3.17.3)
    Requirement already satisfied: keras<2.9,>=2.8.0rc0 in /usr/local/lib/python3.7/dist-packages (from tensorflow>=2.2.0->qmc==0.0.1) (2.8.0)
    Requirement already satisfied: opt-einsum>=2.3.2 in /usr/local/lib/python3.7/dist-packages (from tensorflow>=2.2.0->qmc==0.0.1) (3.3.0)
    Requirement already satisfied: six>=1.12.0 in /usr/local/lib/python3.7/dist-packages (from tensorflow>=2.2.0->qmc==0.0.1) (1.15.0)
    Requirement already satisfied: tensorboard<2.9,>=2.8 in /usr/local/lib/python3.7/dist-packages (from tensorflow>=2.2.0->qmc==0.0.1) (2.8.0)
    Requirement already satisfied: grpcio<2.0,>=1.24.3 in /usr/local/lib/python3.7/dist-packages (from tensorflow>=2.2.0->qmc==0.0.1) (1.46.3)
    Requirement already satisfied: gast>=0.2.1 in /usr/local/lib/python3.7/dist-packages (from tensorflow>=2.2.0->qmc==0.0.1) (0.5.3)
    Requirement already satisfied: tensorflow-estimator<2.9,>=2.8 in /usr/local/lib/python3.7/dist-packages (from tensorflow>=2.2.0->qmc==0.0.1) (2.8.0)
    Requirement already satisfied: termcolor>=1.1.0 in /usr/local/lib/python3.7/dist-packages (from tensorflow>=2.2.0->qmc==0.0.1) (1.1.0)
    Requirement already satisfied: tensorflow-io-gcs-filesystem>=0.23.1 in /usr/local/lib/python3.7/dist-packages (from tensorflow>=2.2.0->qmc==0.0.1) (0.26.0)
    Requirement already satisfied: astunparse>=1.6.0 in /usr/local/lib/python3.7/dist-packages (from tensorflow>=2.2.0->qmc==0.0.1) (1.6.3)
    Requirement already satisfied: google-pasta>=0.1.1 in /usr/local/lib/python3.7/dist-packages (from tensorflow>=2.2.0->qmc==0.0.1) (0.2.0)
    Requirement already satisfied: typing-extensions>=3.6.6 in /usr/local/lib/python3.7/dist-packages (from tensorflow>=2.2.0->qmc==0.0.1) (4.2.0)
    Requirement already satisfied: libclang>=9.0.1 in /usr/local/lib/python3.7/dist-packages (from tensorflow>=2.2.0->qmc==0.0.1) (14.0.1)
    Requirement already satisfied: wrapt>=1.11.0 in /usr/local/lib/python3.7/dist-packages (from tensorflow>=2.2.0->qmc==0.0.1) (1.14.1)
    Requirement already satisfied: h5py>=2.9.0 in /usr/local/lib/python3.7/dist-packages (from tensorflow>=2.2.0->qmc==0.0.1) (3.1.0)
    Requirement already satisfied: flatbuffers>=1.12 in /usr/local/lib/python3.7/dist-packages (from tensorflow>=2.2.0->qmc==0.0.1) (2.0)
    Requirement already satisfied: absl-py>=0.4.0 in /usr/local/lib/python3.7/dist-packages (from tensorflow>=2.2.0->qmc==0.0.1) (1.0.0)
    Requirement already satisfied: keras-preprocessing>=1.1.1 in /usr/local/lib/python3.7/dist-packages (from tensorflow>=2.2.0->qmc==0.0.1) (1.1.2)
    Requirement already satisfied: setuptools in /usr/local/lib/python3.7/dist-packages (from tensorflow>=2.2.0->qmc==0.0.1) (57.4.0)
    Requirement already satisfied: wheel<1.0,>=0.23.0 in /usr/local/lib/python3.7/dist-packages (from astunparse>=1.6.0->tensorflow>=2.2.0->qmc==0.0.1) (0.37.1)
    Requirement already satisfied: cached-property in /usr/local/lib/python3.7/dist-packages (from h5py>=2.9.0->tensorflow>=2.2.0->qmc==0.0.1) (1.5.2)
    Requirement already satisfied: google-auth-oauthlib<0.5,>=0.4.1 in /usr/local/lib/python3.7/dist-packages (from tensorboard<2.9,>=2.8->tensorflow>=2.2.0->qmc==0.0.1) (0.4.6)
    Requirement already satisfied: tensorboard-data-server<0.7.0,>=0.6.0 in /usr/local/lib/python3.7/dist-packages (from tensorboard<2.9,>=2.8->tensorflow>=2.2.0->qmc==0.0.1) (0.6.1)
    Requirement already satisfied: werkzeug>=0.11.15 in /usr/local/lib/python3.7/dist-packages (from tensorboard<2.9,>=2.8->tensorflow>=2.2.0->qmc==0.0.1) (1.0.1)
    Requirement already satisfied: tensorboard-plugin-wit>=1.6.0 in /usr/local/lib/python3.7/dist-packages (from tensorboard<2.9,>=2.8->tensorflow>=2.2.0->qmc==0.0.1) (1.8.1)
    Requirement already satisfied: requests<3,>=2.21.0 in /usr/local/lib/python3.7/dist-packages (from tensorboard<2.9,>=2.8->tensorflow>=2.2.0->qmc==0.0.1) (2.23.0)
    Requirement already satisfied: google-auth<3,>=1.6.3 in /usr/local/lib/python3.7/dist-packages (from tensorboard<2.9,>=2.8->tensorflow>=2.2.0->qmc==0.0.1) (1.35.0)
    Requirement already satisfied: markdown>=2.6.8 in /usr/local/lib/python3.7/dist-packages (from tensorboard<2.9,>=2.8->tensorflow>=2.2.0->qmc==0.0.1) (3.3.7)
    Requirement already satisfied: pyasn1-modules>=0.2.1 in /usr/local/lib/python3.7/dist-packages (from google-auth<3,>=1.6.3->tensorboard<2.9,>=2.8->tensorflow>=2.2.0->qmc==0.0.1) (0.2.8)
    Requirement already satisfied: rsa<5,>=3.1.4 in /usr/local/lib/python3.7/dist-packages (from google-auth<3,>=1.6.3->tensorboard<2.9,>=2.8->tensorflow>=2.2.0->qmc==0.0.1) (4.8)
    Requirement already satisfied: cachetools<5.0,>=2.0.0 in /usr/local/lib/python3.7/dist-packages (from google-auth<3,>=1.6.3->tensorboard<2.9,>=2.8->tensorflow>=2.2.0->qmc==0.0.1) (4.2.4)
    Requirement already satisfied: requests-oauthlib>=0.7.0 in /usr/local/lib/python3.7/dist-packages (from google-auth-oauthlib<0.5,>=0.4.1->tensorboard<2.9,>=2.8->tensorflow>=2.2.0->qmc==0.0.1) (1.3.1)
    Requirement already satisfied: importlib-metadata>=4.4 in /usr/local/lib/python3.7/dist-packages (from markdown>=2.6.8->tensorboard<2.9,>=2.8->tensorflow>=2.2.0->qmc==0.0.1) (4.11.4)
    Requirement already satisfied: zipp>=0.5 in /usr/local/lib/python3.7/dist-packages (from importlib-metadata>=4.4->markdown>=2.6.8->tensorboard<2.9,>=2.8->tensorflow>=2.2.0->qmc==0.0.1) (3.8.0)
    Requirement already satisfied: pyasn1<0.5.0,>=0.4.6 in /usr/local/lib/python3.7/dist-packages (from pyasn1-modules>=0.2.1->google-auth<3,>=1.6.3->tensorboard<2.9,>=2.8->tensorflow>=2.2.0->qmc==0.0.1) (0.4.8)
    Requirement already satisfied: idna<3,>=2.5 in /usr/local/lib/python3.7/dist-packages (from requests<3,>=2.21.0->tensorboard<2.9,>=2.8->tensorflow>=2.2.0->qmc==0.0.1) (2.10)
    Requirement already satisfied: chardet<4,>=3.0.2 in /usr/local/lib/python3.7/dist-packages (from requests<3,>=2.21.0->tensorboard<2.9,>=2.8->tensorflow>=2.2.0->qmc==0.0.1) (3.0.4)
    Requirement already satisfied: urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1 in /usr/local/lib/python3.7/dist-packages (from requests<3,>=2.21.0->tensorboard<2.9,>=2.8->tensorflow>=2.2.0->qmc==0.0.1) (1.24.3)
    Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.7/dist-packages (from requests<3,>=2.21.0->tensorboard<2.9,>=2.8->tensorflow>=2.2.0->qmc==0.0.1) (2022.5.18.1)
    Requirement already satisfied: oauthlib>=3.0.0 in /usr/local/lib/python3.7/dist-packages (from requests-oauthlib>=0.7.0->google-auth-oauthlib<0.5,>=0.4.1->tensorboard<2.9,>=2.8->tensorflow>=2.2.0->qmc==0.0.1) (3.2.0)
    Requirement already satisfied: joblib>=0.11 in /usr/local/lib/python3.7/dist-packages (from scikit-learn->qmc==0.0.1) (1.1.0)
    Requirement already satisfied: threadpoolctl>=2.0.0 in /usr/local/lib/python3.7/dist-packages (from scikit-learn->qmc==0.0.1) (3.1.0)
    Building wheels for collected packages: qmc
      Building wheel for qmc (setup.py) ... done
      Created wheel for qmc: filename=qmc-0.0.1-py3-none-any.whl size=12757 sha256=97575d9b21b37edc1e810b591f74f15ebf1cacda8041d9c863ab509b369bec65
      Stored in directory: /tmp/pip-ephem-wheel-cache-y7xaqn18/wheels/b2/d2/8d/5870208920445c46dfe694f549251e5f63d7afbee56c01f720
    Successfully built qmc
    Installing collected packages: qmc
    Successfully installed qmc-0.0.1

```python
import tensorflow as tf
import numpy as np
import qmc.tf.layers as layers
import qmc.tf.models as models
```

## One dimensional approximation

```python
import tensorflow as tf

class QFeatureMapAdaptRFF(layers.QFeatureMapRFF):
    def __init__(
                 self,
                 gamma_trainable=True,
                 weights_trainable=True,
                 **kwargs
                 ):
        self.g_trainable = gamma_trainable
        self.w_trainable = weights_trainable
        super().__init__(**kwargs)

    def build(self, input_shape):
        rbf_sampler = RBFSampler(
            gamma=0.5,
            n_components=self.dim,
            random_state=self.random_state)
        x = np.zeros(shape=(1, self.input_dim))
        rbf_sampler.fit(x)
        self.gamma_val = tf.Variable(
            initial_value=self.gamma,
            dtype=tf.float32,
            trainable=self.g_trainable,
            name="rff_gamma")
        self.rff_weights = tf.Variable(
            initial_value=rbf_sampler.random_weights_,
            dtype=tf.float32,
            trainable=self.w_trainable,
            name="rff_weights")
        self.offset = tf.Variable(
            initial_value=rbf_sampler.random_offset_,
            dtype=tf.float32,
            trainable=self.w_trainable,
            name="offset")
        self.built = True

    def call(self, inputs):
        vals = tf.sqrt(2 * self.gamma_val) * tf.matmul(inputs, self.rff_weights) + self.offset
        vals = tf.cos(vals)
        vals = vals * tf.sqrt(2. / self.dim)
        norms = tf.linalg.norm(vals, axis=-1)
        psi = vals / tf.expand_dims(norms, axis=-1)
        return psi

class DMRFF(tf.keras.Model):
    def __init__(self,
                 dim_x,
                 num_rff,
                 gamma=1,
                 random_state=None):
        super().__init__()
        self.rff_layer = QFeatureMapAdaptRFF(input_dim=dim_x, dim=num_rff, gamma=gamma, random_state=random_state, gamma_trainable=False)

    def call(self, inputs):
        x1 = inputs[:, 0]
        x2 = inputs[:, 1]
        phi1 = self.rff_layer(x1)
        phi2 = self.rff_layer(x2)
        dot = tf.einsum('...i,...i->...', phi1, phi2) ** 2
        return dot

def calc_rbf(dmrff, x1, x2):
    return dmrff.predict(np.concatenate([x1[:, np.newaxis, ...],
                                         x2[:, np.newaxis, ...]],
                                        axis=1),
                         batch_size=256)

# dmrff = DMRFF(dim_x=22, num_rff=n_rffs, gamma=gamma / 2, random_state=0)
# dm_rff_pdf = calc_rbf(dmrff, np.broadcast_to(mean, x.shape), x)
# pl.plot(x, gauss_pdf, 'r-',  alpha=0.6, label='Gaussian kernel')
# pl.plot(x, dm_rff_pdf, 'b-',  alpha=0.6, label='dmrff kernel')
# pl.title("$dim = "+str(n_rffs)+"$")
# pl.legend()
```

# QAD Adaptive RFF

```python
import pylab as pl
```

```python
X_train.shape
X_test.shape
```

    (367, 21)

```python
num_samples = 100000
rnd_idx1 = np.random.randint(X_train.shape[0],size=(num_samples, ))
rnd_idx2 = np.random.randint(X_train.shape[0],size=(num_samples, ))
#x_train_rff = [X_train[rnd_idx1], X_train[rnd_idx2]]
x_train_rff = np.concatenate([X_train[rnd_idx1][:, np.newaxis, ...],
                          X_train[rnd_idx2][:, np.newaxis, ...]],
                         axis=1)
dists = np.linalg.norm(x_train_rff[:, 0, ...] - x_train_rff[:, 1, ...], axis=1)
print(dists.shape)
pl.hist(dists)
print(np.quantile(dists, 0.001))
rnd_idx1 = np.random.randint(X_test.shape[0],size=(num_samples, ))
rnd_idx2 = np.random.randint(X_test.shape[0],size=(num_samples, ))
#x_test_rff = [X_test[rnd_idx1], X_test[rnd_idx2]]
x_test_rff = np.concatenate([X_test[rnd_idx1][:, np.newaxis, ...],
                          X_test[rnd_idx2][:, np.newaxis, ...]],
                         axis=1)
```

```python
def gauss_kernel_arr(x, y, gamma):
    return np.exp(-gamma * np.linalg.norm(x - y, axis=1) ** 2)
```

```python
sigma = np.quantile(dists, 0.01)
gamma = 1/(2 * sigma ** 2)
gamma_index = 6 # index 7 corresponds to gamma = 2**(-7)
gammas = 1/(2**(np.arange(11)))
print(gammas)
n_rffs = 4
print(f'Gamma: {gammas[gamma_index ]}')
y_train_rff = gauss_kernel_arr(x_train_rff[:, 0, ...], x_train_rff[:, 1, ...], gamma=gammas[gamma_index]) # gamma without square
y_test_rff = gauss_kernel_arr(x_test_rff[:, 0, ...], x_test_rff[:, 1, ...], gamma=gammas[gamma_index]) # gamma without square
dmrff = DMRFF(dim_x=21, num_rff=n_rffs, gamma=gammas[gamma_index ]/2, random_state=np.random.randint(10000)) # original rs = 0
dm_rbf = calc_rbf(dmrff, x_test_rff[:, 0, ...], x_test_rff[:, 1, ...])
pl.plot(y_test_rff, dm_rbf, '.')
dmrff.compile(optimizer="adam", loss='mse')
dmrff.evaluate(x_test_rff, y_test_rff, batch_size=16)
```

```python
print(f'Mean: {np.mean(dmrff.rff_layer.rff_weights)}')
print(f'Std: {np.std(dmrff.rff_layer.rff_weights)}')
print(f'Gamma: {dmrff.rff_layer.gamma_val.numpy()}')
pl.hist(dmrff.rff_layer.rff_weights.numpy().flatten(), bins=30);
```

```python
dmrff.fit(x_train_rff, y_train_rff, validation_split=0.1, epochs=40, batch_size=128)
```

    Epoch 1/40
    704/704 [==============================] - 5s 5ms/step - loss: 0.0406 - val_loss: 0.0330
    Epoch 2/40
    704/704 [==============================] - 2s 3ms/step - loss: 0.0297 - val_loss: 0.0263
    Epoch 3/40
    704/704 [==============================] - 3s 4ms/step - loss: 0.0233 - val_loss: 0.0203
    Epoch 4/40
    704/704 [==============================] - 2s 3ms/step - loss: 0.0183 - val_loss: 0.0164
    Epoch 5/40
    704/704 [==============================] - 2s 3ms/step - loss: 0.0157 - val_loss: 0.0148
    Epoch 6/40
    704/704 [==============================] - 1s 2ms/step - loss: 0.0146 - val_loss: 0.0141
    Epoch 7/40
    704/704 [==============================] - 1s 2ms/step - loss: 0.0140 - val_loss: 0.0137
    Epoch 8/40
    704/704 [==============================] - 1s 2ms/step - loss: 0.0137 - val_loss: 0.0134
    Epoch 9/40
    704/704 [==============================] - 1s 2ms/step - loss: 0.0134 - val_loss: 0.0132
    Epoch 10/40
    704/704 [==============================] - 1s 2ms/step - loss: 0.0133 - val_loss: 0.0130
    Epoch 11/40
    704/704 [==============================] - 1s 2ms/step - loss: 0.0132 - val_loss: 0.0129
    Epoch 12/40
    704/704 [==============================] - 2s 2ms/step - loss: 0.0131 - val_loss: 0.0128
    Epoch 13/40
    704/704 [==============================] - 1s 2ms/step - loss: 0.0130 - val_loss: 0.0128
    Epoch 14/40
    704/704 [==============================] - 1s 2ms/step - loss: 0.0129 - val_loss: 0.0127
    Epoch 15/40
    704/704 [==============================] - 1s 2ms/step - loss: 0.0129 - val_loss: 0.0127
    Epoch 16/40
    704/704 [==============================] - 1s 2ms/step - loss: 0.0128 - val_loss: 0.0126
    Epoch 17/40
    704/704 [==============================] - 1s 2ms/step - loss: 0.0128 - val_loss: 0.0126
    Epoch 18/40
    704/704 [==============================] - 2s 2ms/step - loss: 0.0128 - val_loss: 0.0126
    Epoch 19/40
    704/704 [==============================] - 1s 2ms/step - loss: 0.0127 - val_loss: 0.0126
    Epoch 20/40
    704/704 [==============================] - 1s 2ms/step - loss: 0.0127 - val_loss: 0.0126
    Epoch 21/40
    704/704 [==============================] - 1s 2ms/step - loss: 0.0127 - val_loss: 0.0125
    Epoch 22/40
    704/704 [==============================] - 1s 2ms/step - loss: 0.0127 - val_loss: 0.0126
    Epoch 23/40
    704/704 [==============================] - 2s 2ms/step - loss: 0.0127 - val_loss: 0.0125
    Epoch 24/40
    704/704 [==============================] - 1s 2ms/step - loss: 0.0126 - val_loss: 0.0125
    Epoch 25/40
    704/704 [==============================] - 1s 2ms/step - loss: 0.0126 - val_loss: 0.0125
    Epoch 26/40
    704/704 [==============================] - 1s 2ms/step - loss: 0.0126 - val_loss: 0.0125
    Epoch 27/40
    704/704 [==============================] - 1s 2ms/step - loss: 0.0126 - val_loss: 0.0125
    Epoch 28/40
    704/704 [==============================] - 1s 2ms/step - loss: 0.0126 - val_loss: 0.0125
    Epoch 29/40
    704/704 [==============================] - 1s 2ms/step - loss: 0.0126 - val_loss: 0.0125
    Epoch 30/40
    704/704 [==============================] - 1s 2ms/step - loss: 0.0126 - val_loss: 0.0125
    Epoch 31/40
    704/704 [==============================] - 2s 2ms/step - loss: 0.0126 - val_loss: 0.0125
    Epoch 32/40
    704/704 [==============================] - 2s 2ms/step - loss: 0.0126 - val_loss: 0.0125
    Epoch 33/40
    704/704 [==============================] - 1s 2ms/step - loss: 0.0126 - val_loss: 0.0125
    Epoch 34/40
    704/704 [==============================] - 1s 2ms/step - loss: 0.0126 - val_loss: 0.0125
    Epoch 35/40
    704/704 [==============================] - 1s 2ms/step - loss: 0.0126 - val_loss: 0.0125
    Epoch 36/40
    704/704 [==============================] - 1s 2ms/step - loss: 0.0126 - val_loss: 0.0125
    Epoch 37/40
    704/704 [==============================] - 2s 2ms/step - loss: 0.0126 - val_loss: 0.0125
    Epoch 38/40
    704/704 [==============================] - 1s 2ms/step - loss: 0.0126 - val_loss: 0.0125
    Epoch 39/40
    704/704 [==============================] - 1s 2ms/step - loss: 0.0126 - val_loss: 0.0125
    Epoch 40/40
    704/704 [==============================] - 1s 2ms/step - loss: 0.0126 - val_loss: 0.0125

    <keras.callbacks.History at 0x7f46623fee10>

```python
dm_rbf = calc_rbf(dmrff, x_test_rff[:, 0, ...], x_test_rff[:, 1, ...])
pl.plot(y_test_rff, dm_rbf, '.')
dmrff.evaluate(x_test_rff, y_test_rff, batch_size=128)
```

```python
print(f'Mean: {np.mean(dmrff.rff_layer.rff_weights)}')
print(f'Std: {np.std(dmrff.rff_layer.rff_weights)}')
print(f'Gamma: {dmrff.rff_layer.gamma_val.numpy()}')
pl.hist(dmrff.rff_layer.rff_weights.numpy().flatten(), bins=30);
```

```python
X_feat_train = dmrff.rff_layer.call(tf.cast(X_train, tf.float32))
X_feat_test = dmrff.rff_layer.call(tf.cast(X_test, tf.float32))
X_feat_val = dmrff.rff_layer.call(tf.cast(X_val, tf.float32))

X_feat_train = np.float64((X_feat_train).numpy())
X_feat_test = np.float64((X_feat_test).numpy())
X_feat_val = np.float64((X_feat_val).numpy())

X_feat_train = X_feat_train / np.linalg.norm(X_feat_train, axis = 1).reshape(-1, 1)
X_feat_test = X_feat_test / np.linalg.norm(X_feat_test, axis = 1).reshape(-1, 1)
X_feat_val = X_feat_val / np.linalg.norm(X_feat_val, axis = 1).reshape(-1, 1)

X_feat_train.shape, X_feat_test.shape, X_feat_val.shape
```

    ((1098, 4), (367, 4), (366, 4))

## Classical Pred AdpRFF

```python
from sklearn.metrics import roc_curve, f1_score
from sklearn.metrics import classification_report

def classification(preds_val, preds_test, y_test):

    thredhold = np.percentile(preds_val, q = 9.54)
    y_pred = preds_test > thredhold
    return classification_report(y_test, y_pred, digits=4)
```

```python
gamma = dmrff.rff_layer.gamma_val.numpy()
dim = n_rffs

print(f"{dim}x{dim} Pure, experiment AdaptiveRFF")
print("Gamma:", gamma)

## Training pure state and create the Unitary matrix to initialize such state

psi_train = X_feat_train.sum(axis = 0)
psi_train = psi_train / np.linalg.norm(psi_train)

preds_val_expected = np.sqrt((X_feat_val @ psi_train)**2)
preds_test_expected = np.sqrt((X_feat_test @ psi_train)**2)

print(classification(preds_val_expected, preds_test_expected, y_test))
print(f"AUC = {round(roc_auc_score(y_test, preds_test_expected), 4)}")
```

    4x4 Pure, experiment AdaptiveRFF
    Gamma: 0.0078125
                  precision    recall  f1-score   support

             0.0     0.7027    0.7429    0.7222        35
             1.0     0.9727    0.9669    0.9698       332

        accuracy                         0.9455       367
       macro avg     0.8377    0.8549    0.8460       367
    weighted avg     0.9470    0.9455    0.9462       367

    AUC = 0.9571

```python
gamma = dmrff.rff_layer.gamma_val.numpy()
dim = n_rffs

print(f"{dim}x{dim} mixed, experiment AdaptiveRFF")
print("Gamma:", gamma)

## Training mixed state and create the Unitary matrix to initialize such state
rho_train = np.zeros((dim, dim))

#for i in range(1000):
for i in range(len(X_feat_train)):
  rho_train += np.outer(X_feat_train[i], X_feat_train[i])

rho_train = rho_train / len(X_feat_train)

# Classical prediction
preds_val_mixed = np.zeros(len(X_feat_val))

for i in range(len(X_feat_val)):
  preds_val_mixed[i] = X_feat_val[i].T @ rho_train @ X_feat_val[i]

preds_test_mixed = np.zeros(len(X_feat_test))

for i in range(len(X_feat_test)):
  preds_test_mixed[i] = X_feat_test[i].T @ rho_train @ X_feat_test[i]

print(classification(preds_val_mixed, preds_test_mixed, y_test))
print(f"AUC = {round(roc_auc_score(y_test, preds_test_mixed), 4)}")
```

    4x4 mixed, experiment AdaptiveRFF
    Gamma: 0.0078125
                  precision    recall  f1-score   support

             0.0     0.6944    0.7143    0.7042        35
             1.0     0.9698    0.9669    0.9683       332

        accuracy                         0.9428       367
       macro avg     0.8321    0.8406    0.8363       367
    weighted avg     0.9435    0.9428    0.9431       367

    AUC = 0.956

## Quantum Simulator Pred AdpRFF

```python
def quantum_circuit_mixed(Ctest, eigvals, U_train, n_shots=10000, n_rffs = 16):

    num_qubits = int(np.log2(n_rffs)) # qubits to initialize
    qb_ls = [l for l in range(num_qubits)] # (0, 1, .., n-1)
    qb_ls2 = [l + num_qubits for l in range(num_qubits)] # (n, n+1, ..., 2*n-1)
    print(num_qubits)
    Cpred = []
    for i in range(len(Ctest)):
        qc = QuantumCircuit(2*num_qubits, num_qubits)

        qc.initialize(Ctest[i], qb_ls)
        qc.initialize(np.sqrt(eigvals), qb_ls2)
        qc.isometry(U_train.T, [], qb_ls) # ArbRot as a isometry

        for j in range(num_qubits):
          qc.cnot(j+num_qubits, j)

        for k in range(num_qubits):
          qc.measure(k, k)

        counts = execute(qc, backend2, shots=n_shots).result().get_counts()
        try:
            Cpred.append(counts['0'*num_qubits] / n_shots)
        except:
            Cpred.append(0)

    return Cpred

def quantum_circuit_pure(Ctest, U_train, n_shots=10000, n_rffs = 16):

    num_qubits = int(np.log2(n_rffs)) # num qubits to initialize
    print(num_qubits)
    qb_ls = [l for l in range(num_qubits)] # (0, 1, .., n-1)
    Cpred = []
    for i in range(len(Ctest)):
        qc = QuantumCircuit(num_qubits, num_qubits)
        qc.initialize(Ctest[i], qb_ls )
        qc.isometry(U_train.T, [], qb_ls )
        for k in range(num_qubits):
          qc.measure(k, k)

        counts = execute(qc, backend2, shots=n_shots).result().get_counts()
        try:
            Cpred.append(np.sqrt(counts['0'*num_qubits] / n_shots))
        except:
            Cpred.append(0)

    return Cpred
```

```python
backend2 = Aer.get_backend('qasm_simulator')
exp_time = time()
n_rff = n_rffs
print(f"{n_rff}x{n_rff} Pure, experiment")
print("Gamma:", dmrff.rff_layer.gamma_val.numpy())
U = pure_state(X_feat_train)

preds_val = quantum_circuit_pure(X_feat_val, U, 10000, n_rff)
preds_test = quantum_circuit_pure(X_feat_test, U, 10000, n_rff)

print(classification(preds_val, preds_test, y_test))
print(f"AUC = {round(roc_auc_score(y_test, preds_test), 4)}")
print(time() - exp_time)
exp_time = time()
```

    4x4 Pure, experiment
    Gamma: 0.0078125
    2
    2
                  precision    recall  f1-score   support

             0.0     0.7027    0.7429    0.7222        35
             1.0     0.9727    0.9669    0.9698       332

        accuracy                         0.9455       367
       macro avg     0.8377    0.8549    0.8460       367
    weighted avg     0.9470    0.9455    0.9462       367

    AUC = 0.9569
    79.42946267127991

```python
backend2 = Aer.get_backend('qasm_simulator')
exp_time = time()
n_rff = n_rffs
print(f"{n_rff}x{n_rff} Mixed, experiment")
print("Gamma:", dmrff.rff_layer.gamma_val.numpy())

eigvals, U = mixed_state(X_feat_train)

preds_val = quantum_circuit_mixed(X_feat_val, eigvals, U, 10000, n_rff)
preds_test = quantum_circuit_mixed(X_feat_test, eigvals, U, 10000, n_rff)

print(classification(preds_val, preds_test, y_test))
print(f"AUC = {round(roc_auc_score(y_test, preds_test), 4)}")
print(time() - exp_time)
exp_time = time()
```

    4x4 Mixed, experiment
    Gamma: 0.0078125
    2
    2
                  precision    recall  f1-score   support

             0.0     0.6842    0.7429    0.7123        35
             1.0     0.9726    0.9639    0.9682       332

        accuracy                         0.9428       367
       macro avg     0.8284    0.8534    0.8403       367
    weighted avg     0.9451    0.9428    0.9438       367

    AUC = 0.9568
    408.50582551956177