Rotational Seismology

Estimate Love Wave Phase-Velocities

Seismo-Live: http://seismo-live.org

##### Authors:
* Johannes Salvermoser ([@salve-](https://github.com/salve-))

---

Rotational Seismology Tutorial: Estimate Phase Velocities

**1 Load waveforms**

**2 Calculate correlation coefficients**

**3 Estimate Love wave phase velocities**

```python
%matplotlib inline
import matplotlib.pyplot as plt
plt.style.use('ggplot')
plt.rcParams['figure.figsize'] = 15, 8
```

### 1 Load pre-processed waveforms and event-xml

Waveforms of transverse acceleration and vertical rotation rate are loaded from the repository.

They were pre-processed as described in the **Data Download + Pre-processing**-Tutorial.

That means they are:

instrument corrected

resampled

filtered

rotated to radial-transverse

We also need the locations of station and event epicenter to obtain the theoretical backazimuth.

```python
from obspy import read, read_events
from obspy.geodetics.base import gps2dist_azimuth
AC = read('data/acc_Tohoku_preproc.mseed')
RLAS = read('data/rot_Tohoku_preproc.mseed')
cat = read_events('data/xml_Tohoku.xml')
event = cat[0]

# event location from event info
source_latitude = event.origins[0].latitude
source_longitude = event.origins[0].longitude

# station location (Wettzell)
station_latitude = 49.144001
station_longitude = 12.8782

# theoretical backazimuth and distance
baz = gps2dist_azimuth(source_latitude, source_longitude, station_latitude, station_longitude)
```

### 2 Calculate correlation coefficients for time windows
For the phase velocity analysis, the waveforms are divided into sub-windows. Due to the dispersive character of Love waves lower frequency waves are faster and arrive earlier at the measurement station than the higher frequency waves. We calculate phase velocities for subsequent time windows in which different frequencies dominate to catch this dispersive behavior.

In the first step, for each time window, we need to calculate zero-lag correlation coefficients between the transverse component of acceleration and the vertical rotation rate. The correlation yields the similarity of the two observed waveforms and is used as a threshold for the phase velocity estimation.

```python
from obspy.signal.cross_correlation import xcorr
import numpy as np

sampling_rate = int(RLAS[0].stats.sampling_rate)
sec = 120  # window length for correlation (teleseismic event)

# calculate correlation coefficients
corrcoefs = []
for ic in range(0, len(RLAS[0]) // (int(sampling_rate * sec))):
        coeffs = xcorr(RLAS[0].data[sampling_rate * sec * ic : sampling_rate * sec * (ic + 1)],
                       AC[0].data[sampling_rate * sec * ic : sampling_rate * sec * (ic + 1)], 0)
        corrcoefs.append(coeffs[1])

# plot waveforms in comparison and correlation coefficients in time windows
plt.figure(figsize=(15,2))
ax=plt.subplot(111)
ax.plot(RLAS[0].times(), RLAS[0].data/np.max(np.abs(RLAS[0].data)), 'r', label='vertical rotation rate')
ax.plot(AC[0].times(), AC[0].data/np.max(np.abs(AC[0].data)), 'k', label='transverse acceleration')
ax.set_xlabel('time [s]')
ax.set_ylabel('normalized amplitude')
ax.set_ylim(-1.0,1.2)
ax.axhline(y=.75, linewidth=1, c='k',ls='dashed')
ax.legend(loc=2, prop={"size":12})
for xi in range(sec,sec * len(corrcoefs)-1, sec):
    ax.axvline(x=xi, color='.6')

ax2=ax.twinx()
ax2.scatter(np.arange(60,sec * len(corrcoefs),sec),corrcoefs,c='Dodgerblue',s=50,edgecolors='w',linewidth=1.5)
ax2.set_xlim(0, RLAS[0].times()[-1])
ax2.set_ylim(-1,1.2)
ax2.set_ylabel('correlation \ncoefficient', color='Dodgerblue')
ax2.annotate('0.75 threshold', xy=(1000,.8),xycoords='data')
ax2.grid(visible=False)

plt.show()
```

### 3 Estimate Love wave phase velocities
At any time rotation rate and transverse acceleration are in phase and the amplitudes are related by

\begin{equation}
    \frac{a_{transv}(x, t)}{\dot{\Omega_z}(x, t)} = −2 \cdot v_{ph}
\end{equation}

The phase velocities can now be estimated by best-fitting waveforms the sliding time-windows along the seismic signal.

For a detailed deduction of this relationship, see **Igel et al. [2005]**  (link in References section).

Get theoretical P- and S-Wave first arrivals from a 1D velocity model using **TauPy**:

```python
from obspy.taup import TauPyModel
TauPy_model = TauPyModel('ak135')
arrivals_p = TauPy_model.get_travel_times(distance_in_degree=0.001 * baz[0] / 111.11,
                                        source_depth_in_km=event.origins[0].depth*0.001,
                                       phase_list=["P","p","Pdiff","PP","PKiKP","PKIKP","Pn","Pg"])
arrivals_s = TauPy_model.get_travel_times(distance_in_degree=0.001 * baz[0] / 111.11,
                                        source_depth_in_km=event.origins[0].depth*0.001,
                                       phase_list=["S","s","Sdiff","SS","SKiKS","SKIKS","Sn","Sg"])
tiemp = []
tiems = []
for i in range(0,len(arrivals_p)): tiemp.append(arrivals_p[i].time)
for ii in range(0,len(arrivals_s)): tiems.append(arrivals_s[ii].time)

# first arrivals
arriv_p = min(tiemp)
arriv_s = min(tiems)
print("P-wave arrival: ", arriv_p, "sec")
print("S-wave arrival: ", arriv_s, "sec")
```

    P-wave arrival:  743.918841784 sec
    S-wave arrival:  1362.23758331 sec

**Estimate phase velocities**

The Love wave phase velocities are estimated only for time-windows later than S-waves and for a threshold of 75% correlation between transverse acceleration and vertical rotation rate.

```python
sampling_rate = int(RLAS[0].stats.sampling_rate)

# calculate Love wave phase velocities [km/s] for time windows featuring correlation coefficients > 0.75
# and after S-wave arrival
surf = int(arriv_s/120.)+2  # window number after S-waves
phasv = []
for iph in range(surf, len(corrcoefs)):
    if corrcoefs[iph] >= 0.75:
        phas_v = 0.001 * 0.5 * max(AC[0].data[sampling_rate * sec * iph : sampling_rate * sec * (iph + 1)]) /\
                 max(RLAS[0].data[sampling_rate * sec * iph : sampling_rate * sec * (iph + 1)])
    else:
        phas_v = np.NaN
    phasv.append(phas_v)
```

**Plot phase velocities**

```python
import matplotlib as mpl

plt.subplot2grid((4, 30), (0, 0), colspan=29)
plt.plot(RLAS[0].times(), RLAS[0].data,label='vertical rotation rate')
plt.xlim(0, RLAS[0].times()[-1])
plt.ylabel('vert. rot. rate \n[nrad/s]')
plt.legend(loc=3)

# add P- and S-wave arrivals
plt.axvline(arriv_p);plt.annotate('P-arrival', xy=(arriv_p+20,np.max(RLAS[0].data)),xycoords='data');
plt.axvline(arriv_s);plt.annotate('S-arrival', xy=(arriv_s+20,np.max(RLAS[0].data)),xycoords='data');

# transverse acceleration
plt.subplot2grid((4, 30), (1, 0), colspan=29)
plt.plot(AC[0].times(), AC[0].data, 'k',label='transverse acceleration')
plt.xlim(0, AC[0].times()[-1])
plt.ylabel('transv. acc. \n[$nm/s^2$]')
plt.ticklabel_format(axis='y', style='sci', scilimits=(-2,2))
plt.legend(loc=3)
plt.axvline(arriv_p)
plt.axvline(arriv_s)

# phase velocities plot
plt.subplot2grid((4, 30), (2, 0), colspan=29)
vel = plt.scatter(np.arange(sec/2. + surf*sec, sec/2. + sec * (len(phasv)+surf), sec), phasv,c=corrcoefs[surf:], vmin=0.75, vmax=1, s=35,cmap=plt.cm.autumn_r)
plt.xlim(0, RLAS[0].stats.delta * len(RLAS[0].data))
plt.ylabel('phase vel. \n[km/s]')
plt.xlabel('time [s]')

# add colorbar
fig = plt.subplot2grid((4, 30), (2, 29))
norm = mpl.colors.Normalize(vmin=.75, vmax=1)
cb1 = mpl.colorbar.ColorbarBase(fig, cmap=plt.cm.autumn_r, norm=norm, orientation='vertical')
cb1.set_ticks([0.75,0.8,0.85,0.9,0.95,1])
cb1.set_label(u'X-corr. coeff.', fontweight='bold')

# correlation coefficients plot
plt.subplot2grid((4, 30), (3, 0), colspan=29)
plt.scatter(np.arange(sec/2., sec/2. + sec * len(corrcoefs), sec), corrcoefs,c='k', s=20)
plt.xlim(0, RLAS[0].stats.delta * len(RLAS[0].data))
plt.xlabel('time [s]')
plt.ylabel('correlation \ncoefficient')
plt.axhline(y=.75, linewidth=1, c='k',ls='dashed')
plt.annotate('0.75 threshold', xy=(500,.8),xycoords='data')

plt.show()
```

### References

    Beyreuther, M., R. Barsch, L. Krischer, T. Megies, Y. Behr and J. Wassermann (2010)

    ObsPy: A Python Toolbox for Seismology

    SRL, 81(3), 530-533, doi:10.1785/gssrl.81.3.530

    Igel, H., U. Schreiber, A. Flaws, B. Schuberth, A. Velicoseltsev, A. Cochard (2005)

    Rotational motions induced by the M8.1 Tokachi-oki earthquake, September 25, 2003

    GRL, doi:10.1029/2004GL022336

    Krischer, L., T. Megies, R. Barsch, M. Beyreuther, T. Lecocq, C. Caudron, J. Wassermann (2015)

    ObsPy: a bridge for seismology into the scientific Python ecosystem

    Computational Science & Discovery, 8(1), 014003, doi:10.1088/1749-4699/8/1/014003

    Megies, T., M. Beyreuther, R. Barsch, L. Krischer, J. Wassermann (2011)

    ObsPy – What can it do for data centers and observatories?

    Annals Of Geophysics, 54(1), 47-58, doi:10.4401/ag-4838

```python

```


```
%%capture
!pip install --quiet --upgrade pip
!pip install --quiet cirq==0.7
```

# Rabi Oscillation Experiment

In this experiment, you are going to use Cirq to check that rotating a qubit by an increasing angle, and then measuring the qubit, produces Rabi oscillations. This requires you to do the following things:

1. Prepare the $|0\rangle$ state.
2. Rotate by an angle $\theta$ around the $X$ axis.
3. Measure to see if the result is a 1 or a 0.
4. Repeat steps 1-3 $k$ times.
5. Report the fraction of $\frac{\text{Number of 1's}}{k}$
found in step 3.

## 1. Getting to know Cirq

Cirq emphasizes the details of implementing quantum algorithms on near term devices.
For example, when you work on a qubit in Cirq you don't operate on an unspecified qubit that will later be mapped onto a device by a hidden step.
Instead, you are always operating on specific qubits at specific locations that you specify.

Suppose you are working with a 54 qubit Sycamore chip.
This device is included in Cirq by default.
It is called `cirq.google.Sycamore`, and you can see its layout by printing it.

```
import cirq
working_device = cirq.google.Sycamore
print(working_device)
```

                                                 (0, 5)───(0, 6)
                                                 │        │
                                                 │        │
                                        (1, 4)───(1, 5)───(1, 6)───(1, 7)
                                        │        │        │        │
                                        │        │        │        │
                               (2, 3)───(2, 4)───(2, 5)───(2, 6)───(2, 7)───(2, 8)
                               │        │        │        │        │        │
                               │        │        │        │        │        │
                      (3, 2)───(3, 3)───(3, 4)───(3, 5)───(3, 6)───(3, 7)───(3, 8)───(3, 9)
                      │        │        │        │        │        │        │        │
                      │        │        │        │        │        │        │        │
             (4, 1)───(4, 2)───(4, 3)───(4, 4)───(4, 5)───(4, 6)───(4, 7)───(4, 8)───(4, 9)
             │        │        │        │        │        │        │        │
             │        │        │        │        │        │        │        │
    (5, 0)───(5, 1)───(5, 2)───(5, 3)───(5, 4)───(5, 5)───(5, 6)───(5, 7)───(5, 8)
             │        │        │        │        │        │        │
             │        │        │        │        │        │        │
             (6, 1)───(6, 2)───(6, 3)───(6, 4)───(6, 5)───(6, 6)───(6, 7)
                      │        │        │        │        │
                      │        │        │        │        │
                      (7, 2)───(7, 3)───(7, 4)───(7, 5)───(7, 6)
                               │        │        │
                               │        │        │
                               (8, 3)───(8, 4)───(8, 5)
                                        │
                                        │
                                        (9, 4)

For this experiment you only need one qubit and you can just pick whichever one you like.

```
my_qubit = cirq.GridQubit(5, 6)
```

Once you've chosen your qubit you can build circuits that use it.

```
from cirq.contrib.svg import SVGCircuit

# Create a circuit with X, Ry(pi/2) and H.
my_circuit = cirq.Circuit(
    # Rotate the qubit pi/2 radians around the X axis.
    cirq.rx(3.141 / 2).on(my_qubit),
    # Measure the qubit.
    cirq.measure(my_qubit, key='out')
)
SVGCircuit(my_circuit)
```

    findfont: Font family ['Arial'] not found. Falling back to DejaVu Sans.

Now you can simulate sampling from your circuit using `cirq.Simulator`.

```
sim = cirq.Simulator()
samples = sim.sample(my_circuit, repetitions=10)
samples
```

      out

      0
      1

      1
      0

      2
      0

      3
      1

      4
      1

      5
      0

      6
      1

      7
      1

      8
      0

      9
      1

You can also get properties of the circuit, such as the density matrix of the circuit's output or the state vector just before the terminal measurement.

```
state_vector_before_measurement = sim.simulate(my_circuit[:-1])
sampled_state_vector_after_measurement = sim.simulate(my_circuit)

print(f'State before measurement:')
print(state_vector_before_measurement)
print(f'State after measurement:')
print(sampled_state_vector_after_measurement)

```

    State before measurement:
    measurements: (no measurements)
    output vector: 0.707|0⟩ - 0.707j|1⟩
    State after measurement:
    measurements: out=1
    output vector: -1j|1⟩

You can also examine the outputs from a noisy environment.
For example, an environment where 10% depolarization is applied to each qubit after each operation in the circuit:

```
noisy_sim = cirq.DensityMatrixSimulator(noise=cirq.depolarize(0.1))
noisy_post_measurement_state = noisy_sim.simulate(my_circuit)
noisy_pre_measurement_state = noisy_sim.simulate(my_circuit[:-1])

print('Noisy state after measurement:' + str(noisy_post_measurement_state))
print('Noisy state before measurement:' + str(noisy_pre_measurement_state))
```

    Noisy state after measurement:measurements: out=0
    final density matrix:
    [[0.9333334 +0.j 0.        +0.j]
     [0.        +0.j 0.06666666+0.j]]
    Noisy state before measurement:measurements: (no measurements)
    final density matrix:
    [[0.50012845+0.j         0.        +0.43333334j]
     [0.        -0.43333334j 0.49987155+0.j        ]]

## 2. Parameterized Circuits and Sweeps

Now that you have some of the basics end to end, you can create a parameterized circuit that rotates by an angle $\theta$:

```
import sympy
theta = sympy.Symbol('theta')

parameterized_circuit = cirq.Circuit(
    cirq.rx(theta).on(my_qubit),
    cirq.measure(my_qubit, key='out')
)
SVGCircuit(parameterized_circuit)
```

In the above block you saw that there is a `sympy.Symbol` that you placed in the circuit. Cirq supports symbolic computation involving circuits. What this means is that when you construct `cirq.Circuit` objects you can put placeholders in many of the classical control parameters of the circuit which you can fill with values later on.

Now if you wanted to use `cirq.simulate` or `cirq.sample` with the parameterized circuit you would also need to specify a value for `theta`.

```
samples_at_theta_equals_2 = sim.sample(
    parameterized_circuit,
    params={theta: 2},
    repetitions=10)
samples_at_theta_equals_2
```

      theta
      out

      0
      2
      1

      1
      2
      0

      2
      2
      1

      3
      2
      1

      4
      2
      1

      5
      2
      0

      6
      2
      1

      7
      2
      0

      8
      2
      1

      9
      2
      0

You can also specify *multiple* values of `theta`, and get samples back for each value.

```
samples_at_multiple_theta = sim.sample(
    parameterized_circuit,
    params=[{theta: 0.5}, {theta: 3.141}],
    repetitions=10)
samples_at_multiple_theta
```

      theta
      out

      0
      0.500
      0

      1
      0.500
      0

      2
      0.500
      0

      3
      0.500
      0

      4
      0.500
      0

      5
      0.500
      0

      6
      0.500
      0

      7
      0.500
      0

      8
      0.500
      0

      9
      0.500
      0

      0
      3.141
      1

      1
      3.141
      1

      2
      3.141
      1

      3
      3.141
      1

      4
      3.141
      1

      5
      3.141
      1

      6
      3.141
      1

      7
      3.141
      1

      8
      3.141
      1

      9
      3.141
      1

Cirq has shorthand notation you can use to sweep `theta` over a range of values.

```
samples_at_swept_theta = sim.sample(
    parameterized_circuit,
    params=cirq.Linspace(theta, start=0, stop=3.14159, length=5),
    repetitions=5)
samples_at_swept_theta
```

      theta
      out

      0
      0.000000
      0

      1
      0.000000
      0

      2
      0.000000
      0

      3
      0.000000
      0

      4
      0.000000
      0

      0
      0.785397
      0

      1
      0.785397
      0

      2
      0.785397
      0

      3
      0.785397
      0

      4
      0.785397
      0

      0
      1.570795
      1

      1
      1.570795
      0

      2
      1.570795
      0

      3
      1.570795
      1

      4
      1.570795
      0

      0
      2.356192
      1

      1
      2.356192
      1

      2
      2.356192
      1

      3
      2.356192
      0

      4
      2.356192
      0

      0
      3.141590
      1

      1
      3.141590
      1

      2
      3.141590
      1

      3
      3.141590
      1

      4
      3.141590
      1

The result value being returned by `sim.sample` is a `pandas.DataFrame` object.
Pandas is a common library for working with table data in python.
You can use standard pandas methods to analyze and summarize your results.

```
import pandas

big_results = sim.sample(
    parameterized_circuit,
    params=cirq.Linspace(theta, start=0, stop=3.14159, length=20),
    repetitions=10_000)

# big_results is too big to look at. Plot cross tabulated data instead.
pandas.crosstab(big_results.theta, big_results.out).plot()
```

## 3. The built-in experiment

Cirq comes with a pre-written Rabi oscillation experiment `cirq.experiments.rabi_oscillations`.
This method takes a `cirq.Sampler`, which could be a simulator or a network connection to real hardware.
The method takes a few more experimental parameters, and returns a result object
that can be plotted.

```
import datetime

result = cirq.experiments.rabi_oscillations(
    sampler=noisy_sim,
    qubit=my_qubit,
    num_points=50,
    repetitions=10000)
result.plot()
```

Notice that you can tell from the plot that you used the noisy simulator you defined earlier.
You can also tell that the amount of depolarization is roughly 10%.

## 4. Exercise: Find the best qubit

As you have seen, you can use Cirq to perform a Rabi oscillation experiment.
You can either make the experiment yourself out of the basic pieces made available by Cirq, or use the prebuilt experiment method.

Now you're going to put this knowledge to the test.

There is some amount of depolarizing noise on each qubit.
Your goal is to characterize every qubit from the Sycamore chip using a Rabi oscillation experiment, and find the qubit with the lowest noise according to the secret noise model.

```
import hashlib

class SecretNoiseModel(cirq.NoiseModel):
    def noisy_operation(self, op):
        # Hey! No peeking!
        q = op.qubits[0]
        v = hashlib.sha256(str(q).encode()).digest()[0] / 256
        yield cirq.depolarize(v).on(q)
        yield op

secret_noise_sampler = cirq.DensityMatrixSimulator(noise=SecretNoiseModel())
```

```
q = cirq.google.Sycamore.qubits[3]
print('qubit', repr(q))
cirq.experiments.rabi_oscillations(
    sampler=secret_noise_sampler,
    qubit=q
).plot()
```

```

```


Trusted Notebook" width="500 px" align="left">

## _*The Simon Algorithm*_

The latest version of this notebook is available on https://github.com/qiskit/qiskit-tutorial.

The Simon algorithm is an example that shows a quantum algorithm can solve a problem exponentially efficient than any classical algorithms. Like the Grover search, it depends on the existence of a blackbox (or, oracle) function that returns a predefined output over specific input or query. In the query-complexity setting, one cares only about how many queries are required to solve a specific problem, but does not care how the blackbox is realized. However, in this tutorial we have to implement it using the unit gates available in QISKit, just like we have done with the Grover search.

We first describe the problem addressed by the Simon algorithm, show the steps of the algorithm and the construction of the blackbox function, and present the experimental results on simulators and real devices.

***
### Contributors
Rudy Raymond

### Qiskit Package Versions

```python
import qiskit
qiskit.__qiskit_version__
```

    {'qiskit': '0.10.3',
     'qiskit-terra': '0.8.1',
     'qiskit-ignis': '0.1.1',
     'qiskit-aer': '0.2.1',
     'qiskit-ibmq-provider': '0.2.2',
     'qiskit-aqua': '0.5.1'}

## The Problem

The Simon algorithm deals with finding a hidden integer $s \in \{0,1\}^n$ from an oracle $f_s$ that satisfies $f_s(x) = f_s(y)$ if and only if $y = x \oplus s$ for all $x \in \{0,1\}^n$.  Here, the $\oplus$ is the bitwise XOR operation. Thus, if $s = 0\ldots 0$, i.e., the all-zero bitstring, then $f_s$ is a 1-to-1 (or, permutation) function. Otherwise, if $s \neq 0\ldots 0$, then $f_s$ is a 2-to-1 function.

The Simon algorithm can find the hidden integer using only $O(n)$ queries to the blackbox function, while any classical algorithms require $\Omega(\sqrt{2^n})$ queries.

## The Algorithm to Find the Hidden Integer

The Simon algorithm finds the hidden integer by combining quantum algorithm with postprocessing on classical computers as below.

1. Prepare two quantum registers each of length $n$ that are initialized to all-zero bitstring: the first one as input and the second one as output of the blackbox function.
$$
|0\rangle |0\rangle
$$

2. Apply Hadamard gates to the first register to create superposition of all possible inputs.
$$
H^{\otimes n} |0\rangle |0\rangle = \frac{1}{\sqrt{2^n}} \sum_{x=0}^{2^n-1} |x\rangle |0\rangle
$$

3. Query the blackbox function to obtain the answer to queries on the second register.
$$
\frac{1}{\sqrt{2^n}} \sum_{x=0}^{2^n-1} U_{f_s}|x\rangle |0\rangle = \frac{1}{\sqrt{2^n}} \sum_{x=0}^{2^n-1} |x\rangle |f_s(x)\rangle
$$

4. Apply Hadamard gates to the first register.
$$
\frac{1}{\sqrt{2^n}} \sum_{x=0}^{2^n-1} H^{\otimes n}|x\rangle |f_s(x)\rangle = \frac{1}{2^n} \sum_{y=0}^{2^n-1}\sum_{x=0}^{2^n-1} (-1)^{x \cdot y}|y\rangle |f_s(x)\rangle = \frac{1}{2^n} \sum_{y=0}^{2^n-1} |y\rangle \sum_{x=0}^{2^n-1} ( (-1)^{x \cdot y} + (-1)^{(x\oplus s) \cdot y} )  |f_s(x)\rangle
$$

    Notice that at the right-hand side of the above equation, because $(-1)^{(x\oplus s) \cdot y} = (-1)^{x\cdot y +  s \cdot y}$ we can conclude that the probability amplitude of the basis state $|y\rangle |f_s(x)\rangle$ is $(-1)^{x\cdot y} (1 + (-1)^{s \cdot y}  )$, which is zero if and only if $s \cdot y = 1$. Thus, measuring the first register will always give $y$ such that $s \cdot y = 0$. Moreover, we can obtain many different $y$'s by repeating Step 1 to 4.

5. Repeat Step 1 to 4 for $m$ times to obtain $y_1, y_2, \ldots, y_m$.

6. **(Classical post-processing)** Let $\mathbf{Y}$ be an $m\times n$ matrix whose $i$-th row is $y_i$ in Step 5, and $\vec{s}$ be the column vector whose $j$-th element is the $j$-th bit of $s$. Solve the following system of linear equations to obtain $s$.
$$
\mathbf{Y} \vec{s} = 0
$$

## The Circuit

We now implement the Simon algorithm with Qiskit by first preparing the environment.

```python
#initialization
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

# importing Qiskit
from qiskit import BasicAer, IBMQ
from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister, execute
from qiskit.compiler import transpile
from qiskit.tools.monitor import job_monitor

# import basic plot tools
from qiskit.tools.visualization import plot_histogram
```

```python
# Load the saved IBMQ accounts
IBMQ.load_accounts()
```

We then set the hidden bitstring $s$ that will be used to construct the circuit of the blackbox function (whose details will be given later). The number of qubits used in the experiment is twice the length of the bitstring $s$.

```python
s = "010101" # the hidden bitstring

assert 1 < len(s) < 20, "The length of s must be between 2 and 19"

for c in s:
    assert c == "0" or c == "1", "s must be a bitstring of '0' and '1'"

n = len(s) #the length of the bitstring
```

We then use Qiskit to create the circuit of the Simon algorithm prior the querying the blackbox function.

```python
# Step 1
# Creating registers
# qubits for querying the oracle and recording its output
qr = QuantumRegister(2*n)
# for recording the measurement on the first register of qr
cr = ClassicalRegister(n)

circuitName = "Simon"
simonCircuit = QuantumCircuit(qr, cr)

# Step 2
# Apply Hadamard gates before querying the oracle
for i in range(n):
    simonCircuit.h(qr[i])

# Apply barrier to mark the beginning of the blackbox function
simonCircuit.barrier()
```

    <qiskit.extensions.standard.barrier.Barrier at 0x11baca0b8>

### Constructing a Circuit for the Blackbox Function

We now details the construction of 1-to-1 and 2-to-1 permutation circuit of the blackbox function. Let us assume the blackbox function receive $|x\rangle|0\rangle$ as input. With regards to a predetermined $s$, the blackbox function writes its output to the second register so that it transforms the input to $|x\rangle|f_s(x)\rangle$ such that $f(x) = f(x\oplus s)$ for all $x \in \{0,1\}^n$.

Such a blackbox function can be realized by the following procedures.

-  Copy the content of the first register to the second register.
$$
|x\rangle|0\rangle \rightarrow |x\rangle|x\rangle
$$

-  **(Creating 1-to-1 or 2-to-1 mapping)** If $s$ is not all-zero, then there is the least index $j$ so that $s_j = 1$. If $x_j = 0$, then XOR the second register with $s$. Otherwise, do not change the second register.
$$
|x\rangle|x\rangle \rightarrow |x\rangle|x \oplus s\rangle~\mbox{if}~x_j = 0~\mbox{for the least index j}
$$

-  **(Creating random permutation)** Randomly permute and flip the qubits of the second register.
$$
|x\rangle|y\rangle \rightarrow |x\rangle|f_s(y)\rangle
$$

Below is the circuit of the blackbox function based on the above procedures.

```python
# Step 3 query the blackbox function
# copy the content of the first register to the second register
for i in range(n):
    simonCircuit.cx(qr[i], qr[n+i])

# get the least index j such that s_j is "1"
j = -1
for i, c in enumerate(s):
    if c == "1":
        j = i
        break

# Creating 1-to-1 or 2-to-1 mapping with the j-th qubit of x as control to XOR the second register with s
for i, c in enumerate(s):
    if c == "1" and j >= 0:
        simonCircuit.cx(qr[j], qr[n+i]) #the i-th qubit is flipped if s_i is 1

# get random permutation of n qubits
perm = list(np.random.permutation(n))

#initial position
init = list(range(n))
i = 0
while i < n:
    if init[i] != perm[i]:
        k = perm.index(init[i])
        simonCircuit.swap(qr[n+i], qr[n+k]) #swap qubits
        init[i], init[k] = init[k], init[i] #marked swapped qubits
    else:
        i += 1

# randomly flip the qubit
for i in range(n):
    if np.random.random() > 0.5:
        simonCircuit.x(qr[n+i])

# Apply the barrier to mark the end of the blackbox function
simonCircuit.barrier()
```

    <qiskit.extensions.standard.barrier.Barrier at 0x11bae6518>

Now we can continue with the steps of the Simon algorithm: applying the Hadamard gates to the first register and measure.

```python
# Step 4 apply Hadamard gates to the first register
for i in range(n):
    simonCircuit.h(qr[i])

# Step 5 perform measurement on the first register
for i in range(n):
    simonCircuit.measure(qr[i], cr[i])

#draw the circuit
simonCircuit.draw(output='mpl')
```

## Experimenting with Simulators

We show the experiments of finding the hidden integer with simulators.

```python
# use local simulator
backend = BasicAer.get_backend("qasm_simulator")
# the number of shots is twice the length of the bitstring
shots = 2*n
job = execute(simonCircuit, backend=backend, shots=shots)
answer = job.result().get_counts()

plot_histogram(answer)
```

We can see that the results of the measurements are the basis whose inner product with the hidden string $s$ are zero.

*(Notice that the basis on the label of the x-axis in the above plot are numbered from right to left instead of from left to right that we used for $s$)*

Gathering the measurement results, we proceed to post-processing with computations that can be done on classical computers.

### Post Processing with Gaussian Elimination

The post processing is done with Gaussian elimination to solve the system of linear equations to determine $s$.

```python
# Post-processing step
# Constructing the system of linear equations Y s = 0
# By k[::-1], we reverse the order of the bitstring
lAnswer = [ (k[::-1],v) for k,v in answer.items() if k != "0"*n  ] #excluding the trivial all-zero
#Sort the basis by their probabilities
lAnswer.sort(key = lambda x: x[1], reverse=True)

Y = []
for k, v in lAnswer:
    Y.append( [ int(c) for c in k ] )

#import tools from sympy
from sympy import Matrix, pprint, MatrixSymbol, expand, mod_inverse

Y = Matrix(Y)

#pprint(Y)

#Perform Gaussian elimination on Y
Y_transformed = Y.rref(iszerofunc=lambda x: x % 2==0) # linear algebra on GF(2)

#to convert rational and negatives in rref of linear algebra on GF(2)
def mod(x,modulus):
    numer, denom = x.as_numer_denom()
    return numer*mod_inverse(denom,modulus) % modulus

Y_new = Y_transformed[0].applyfunc(lambda x: mod(x,2)) #must takecare of negatives and fractional values
#pprint(Y_new)

print("The hidden bistring s[ 0 ], s[ 1 ]....s[",n-1,"] is the one satisfying the following system of linear equations:")
rows, cols = Y_new.shape
for r in range(rows):
    Yr = [ "s[ "+str(i)+" ]" for i, v in enumerate(list(Y_new[r,:])) if v == 1 ]
    if len(Yr) > 0:
        tStr = " + ".join(Yr)
        print(tStr, "= 0")

```

    The hidden bistring s[ 0 ], s[ 1 ]....s[ 5 ] is the one satisfying the following system of linear equations:
    s[ 0 ] = 0
    s[ 1 ] + s[ 5 ] = 0
    s[ 2 ] = 0
    s[ 3 ] + s[ 5 ] = 0
    s[ 4 ] = 0

As seen above, the system of linear equations is satisfied by the hidden integer $s$. Notice that there can be more than one solutions to the system. In fact, all-zero bitstring is a trivial solution to the system of linear equations. But by having more samples one can narrow down the candidates of the solution, and then test the solution by querying the blackbock in the classical manner.

## Experimenting with Real Devices

We see how one can still find out the hidden integer by running the Simon algorithm on real devices. Due to imperfect quantum computers, obtaining the conclusion is not as easy as done with the simulator of perfect quantum computers.

```python
#Use one of the available backends
backend = IBMQ.get_backend("ibmq_16_melbourne")

# show the status of the backend
print("Status of", backend, "is", backend.status())

shots = 10*n #run more experiments to be certain
max_credits = 3          # Maximum number of credits to spend on executions.

simonCompiled = transpile(simonCircuit, backend=backend, optimization_level=1)
job_exp = execute(simonCompiled, backend=backend, shots=shots, max_credits=max_credits)
job_monitor(job_exp)
```

    Status of ibmq_16_melbourne is BackendStatus(backend_name='ibmq_16_melbourne', backend_version='1.0.0', operational=True, pending_jobs=6, status_msg='active')
    Job Status: job has successfully run

```python
results = job_exp.result()
answer = results.get_counts(simonCircuit)

plot_histogram(answer)
```

```python
# Post-processing step
# Constructing the system of linear equations Y s = 0
# By k[::-1], we reverse the order of the bitstring
lAnswer = [ (k[::-1][:n],v) for k,v in answer.items() ] #excluding the qubits that are not part of the inputs
#Sort the basis by their probabilities
lAnswer.sort(key = lambda x: x[1], reverse=True)

Y = []
for k, v in lAnswer:
    Y.append( [ int(c) for c in k ] )

Y = Matrix(Y)

#Perform Gaussian elimination on Y
Y_transformed = Y.rref(iszerofunc=lambda x: x % 2==0) # linear algebra on GF(2)
Y_new = Y_transformed[0].applyfunc(lambda x: mod(x,2)) #must takecare of negatives and fractional values
#pprint(Y_new)

print("The hidden bistring s[ 0 ], s[ 1 ]....s[",n-1,"] is the one satisfying the following system of linear equations:")
rows, cols = Y_new.shape
for r in range(rows):
    Yr = [ "s[ "+str(i)+" ]" for i, v in enumerate(list(Y_new[r,:])) if v == 1 ]
    if len(Yr) > 0:
        tStr = " + ".join(Yr)
        print(tStr, "= 0")
```

    The hidden bistring s[ 0 ], s[ 1 ]....s[ 5 ] is the one satisfying the following system of linear equations:
    s[ 0 ] = 0
    s[ 1 ] = 0
    s[ 2 ] = 0
    s[ 3 ] = 0
    s[ 4 ] = 0
    s[ 5 ] = 0

## References

[1] "[On the power of quantum computation](https://epubs.siam.org/doi/abs/10.1137/S0097539796298637)", Daniel R. Simon, SIAM J. Comput., 26(5), 1474–1483 (1997)


```python
# %load /Users/lzkelley/init.ipy
# Initialize Auto-Reloading Magic
%reload_ext autoreload
%autoreload 2

# Standard Imports
import os
import sys
import json
import copy
import shutil
import datetime
from collections import OrderedDict
from importlib import reload
import warnings

# Package Imports
import astropy as ap
import numpy as np
import scipy as sp
import matplotlib.pyplot as plt
import matplotlib as mpl
import matplotlib.patheffects

import sympy as sym
import sympy.physics
import sympy.physics.units as spu
from sympy.physics.units.systems import cgs
from sympy.physics.units import c, cm, g, s, km, gravitational_constant as G
pi = sym.pi

import h5py
import pandas as pd
import tqdm.notebook as tqdm
import corner

from zcode.constants import *
import zcode.plot as zplot
import zcode.math as zmath
import zcode.inout as zio
from zcode.inout.notebook import *
import zcode.astro as zastro

import kalepy as kale

# Silence annoying numpy errors
np.seterr(divide='ignore', invalid='ignore', over='ignore')
warnings.filterwarnings("ignore", category=UserWarning)

# Plotting settings
mpl.rc('font', **{'family': 'serif', 'sans-serif': ['Times'], 'size': 12})
mpl.rc('lines', solid_capstyle='round')
mpl.rc('mathtext', fontset='cm')
plt.rcParams.update({'grid.alpha': 0.5})

sym.init_printing(use_latex='mathjax')

```

```python
import cosmopy
reload(cosmopy)
cosmo = cosmopy.Cosmology()
```

```python
NUM = 100
redz = 10**np.random.uniform(-2, 1, int(NUM))
redz = sorted(redz)

funcs = ['tage', 'tlbk', 'dlum', 'dcom']
checks = ['age', 'lookback_time', 'luminosity_distance', 'comoving_distance']
funcs_for = ["z_to_" + ff for ff in funcs]
funcs_rev = [ff + "_to_z" for ff in funcs]

fig, axes = plt.subplots(figsize=[20, 6], ncols=len(funcs))

for ii, (ffor, frev) in enumerate(zip(funcs_for, funcs_rev)):
    val = getattr(cosmo, ffor)(redz)
    vz = getattr(cosmo, frev)(val)
    err = (vz - redz) / np.minimum(vz, redz)

    ax = axes[ii]
    ax.set(xscale='log', xlabel='redz', yscale='linear', ylabel='error')
    ax.plot(redz, err)

plt.show()

```

```python
NUM = 100
redz = 10**np.random.uniform(-2, 1, int(NUM))
redz = np.sort(redz)

funcs = ['tage', 'tlbk', 'dlum', 'dcom']
funcs_check = ['age', 'lookback_time', 'luminosity_distance', 'comoving_distance']
funcs_for = ["z_to_" + ff for ff in funcs]
# funcs_rev = [ff + "_to_z" for ff in funcs]

fig, axes = plt.subplots(figsize=[20, 6], ncols=len(funcs))

for ii, (func, check) in enumerate(zip(funcs_for, funcs_check)):
    val = getattr(cosmo, func)(redz)
    chk = getattr(cosmo, check)(redz).cgs.value
    err = (val - chk) / np.minimum(chk, val)

    ax = axes[ii]
    ax.set(xscale='log', xlabel='redz', yscale='linear', ylabel='error')
    ax.plot(redz, err)

plt.show()

```


# Лабораторая работа 4.06
# Определение размера щели по картине дифракции Фраунгофера

## Выполнил: Коняхин Всеволод Владимирович, M32051

## Краткие теоретические сведения
Явления дифракции принято классифицировать в зависимости от расстояний
источника и точки наблюдения (экрана) от препятствия, поставленного па пути
распространения света. Если эти расстояния очень велики (бесконечно велики), то
дифракция называется дифракцией в параллельных лучах или дифракцией
Фраунгофера. В противоположном случае говорят о дифракции в непараллельных
лучах или дифракции Френеля

Наиболее простым случаем дифракции Фраунгофера является дифракция на
узкой щели заданной ширины b. Высота щели считается стремящейся к
бесконечности. Несмотря на простоту описания, данный случай имеет в то же
время большую практическую значимость, поскольку полученные результаты и
зависимости используются для описания дифракции на множестве одинаковых
щелей, т.е.решетках, прямоугольных отверстиях и т.д.

## Цель работы
Определение ширины щели по картине дифракции в дальней зоне.

## Рабочие формулы и исходные данные

### Формулы

$Z = x_{Э} - x_{О}$

$\Delta x = x_{m+1} - x_{m} = \frac{\lambda}{b} \cdot Z$

$K = \frac{\lambda}{b}$

### Исходные данные
Длина волны $\lambda = (632.82 \pm 0.01)$ нм

## Схема установки

Ниже представлена оптическая схема опыта:

Так выглядит лабораторная установка:

## Результаты измерений и расчеты

```python
import sympy
import scipy
import numpy as np
import pandas as pd
from scipy.signal import argrelextrema
import matplotlib.pyplot as plt
plt.rcParams["figure.figsize"] = (10,5)
%matplotlib inline
```

### Координаты последовательных минимумов (в мм) при разных положениях объекта 32

```python
df32 = pd.DataFrame({'X объекта, мм': [220, 270, 320, 370, 420],
                     '$x_{m1}$, мм' : [19, 18, 17, 16, 16],
                     '$x_{m2}$, мм' : [39, 34, 34, 32, 31],
                     '$x_{m3}$, мм' : [59, 53, 52, 48, 47],
                     '$x_{m4}$, мм' : [78, 74, 69, 64, 62],
                     '$x_{m5}$, мм' : [98, 93, 87, 81, 78]})

x_screen = 1200

alpha = 632.82 * 10 ** (-9)
delta_alpha = 0.01 * 10 ** (-9)

df32
```

      X объекта, мм
      $x_{m1}$, мм
      $x_{m2}$, мм
      $x_{m3}$, мм
      $x_{m4}$, мм
      $x_{m5}$, мм

      0
      220
      19
      39
      59
      78
      98

      1
      270
      18
      34
      53
      74
      93

      2
      320
      17
      34
      52
      69
      87

      3
      370
      16
      32
      48
      64
      81

      4
      420
      16
      31
      47
      62
      78

```python
print('Координата экрана, {} мм'.format(x_screen))
```

    Координата экрана, 1200 мм

Посчитаем расстояние $Z$, период дифракционной картины $\Delta x$:
$Z = x_{Э} - x_{О}$
$\Delta x = \frac{x_{min\space max} - x_{min\space min}}{number\space of\space minimums}$

### Расстояние между объектом и экраном (Z) и период дифференционной картины (ΔX) при различных координатах объекта 32

```python
df32_calc = pd.DataFrame({'X объекта, мм': [220, 270, 320, 370, 420]})
df32_calc['Z, мм'] = x_screen - df32_calc['X объекта, мм']
df32_calc['$\Delta x$, мм'] = (df32['$x_{m5}$, мм'] - df32['$x_{m1}$, мм']) / 5
df32_calc
```

      X объекта, мм
      Z, мм
      $\Delta x$, мм

      0
      220
      980
      15.8

      1
      270
      930
      15.0

      2
      320
      880
      14.0

      3
      370
      830
      13.0

      4
      420
      780
      12.4

### Координаты последовательных минимумов (в мм) при разных положениях объекта 33

```python
df33 = pd.DataFrame({'X объекта, мм': [170, 220, 270, 320, 370],
                     '$x_{m1}$, мм' : [13, 12, 12, 11, 11],
                     '$x_{m2}$, мм' : [26, 25, 24, 22, 21],
                     '$x_{m3}$, мм' : [39, 37, 35, 33, 31],
                     '$x_{m4}$, мм' : [52, 49, 47, 44, 42],
                     '$x_{m5}$, мм' : [65, 62, 59, 56, 53]})

df33
```

      X объекта, мм
      $x_{m1}$, мм
      $x_{m2}$, мм
      $x_{m3}$, мм
      $x_{m4}$, мм
      $x_{m5}$, мм

      0
      170
      13
      26
      39
      52
      65

      1
      220
      12
      25
      37
      49
      62

      2
      270
      12
      24
      35
      47
      59

      3
      320
      11
      22
      33
      44
      56

      4
      370
      11
      21
      31
      42
      53

### Расстояние между объектом и экраном (Z) и период дифференционной картины (ΔX) при различных координатах объекта 33

```python
df33_calc = pd.DataFrame({'X объекта, мм': [170, 220, 270, 320, 370]})
df33_calc['Z, мм'] = x_screen - df33_calc['X объекта, мм']
df33_calc['$\Delta x$, мм'] = (df33['$x_{m5}$, мм'] - df33['$x_{m1}$, мм']) / 5
df33_calc
```

      X объекта, мм
      Z, мм
      $\Delta x$, мм

      0
      170
      1030
      10.4

      1
      220
      980
      10.0

      2
      270
      930
      9.4

      3
      320
      880
      9.0

      4
      370
      830
      8.4

### Построения аппроксимирующей прямой
Для этого обучим линейную регрессию на $\Delta x$ от $Z$:

```python
from sklearn import linear_model

regr_32 = linear_model.LinearRegression()
X_32 = np.array(df32_calc['Z, мм']).reshape(-1, 1)
y_32 = np.array(df32_calc['$\Delta x$, мм'])
regr_32.fit(X_32, y_32)

regr_33 = linear_model.LinearRegression()
X_33 = np.array(df33_calc['Z, мм']).reshape(-1, 1)
y_33 = np.array(df33_calc['$\Delta x$, мм'])
regr_33.fit(X_33, y_33)

print('Для объекта 32, K = {:.4f}, b = {:.3f}'.format(regr_32.coef_[0], regr_32.intercept_))
print('Для объекта 33, K = {:.2f}, b = {:.3f}'.format(regr_33.coef_[0], regr_33.intercept_))
```

    Для объекта 32, K = 0.0176, b = -1.448
    Для объекта 33, K = 0.01, b = 0.140

```python
fig, ax = plt.subplots(figsize=(20, 5))
ax.set_title('Зависимость периода дифракционной картины от расстояния между экраном и объектом для двух объектов')
X_33 = X_33.reshape(5)
ax.scatter(X_33, y_33, c='r')
k, b = regr_33.coef_[0], regr_33.intercept_
x = np.linspace(750, 1050, 50)
ax.plot(x, np.polyval([k, b], x), 'r--')

X_32 = X_32.reshape(5)
ax.scatter(X_32, y_32, c='b')
k, b = regr_32.coef_[0], regr_32.intercept_
x = np.linspace(750, 1050, 50)
ax.plot(x, np.polyval([k, b], x), 'b--')

plt.show()
```

### Найдем ширину щели для каждого объекта 32 и 33:
$b = \frac{\lambda}{K}$

```python
b_32 = alpha / regr_32.coef_[0]
b_33 = alpha / regr_33.coef_[0]

print('Ширина щели для объекта 32: {:.2f} мкм'.format(b_32 * 10 ** 6))
print('Ширина щели для объекта 33: {:.2f} мкм'.format(b_33 * 10 ** 6))
```

    Ширина щели для объекта 32: 35.96 мкм
    Ширина щели для объекта 33: 63.28 мкм

Найдем погрешность для ширины щели:
$\Delta K = \sqrt{\frac{\sum(y_i-Kx_i-b)^2}{(n-2)\sum(x_i-\overline{x})^2}}$

$\Delta b = \sqrt{(\frac{\Delta d}{\Delta \lambda}\cdot \Delta \lambda)^2 +  (\frac{\Delta d}{\Delta K}\cdot \Delta K)^2 } = \sqrt{(\frac{1}{K}\Delta \lambda)^2 + (\frac{\lambda}{K^2}\Delta K)^2}$

```python
from sklearn.metrics import mean_squared_error

def calculate_error(Xs, ys, K, b):
    mse = mean_squared_error(ys, (K * Xs + b)) * 5 / 3
    delta_k = np.sqrt(mse / np.sum((Xs - np.mean(Xs)) ** 2))
    delta_d = np.sqrt((delta_alpha / K) ** 2 + (alpha * delta_k / K ** 2) ** 2)
    return delta_d

delta_b_32 = calculate_error(X_32, y_32, regr_32.coef_[0], regr_32.intercept_)
print('Погрешность для щели для объекта 32: {} мкм'.format(int(round(delta_b_32 * 10 ** 6))))

delta_b_33 = calculate_error(X_33, y_33, regr_33.coef_[0], regr_33.intercept_)
print('Погрешность для щели для объекта 33: {} мкм'.format(int(round(delta_b_33 * 10 ** 6))))
```

    Погрешность для щели для объекта 32: 2 мкм
    Погрешность для щели для объекта 33: 3 мкм

#### Ширина щели для объекта №32:
$b_{32} = 36 \pm 2 \space мкм$

#### Ширина щели для объекта №33:
$b_{33} = 63 \pm 3 \space мкм$

### Выводы и анализ результатов работы
В ходе данной работы была изучена картина дифракции Фраунгофера.
Для двух объектов были измерены координаты последовательных минимумов дифракционной картины на экране,
были вычислены периоды дифракционной картины при разных расстояниях от экрана до объъекта.
Зависимость периода от расстояния получилась линейной, что сходится с теорией.
Обучив линейную регрессию на пяти точках для каждого объекта, были вычислены ширины щели для объектов 32 и 33.
Они получились равными $(36 \pm 2) \space мкм$ и $(63 \pm 3) \space мкм$, соответственно, что разумно соотносится с размерами объектов.


# Generative models - auto-encoders

### Author: Philippe Esling (esling@ircam.fr)

In this course we will cover
1. A brief introduction to [generative models](#generative)
2. A formal presentation of [Auto-Encoders](#ae) (AEs)
3. An explanation of how to [implement AEs](#implement)
4. An [application](#application) of AEs for modeling images
4. An practical exemple of [convolutional denoising AEs](#denoising) for image data **(exercise)**

## Generative models

### Supervised refresher

Until now, we have mostly discussed models that are developed for _supervised_ and _discriminative_ tasks. To formalize this problem, we have a set of data $\{\mathbf{x}_{i}, \mathbf{y}_{i}\}_{i\in[1,n]}$, where the $\mathbf{x}_{i}$ and $\mathbf{y}_{i}$ are linked. Therefore, we want to approximate this relation through
\begin{equation}
    \mathbf{\hat{y}} = \mathcal{F}_\mathbf{\theta}(\mathbf{x})
\end{equation}
where we train the parameters $\mathbf{\theta}$ so that $\mathbf{\hat{y}}\approx\mathbf{y}$. The existence of a label $\mathbf{y}$ ("correct answer") defines a _supervised_ problem

### Going unsupervised

In some cases, we might only have a set of data $\{\mathbf{x}_{i}\}_{i\in[1,n]}$, and still be interested in learning some underlying properties or structure of this set. In that case, the problem is _unsupervised_ as we have to learn without a given answer.

Here, we can turn to _generative_ models [[1](#reference1)], which allows to create new data instances based on the observation of existing examples. Although these models are more naturally defined in a _probabilistic way_, we will assume that we have some simple _code_ $\mathbf{z}$, which allows to control the properties of the generation, and need to learn
\begin{equation}
    \mathbf{\hat{x}} = \mathcal{F}_\mathbf{\theta}(\mathbf{z})
\end{equation}
where we still need to learn $\mathbf{\theta}$, so that $\mathbf{\hat{x}}$ have similar properties to that of the examples in $\{\mathbf{x}_{i}\}_{i\in[1,n]}$.

Now the problem to solve is how we can learn directly from a set of data.

## Auto-encoders

One way to understand a set of data is to try to _compress_, or _simplify_ the corresponding dataset. So the idea is to learn simultaneously how to _encode_ our unlabeled input $\{\mathbf{x}_{i}\}_{i\in[1,n]}$ and to _decode_ the corresponding representation. This idea give rise to the notion of **auto-encoder**.

### Architecture

The auto-encoder is an unsupervised architecture originally proposed to perform _dimensionality reduction_ [[3](#reference3)]. As its name indicates, we will try to train this model to learn an efficient _encoding_ $\mathbf{z}$ of unlabeled input data $\mathbf{x}$. The only way to learn efficient parameters is to also learn a _decoding_ function to _reconstruct_ $\mathbf{x}$ from $\mathbf{z}$.

As shown here, a first model $\mathcal{E}_\phi$ _encodes_ the input into a _latent code_ $\mathbf{z}$ in order to provide a low-dimensional representation of the data. A second model $\mathcal{D}_\theta$ designated as the _decoder_ aims to generate outputs from $\mathbf{z}$ that are as close to the original inputs as possible.

### Formal definition

The latent code $\mathbf{z}$ can be seen as a compressed abstract representation, and may be used as an intermediate space for analysis or generation. This helps to govern the distribution of the data through a simpler and higher-level representation, while enhancing the _expressiveness_ of the generative model.
The behaviour of an auto-encoder can be formalized as:

$$
\begin{align}
\mathbf{z} &= \mathcal{E}_\phi(\mathbf{x}) \\
\mathbf{\hat{x}} &= \mathcal{D}_\theta(\mathbf{z})
\end{align}
$$

with the _encoder_ $\mathcal{E}_\phi$ and _decoder_ $\mathcal{D}_\theta$ functions parameterized respectively by $\phi$ and $\theta$. As we can see this defines the reconstruction relationship
$$
    \mathbf{\hat{x}} = \mathcal{D}_\theta(\mathcal{E}_\phi(\mathbf{x}))
$$

### Training

The training of an auto-encoder consists in finding the optimal functions of encoding $\mathcal{E}^*$ and decoding $\mathcal{D}^*$ by evaluating the \textit{reconstruction error} $\mathcal{L}$ between $\mathbf{x}$ and $\mathbf{\hat{x}}$, such that
\begin{equation}
    \mathcal{E}^*, \mathcal{D}^* = arg\,min_{ \phi, \theta}{\mathcal{L}}(\mathbf{x}, \mathcal{D}_\theta(\mathcal{E}_\phi(\mathbf{x})))
\end{equation}

As the latent space usually has a smaller dimensionality than the input, it acts as an incentive for the network to find the main attributes of variations in the dataset (and also explains its use for _dimensionality reduction_).

### Variants and discussion

There are several variants of auto-encoders, such as denoising auto-encoders or variational auto-encoders. Each address some downside of the basic AE model. For instance, the deterministic nature of the basic auto-encoder implies a point-wise mapping of the latent space, meaning that not all the latent positions can be leveraged to produce relevant reconstructions. Because of this reason, there is no way to ensure that the latent space could allow a robust generalization and that any random $\mathbf{z}$ would generate a meaningful output.

## Implementation

Here, we discuss how we can implement and train a simple auto-encoder network in Pytorch. As discussed earlier, an AE is composed of two parts, an **encoder** and a **decoder**. The goal of the encoder is to "compress" the dataset, representing its principal features with a very small code, while the goal of the decoder is to learn how to reproduce the initial input from this code. Hence, we will first need to use some basic imports and definition to setup our problem

### Import Pytorch and other libraries

```python
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
import torchvision
from helper_plot import hdr_plot_style
hdr_plot_style();
```

### Load the dataset

To start with a pragmatic and simple to understand example, we  will try to train the basic AE using the Fashion MNIST dataset. This dataset contains images of size 28x28 pixels, with different pieces of clothing. The following code allows to load (and eventually download) the dataset, by using the `torchvision.datasets` module. We also plot some randomly selected test examples

```python
import matplotlib
dataset_dir = './data'
# Going to use 80%/20% split for train/valid
valid_ratio = 0.2
# Load the dataset for the training/validation sets
train_valid_dataset = torchvision.datasets.FashionMNIST(root=dataset_dir, train=True, transform=torchvision.transforms.ToTensor(), download=True)
# Split it into training and validation sets
nb_train = int((1.0 - valid_ratio) * len(train_valid_dataset))
nb_valid =  int(valid_ratio * len(train_valid_dataset))
train_dataset, valid_dataset = torch.utils.data.dataset.random_split(train_valid_dataset, [nb_train, nb_valid])
# Load the test set
test_dataset = torchvision.datasets.FashionMNIST(root=dataset_dir, transform=torchvision.transforms.ToTensor(),train=False)
# Prepare
num_threads = 4     # Loading the dataset is using 4 CPU threads
batch_size  = 128   # Using minibatches of 128 samples
train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=batch_size, num_workers=num_threads)
valid_loader = torch.utils.data.DataLoader(dataset=valid_dataset, batch_size=batch_size, shuffle=False, num_workers=num_threads)
test_loader = torch.utils.data.DataLoader(dataset=test_dataset,batch_size=batch_size,shuffle=False,num_workers=num_threads)
```

Here, we are going to check the properties of our different sets and also plot some random examples, in order to better understand what type of data we will be working with.

```python
print("The train set contains {} images, in {} batches".format(len(train_loader.dataset), len(train_loader)))
print("The validation set contains {} images, in {} batches".format(len(valid_loader.dataset), len(valid_loader)))
print("The test set contains {} images, in {} batches".format(len(test_loader.dataset), len(test_loader)))
nsamples = 10
classes_names = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat', 'Sandal','Shirt', 'Sneaker', 'Bag', 'Ankle boot']
imgs_test, labels = next(iter(train_loader))
fig = plt.figure(figsize=(20,5))
for i in range(nsamples):
    ax = plt.subplot(1,nsamples, i+1)
    plt.imshow(imgs_test[i, 0, :, :], vmin=0, vmax=1.0, cmap=matplotlib.cm.gray)
    ax.set_title("{}".format(classes_names[labels[i]]), fontsize=15)
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)
```

## Basic auto-encoder

We recall here that in order to define an autoencoder, we will need an `encoder`, which compresses the images into a small latent vector, and a `decoder`, that reconstructs the original image from this code. Here, we will start very basic and define the encoder and decoder as simple `Dense` layers. To define the model simply, we will use the [Keras API](https://www.tensorflow.org/guide/keras/custom_layers_and_models) defined in the `tensorflow.keras` module (Note that we pre-loaded the `layers` submodule

```python
class AE(nn.Module):
    def __init__(self, encoding_dim):
        super(AE, self).__init__()
        self.latent_dim = encoding_dim
        self.hidden_dim = 256
        self.encoder = nn.Sequential(
          nn.Linear(28 * 28, self.hidden_dim), nn.ReLU(),
          nn.Linear(self.hidden_dim, self.hidden_dim), nn.ReLU(),
          nn.Linear(self.hidden_dim, self.latent_dim), nn.Sigmoid()
        )
        self.decoder = nn.Sequential(
          nn.Linear(self.latent_dim, self.hidden_dim), nn.ReLU(),
          nn.Linear(self.hidden_dim, self.hidden_dim), nn.ReLU(),
          nn.Linear(self.hidden_dim, 28 * 28), nn.Sigmoid()
        )

    def forward(self, x):
        encoded = self.encoder(x.reshape(-1, 28 * 28))
        decoded = self.decoder(encoded)
        return decoded.reshape(-1, 1, 28, 28)
```

Here we can see that the model depends on a given `encoding_dim` variable, which defines the size of the latent code. Therefore, we can instantiate our model arbitrarliy with `64` dimensions

```python
latent_dim = 64
model = AE(latent_dim)
```

The only remaining part that we did not discuss yet is what type of _loss_ (defined as $\mathcal{L}$) we can use to train our model. First, we will simply rely on the _Mean Squared Error_ (MSE) loss, which is defined as
\begin{equation}
    \mathcal{L}_{MSE}(\hat{\mathbf{x}}, \mathbf{x}) = \mid \hat{\mathbf{x}}, \mathbf{x} \mid^{2}
\end{equation}

```python
# Loss function that we will use
loss_fn = torch.nn.MSELoss(reduction='sum')
```

### Training the model

Train the model using `x_train` as both the input and the target. The `encoder` will learn to compress the dataset from 784 dimensions to the latent space, and the `decoder` will learn to reconstruct the original images.
.

```python
# Learning rate
learning_rate = 1e-4
# Optimizer to fit the weights of the network
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
for t in range(20):
    full_loss = torch.Tensor([0])
    # Forward pass: compute predicted y by passing x to the model.
    for i, (x, _) in enumerate(train_loader):
        y_pred = model(x)
        # Compute the loss.
        loss = loss_fn(y_pred, x)
        # Before the backward pass, zero all of the network gradients
        optimizer.zero_grad()
        # Backward pass: compute gradient of the loss with respect to parameters
        loss.backward()
        # Calling the step function to update the parameters
        optimizer.step()
    print(loss)
```

Now that the model is trained, we can test it by encoding and decoding images from the test set.

```python
encoded_imgs = model.encoder(imgs_test.reshape(-1,28*28))
decoded_imgs = model.decoder(encoded_imgs).reshape(-1,28,28).detach().numpy()
```

By plotting the images, we can see that the model is able to perform an adequate (yet somewhat blurry) reconstruction of the input images. The interesting point is that this reconstruction comes from a code of only `64` dimensions, whereas the original images have `784` dimensions.

```python
n = 10
plt.figure(figsize=(20, 4))
for i in range(n):
    # display original
    ax = plt.subplot(2, n, i + 1)
    plt.imshow(imgs_test[i, 0])
    plt.title("original"); plt.gray()
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)
    # display reconstruction
    ax = plt.subplot(2, n, i + 1 + n)
    plt.imshow(decoded_imgs[i])
    plt.title("reconstructed"); plt.gray()
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)
plt.show()
```

### Improvements

Even though this very basic example seems to work, several improvements can be made over the original model. First, we can see that the overall framework does not depend on the exact nature of the `encoder` and `decoder`.

***
1. We rewrite the original class to accept any type of architecture for these (see code below).
2. Fill the missing code to have a new AE model
***

```python
class AE(nn.Module):
    def __init__(self, encoder, decoder, encoding_dim):
        super(AE, self).__init__()
        self.latent_dim = encoding_dim
        self.encoder = encoder
        self.decoder = decoder

    def forward(self, x):
        encoded = self.encoder(x)
        decoded = self.decoder(encoded)
        return decoded

######################
# YOUR CODE GOES HERE
######################

encoder = ...
decoder = ...
model = ...
```

Second, we only used the train dataset, whereas the use of a validation and test sets allows to ensure that we do not overfit our model.

***
1. Write a separate train function, that can be called on different sets
2. Re-write the training loop to test overfitting
***

```python
######################
# YOUR CODE GOES HERE
######################
```

## Exercise: Denoising AE

Imagine (for the sake of argument), that we choose an encoding dimension which is of same dimensionality as the input one. Then, one huge problem is that nothing prevents the AE from simply learning the _identity_ function (try to imagine why). An autoencoder can also be trained to remove noise from images. This type of _regularization_ prevents the model from learning this degenerate situation.

In this exercise, you will need to create your own denoising AE, by relying on a noisy version of the Fashion MNIST dataset (adding random Gaussian noise to each image). You will then train an autoencoder using the noisy image as input, and the original image as the target.

Let's reimport the dataset to omit the modifications made earlier.

```python
class GaussianNoise(object):
    def __init__(self, mean=0., std=1.):
        self.std = std
        self.mean = mean

    def __call__(self, tensor):
        return tensor + torch.randn(tensor.size()) * self.std + self.mean

    def __repr__(self):
        return self.__class__.__name__ + '(mean={0}, std={1})'.format(self.mean, self.std)
```

Here, we create two new train and test sets by adding random noise to the images

```python
# Load the datasets and use our Gaussian noise transform
train_valid_dataset = torchvision.datasets.FashionMNIST(root=dataset_dir, train=True, transform=torchvision.transforms.Compose([torchvision.transforms.ToTensor(), GaussianNoise(0., 0.1)]), download=True)
train_dataset, valid_dataset = torch.utils.data.dataset.random_split(train_valid_dataset, [nb_train, nb_valid])
test_dataset = torchvision.datasets.FashionMNIST(root=dataset_dir, transform=torchvision.transforms.Compose([torchvision.transforms.ToTensor(), GaussianNoise(0., 0.1)]),train=False)
# Create loaders
train_loader_noisy = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=batch_size, num_workers=num_threads)
valid_loader_noisy = torch.utils.data.DataLoader(dataset=valid_dataset, batch_size=batch_size, shuffle=False, num_workers=num_threads)
test_loader_noisy = torch.utils.data.DataLoader(dataset=test_dataset,batch_size=batch_size,shuffle=False,num_workers=num_threads)
```

Plot the noisy images.

```python
x_test, labels = next(iter(test_loader))
x_test_noisy, labels = next(iter(test_loader_noisy))
n = 10
plt.figure(figsize=(20, 4))
for i in range(n):
    ax = plt.subplot(2, n, i + 1)
    plt.title("original")
    plt.imshow(x_test[i].squeeze())
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)
    plt.gray()
    ax = plt.subplot(2, n, i + 1 + n)
    plt.title("original + noise")
    plt.imshow(x_test_noisy[i].squeeze())
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)
    plt.gray()
plt.show()
```

### Define a convolutional autoencoder

In this example, you will train a convolutional autoencoder using  [Conv2D](https://www.tensorflow.org/api_docs/python/tf/keras/layers/Conv2D) layers in the `encoder`, and [Conv2DTranspose](https://www.tensorflow.org/api_docs/python/tf/keras/layers/Conv2DTranspose) layers in the `decoder`.

```python
######################
# YOUR CODE GOES HERE
######################

class DenoisingAE(AE):
    def __init__(self):
        super(DenoisingAE, self).__init__()
        self.encoder = ...
        self.decoder = ...

    def forward(self, x):
        encoded = ...
        decoded = ...
        return decoded

autoencoder = ...
```

Write down the full optimization loop to optimize your denoising auto-encoder

```python
######################
# YOUR CODE GOES HERE
######################
```

Plot both the noisy images and the denoised images produced by the autoencoder to check that your implementation is correct

```python
######################
# YOUR CODE GOES HERE
######################
encoded_imgs = ...
decoded_imgs = ...
plt.figure(figsize=(20, 4))
for i in range(n):
    ...
plt.show()
```

### References

[1] Rezende, Danilo Jimenez, and Shakir Mohamed. "Variational inference with normalizing flows." _arXiv preprint arXiv:1505.05770_ (2015). [link](http://arxiv.org/pdf/1505.05770)

[2] Kingma, Diederik P., Tim Salimans, and Max Welling. "Improving Variational Inference with Inverse Autoregressive Flow." _arXiv preprint arXiv:1606.04934_ (2016). [link](https://arxiv.org/abs/1606.04934)

[3] Kingma, D. P., & Welling, M. Auto-encoding variational bayes. arXiv preprint arXiv:1312.6114. (2013). [link](https://arxiv.org/pdf/1312.6114)

[4] Rezende, D. J., Mohamed, S., & Wierstra, D. Stochastic backpropagation and approximate inference in deep generative models. arXiv preprint arXiv:1401.4082. (2014). [link](https://arxiv.org/pdf/1401.4082)

[5] Gulrajani, I., Kumar, K., Ahmed, F., Taiga, A. A., Visin, F., Vazquez, D., & Courville, A. (2016). Pixelvae: A latent variable model for natural images. arXiv preprint arXiv:1611.05013. [link](https://arxiv.org/pdf/1611.05013)

[6] Van den Oord, A., & Vinyals, O. (2017). Neural discrete representation learning. In NIPS 2017 (pp. 6306-6315). [link](http://papers.nips.cc/paper/7210-neural-discrete-representation-learning.pdf)


# in this notebook

Evaluating the full Lyapunov spectra of the rotating wave phenomena.

Based on the Jupyter Notebook 2016-10-27-LP-Lyapunov-spectra-coupled-systems.

# Setup
## Loading simulation parameters

```python
Coup_scheme='XYZ' # change here to 'XYZ', 'X', 'Y' or 'Z'
#####################

LoadFile='DATA_HUGE_CoupTru_'+Coup_scheme+'_Hildas_PRE_6Rosslers_Ci0_Cf0_2step0_001_Ttime30000'
PathLoadFile=r'/media/leo/76E45D14E45CD7C7/BigData/RotatingWaves/Rossler/2016-08-26-arquive-Manipulating-Coupling/Coupling-tru-'+Coup_scheme

```

```python
# Loading data poincare section
DataFile=h5py.File(PathLoadFile+r'/'+LoadFile+'.hdf5', "r") # creating a file

# Initializing some variables
#Coup_list=DataFile.attrs['Coupling force list']
Coup_list_keys=DataFile.keys()#s_time=MyFile.attrs['Sampling Time (s)']
args_n=DataFile.attrs['System parameters (a,b,c)']
H=Matrix(DataFile.attrs['Coupling through (x,y,z):'].astype('int'))
Nsys=DataFile.attrs['Frequencies'].shape[0]
N_coup=len(Coup_list_keys)

#Vendo os atributos
print '\n - Atributos do arquivo: \n', DataFile.attrs.items()

#DataFile.close()
```

     - Atributos do arquivo:
    [(u'System parameters (a,b,c)', array([ 0.45,  2.  ,  4.  ])), (u'Frequencies', array([ 1.,  1.,  1.,  1.,  1.,  1.])), (u'Integration Time (s)', 0.01), (u'Sampling Time (s)', 0.01), (u'Sampling frequency (Hz)', 100.0), (u'Time length+transient (s)', 30000.0), (u'Time length (s)', 30000.0), (u'Transient (s)', 0.0), (u'Data shape', nan), (u'Coupling force list', array([ 0.   ,  0.001,  0.002,  0.003,  0.004,  0.005,  0.006,  0.007,
            0.008,  0.009,  0.01 ,  0.011,  0.012,  0.013,  0.014,  0.015,
            0.016,  0.017,  0.018,  0.019,  0.02 ,  0.021,  0.022,  0.023,
            0.024,  0.025,  0.026,  0.027,  0.028,  0.029,  0.03 ,  0.031,
            0.032,  0.033,  0.034,  0.035,  0.036,  0.037,  0.038,  0.039,
            0.04 ,  0.041,  0.042,  0.043,  0.044,  0.045,  0.046,  0.047,
            0.048,  0.049,  0.05 ,  0.051,  0.052,  0.053,  0.054,  0.055,
            0.056,  0.057,  0.058,  0.059,  0.06 ,  0.061,  0.062,  0.063,
            0.064,  0.065,  0.066,  0.067,  0.068,  0.069,  0.07 ,  0.071,
            0.072,  0.073,  0.074,  0.075,  0.076,  0.077,  0.078,  0.079,
            0.08 ,  0.081,  0.082,  0.083,  0.084,  0.085,  0.086,  0.087,
            0.088,  0.089,  0.09 ,  0.091,  0.092,  0.093,  0.094,  0.095,
            0.096,  0.097,  0.098,  0.099,  0.1  ,  0.101,  0.102,  0.103,
            0.104,  0.105,  0.106,  0.107,  0.108,  0.109,  0.11 ,  0.111,
            0.112,  0.113,  0.114,  0.115,  0.116,  0.117,  0.118,  0.119,
            0.12 ,  0.121,  0.122,  0.123,  0.124,  0.125,  0.126,  0.127,
            0.128,  0.129,  0.13 ,  0.131,  0.132,  0.133,  0.134,  0.135,
            0.136,  0.137,  0.138,  0.139,  0.14 ,  0.141,  0.142,  0.143,
            0.144,  0.145,  0.146,  0.147,  0.148,  0.149,  0.15 ,  0.151,
            0.152,  0.153,  0.154,  0.155,  0.156,  0.157,  0.158,  0.159,
            0.16 ,  0.161,  0.162,  0.163,  0.164,  0.165,  0.166,  0.167,
            0.168,  0.169,  0.17 ,  0.171,  0.172,  0.173,  0.174,  0.175,
            0.176,  0.177,  0.178,  0.179,  0.18 ,  0.181,  0.182,  0.183,
            0.184,  0.185,  0.186,  0.187,  0.188,  0.189,  0.19 ,  0.191,
            0.192,  0.193,  0.194,  0.195,  0.196,  0.197,  0.198,  0.199])), (u'Number of systems', 6), (u'Topology', 'Chain'), (u'Obs', 'Estrutura eh [x1,x2,...x_Nsys,y1,y2,...y_Nsys,z1,z2,...z_Nsys]'), (u'Coupling through (x,y,z):', array([ 1.,  1.,  1.]))]

### Reordering state vector:
 * Original data format: ['x1' 'x2' 'x3' 'y1' 'y2' 'y3' 'z1' 'z2' 'z3']
 * Format used in the Lyapunov function: ['x1' 'y1' 'z1' 'x2' 'y2' 'z2' 'x3' 'y3' 'z3']

> xData=np.array([i+str(j) for i in ['x', 'y', 'z'] for j in range(1,4)])

> print xData

> print xData.reshape((3,3)).T.ravel()

## Symbolic vectors and matrices

```python
Vars=['x','y','z']

sys_i=[str(i) for i in np.arange(1,Nsys+1)]

labels_temp=[j+i+' ' for i in sys_i for j in Vars]
labels_temp='None '+''.join(labels_temp) # none para que X[1] corresponda ao simbolo x1
#print labels_temp

# state variables
X=symbols(labels_temp, real=True)
vecX=Matrix(X[1:])

vecX.T
```

$$\left[\begin{array}{cccccccccccccccccc}x_{1} & y_{1} & z_{1} & x_{2} & y_{2} & z_{2} & x_{3} & y_{3} & z_{3} & x_{4} & y_{4} & z_{4} & x_{5} & y_{5} & z_{5} & x_{6} & y_{6} & z_{6}\end{array}\right]$$

```python
# Symbolic parameters
a,b,c=symbols('a b c', real=True) # Parameters
args=[a,b,c]

F_temp=[Matrix([
            [-X[2+i]-X[3+i]],
            [X[1+i]+a*X[2+i]],
            [b+X[3+i]*(X[1+i]-c)]
        ])
        for i in np.arange(Nsys)*len(Vars)]

F=F_temp[0]

for i in np.arange(1,Nsys):
    F=F.col_join(F_temp[i])

DF=F.jacobian(vecX)

#F, DF
```

## Introducing the coupling

### Network structure

```python
close_ring=True

# Network topology
Gm = mssa.network_AdjMatrix_GG2011_ring(Nsys) # Conection matrix
if close_ring:
    Gm[0,-1]=1
    Gm[-1,0]=1

Ni_in = np.nansum(Gm, axis=0) # Number of "in" conections of oscilator i
I = np.diag(np.ones(Nsys)) #identity matrix

Agm = Gm  - Ni_in*I  # Coupling matrix [GG 2011 is (a-2d)]

# Visual inspection of the coupling
fig = plt.figure()
D = nx.DiGraph(Gm) #with d here, the network figure will show correctly if there is connection (Gm) and COUPLING (d).
                    # Because with d=0 there is no coupling!!!!

nx.draw_circular(D)
```

### Coupled vector field

```python
#A=Matrix([[0,1],[1,0]])
A=Matrix(Gm.astype('int')) # Adjacency matrix (called Gm in the first cell)
I3=Matrix.eye(3)

# Degree (number of connections per node
degree=np.nansum(matrix2numpy(A,dtype='int'), axis=0)
K=diag(*degree)
L=A-K # Laplacian (Note that it is the same as Agm computed in the cell above)

# Symbolic
epsilon=symbols('epsilon') # coupling strength
E=(H*H.T).multiply_elementwise(I3)
LkronE=TensorProduct(L,E)

Fcoupled=F.subs({i:j for i,j in zip(args,args_n)})+epsilon*(LkronE*Matrix(X[1:]))

print 'Arguments used in the vector field F:', {i:j for i,j in zip(args,args_n)}
print 'Comparing the uncoupled and coupled vector fields:'
F,epsilon*(LkronE*Matrix(X[1:])), Fcoupled
```

    Arguments used in the vector field F: {a: 0.45000000000000001, c: 4.0, b: 2.0}
    Comparing the uncoupled and coupled vector fields:

$$\left ( \left[\begin{matrix}- y_{1} - z_{1}\\a y_{1} + x_{1}\\b + z_{1} \left(- c + x_{1}\right)\\- y_{2} - z_{2}\\a y_{2} + x_{2}\\b + z_{2} \left(- c + x_{2}\right)\\- y_{3} - z_{3}\\a y_{3} + x_{3}\\b + z_{3} \left(- c + x_{3}\right)\\- y_{4} - z_{4}\\a y_{4} + x_{4}\\b + z_{4} \left(- c + x_{4}\right)\\- y_{5} - z_{5}\\a y_{5} + x_{5}\\b + z_{5} \left(- c + x_{5}\right)\\- y_{6} - z_{6}\\a y_{6} + x_{6}\\b + z_{6} \left(- c + x_{6}\right)\end{matrix}\right], \quad \left[\begin{matrix}\epsilon \left(- 2 x_{1} + x_{2} + x_{6}\right)\\\epsilon \left(- 2 y_{1} + y_{2} + y_{6}\right)\\\epsilon \left(- 2 z_{1} + z_{2} + z_{6}\right)\\\epsilon \left(x_{1} - 2 x_{2} + x_{3}\right)\\\epsilon \left(y_{1} - 2 y_{2} + y_{3}\right)\\\epsilon \left(z_{1} - 2 z_{2} + z_{3}\right)\\\epsilon \left(x_{2} - 2 x_{3} + x_{4}\right)\\\epsilon \left(y_{2} - 2 y_{3} + y_{4}\right)\\\epsilon \left(z_{2} - 2 z_{3} + z_{4}\right)\\\epsilon \left(x_{3} - 2 x_{4} + x_{5}\right)\\\epsilon \left(y_{3} - 2 y_{4} + y_{5}\right)\\\epsilon \left(z_{3} - 2 z_{4} + z_{5}\right)\\\epsilon \left(x_{4} - 2 x_{5} + x_{6}\right)\\\epsilon \left(y_{4} - 2 y_{5} + y_{6}\right)\\\epsilon \left(z_{4} - 2 z_{5} + z_{6}\right)\\\epsilon \left(x_{1} + x_{5} - 2 x_{6}\right)\\\epsilon \left(y_{1} + y_{5} - 2 y_{6}\right)\\\epsilon \left(z_{1} + z_{5} - 2 z_{6}\right)\end{matrix}\right], \quad \left[\begin{matrix}\epsilon \left(- 2 x_{1} + x_{2} + x_{6}\right) - y_{1} - z_{1}\\\epsilon \left(- 2 y_{1} + y_{2} + y_{6}\right) + x_{1} + 0.45 y_{1}\\\epsilon \left(- 2 z_{1} + z_{2} + z_{6}\right) + z_{1} \left(x_{1} - 4.0\right) + 2.0\\\epsilon \left(x_{1} - 2 x_{2} + x_{3}\right) - y_{2} - z_{2}\\\epsilon \left(y_{1} - 2 y_{2} + y_{3}\right) + x_{2} + 0.45 y_{2}\\\epsilon \left(z_{1} - 2 z_{2} + z_{3}\right) + z_{2} \left(x_{2} - 4.0\right) + 2.0\\\epsilon \left(x_{2} - 2 x_{3} + x_{4}\right) - y_{3} - z_{3}\\\epsilon \left(y_{2} - 2 y_{3} + y_{4}\right) + x_{3} + 0.45 y_{3}\\\epsilon \left(z_{2} - 2 z_{3} + z_{4}\right) + z_{3} \left(x_{3} - 4.0\right) + 2.0\\\epsilon \left(x_{3} - 2 x_{4} + x_{5}\right) - y_{4} - z_{4}\\\epsilon \left(y_{3} - 2 y_{4} + y_{5}\right) + x_{4} + 0.45 y_{4}\\\epsilon \left(z_{3} - 2 z_{4} + z_{5}\right) + z_{4} \left(x_{4} - 4.0\right) + 2.0\\\epsilon \left(x_{4} - 2 x_{5} + x_{6}\right) - y_{5} - z_{5}\\\epsilon \left(y_{4} - 2 y_{5} + y_{6}\right) + x_{5} + 0.45 y_{5}\\\epsilon \left(z_{4} - 2 z_{5} + z_{6}\right) + z_{5} \left(x_{5} - 4.0\right) + 2.0\\\epsilon \left(x_{1} + x_{5} - 2 x_{6}\right) - y_{6} - z_{6}\\\epsilon \left(y_{1} + y_{5} - 2 y_{6}\right) + x_{6} + 0.45 y_{6}\\\epsilon \left(z_{1} + z_{5} - 2 z_{6}\right) + z_{6} \left(x_{6} - 4.0\right) + 2.0\end{matrix}\right]\right )$$

# Lyapunov spectra estimate

## Simulation parameters

```python
Coup_list_keys
```

    [u'0.0',
     u'0.001',
     u'0.002',
     u'0.003',
     u'0.004',
     u'0.005',
     u'0.006',
     u'0.007',
     u'0.008',
     u'0.009',
     u'0.01',
     u'0.011',
     u'0.012',
     u'0.013',
     u'0.014',
     u'0.015',
     u'0.016',
     u'0.017',
     u'0.018',
     u'0.019',
     u'0.02',
     u'0.021',
     u'0.022',
     u'0.023',
     u'0.024',
     u'0.025',
     u'0.026',
     u'0.027',
     u'0.028',
     u'0.029',
     u'0.03',
     u'0.031',
     u'0.032',
     u'0.033',
     u'0.034',
     u'0.035',
     u'0.036',
     u'0.037',
     u'0.038',
     u'0.039',
     u'0.04',
     u'0.041',
     u'0.042',
     u'0.043',
     u'0.044',
     u'0.045',
     u'0.046',
     u'0.047',
     u'0.048',
     u'0.049',
     u'0.05',
     u'0.051',
     u'0.052',
     u'0.053',
     u'0.054',
     u'0.055',
     u'0.056',
     u'0.057',
     u'0.058',
     u'0.059',
     u'0.06',
     u'0.061',
     u'0.062',
     u'0.063',
     u'0.064',
     u'0.065',
     u'0.066',
     u'0.067',
     u'0.068',
     u'0.069',
     u'0.07',
     u'0.071',
     u'0.072',
     u'0.073',
     u'0.074',
     u'0.075',
     u'0.076',
     u'0.077',
     u'0.078',
     u'0.079',
     u'0.08',
     u'0.081',
     u'0.082',
     u'0.083',
     u'0.084',
     u'0.085',
     u'0.086',
     u'0.087',
     u'0.088',
     u'0.089',
     u'0.09',
     u'0.091',
     u'0.092',
     u'0.093',
     u'0.094',
     u'0.095',
     u'0.096',
     u'0.097',
     u'0.098',
     u'0.099',
     u'0.1',
     u'0.101',
     u'0.102',
     u'0.103',
     u'0.104',
     u'0.105',
     u'0.106',
     u'0.107',
     u'0.108',
     u'0.109',
     u'0.11',
     u'0.111',
     u'0.112',
     u'0.113',
     u'0.114',
     u'0.115',
     u'0.116',
     u'0.117',
     u'0.118',
     u'0.119',
     u'0.12',
     u'0.121',
     u'0.122',
     u'0.123',
     u'0.124',
     u'0.125',
     u'0.126',
     u'0.127',
     u'0.128',
     u'0.129',
     u'0.13',
     u'0.131',
     u'0.132',
     u'0.133',
     u'0.134',
     u'0.135',
     u'0.136',
     u'0.137',
     u'0.138',
     u'0.139',
     u'0.14',
     u'0.141',
     u'0.142',
     u'0.143',
     u'0.144',
     u'0.145',
     u'0.146',
     u'0.147',
     u'0.148',
     u'0.149',
     u'0.15',
     u'0.151',
     u'0.152',
     u'0.153',
     u'0.154',
     u'0.155',
     u'0.156',
     u'0.157',
     u'0.158',
     u'0.159',
     u'0.16',
     u'0.161',
     u'0.162',
     u'0.163',
     u'0.164',
     u'0.165',
     u'0.166',
     u'0.167',
     u'0.168',
     u'0.169',
     u'0.17',
     u'0.171',
     u'0.172',
     u'0.173',
     u'0.174',
     u'0.175',
     u'0.176',
     u'0.177',
     u'0.178',
     u'0.179',
     u'0.18',
     u'0.181',
     u'0.182',
     u'0.183',
     u'0.184',
     u'0.185',
     u'0.186',
     u'0.187',
     u'0.188',
     u'0.189',
     u'0.19',
     u'0.191',
     u'0.192',
     u'0.193',
     u'0.194',
     u'0.195',
     u'0.196',
     u'0.197',
     u'0.198',
     u'0.199']

```python
i_coup=Coup_list_keys[60]
i_coup
```

    u'0.06'

```python
# Simulation parameters
epsilon_n=np.array(i_coup, dtype='float')
dt_n,Transient,T,K=0.02, 3000, 0.6, 3000

# dt_n, integration step
# Transient, transient time to drop away
# T, time evolution before callback (Gram-Schmidt)
# K, Iterations per callback

# Initial condition
ydata=DataFile[i_coup][0,:]
Y0_n= ydata.reshape((3,Nsys)).T.ravel() # xData.reshape((3,3)).T.ravel()
#######################################

Fcoupled_epsilon_n=Fcoupled.subs(epsilon,epsilon_n)

# Plotting initial condition phase space
plt.plot(ydata[:6],ydata[6:12])
plt.plot(ydata[:6],ydata[6:12],'ro')
plt.title('Initial condition $\epsilon=$'+i_coup)
plt.xlabel('$x(0)$',fontsize=20)
plt.ylabel('$y(0)$',fontsize=20)
#################################

print '- Trajectory length (s) will be:',K*T
print '- Initial condition (x1y1z1 x2y2z2...:\n', Y0_n
print '- New vector field:\n'
Fcoupled_epsilon_n

```

## Plotting the fiducial trajectory

```python
start = time.time()
# 2 minutes for 6 Rossler systems: dt_n,Transient,T,K=0.02, 0, 0.6, 1000

yt=Simulate(Fcoupled_epsilon_n,Vars,Nsys,
        Y0_n,dt_n,T*K,K,
        Transient)

#How much time I waited?
elapsed = (time.time() - start)
print elapsed, 'seconds\n', elapsed/60, 'minutes'

print 'yt.shape', yt.shape
```

    415.655236006 seconds
    6.92758726676 minutes
    yt.shape (30000, 342)

```python
fig=plt.figure()
nr,nc=2,3
k=1
for i in xrange(6):
    ax=fig.add_subplot(nr,nc,k)
    #print 3*i,3*i+3
    ax.plot(yt[:,3*i],yt[:,3*i+1])
    ax.plot(yt[-1,3*i],yt[-1,3*i+1],'ro')
    k+=1
```

## Computation

```python
start = time.time()
# 2 minutes for 6 Rossler systems: dt_n,Transient,T,K=0.02, 0, 0.6, 1000

lces=ds.LCEs(Fcoupled_epsilon_n,Vars,Nsys,
        Y0_n,dt_n,T,K,
        Transient)

#How much time I waited?
elapsed = (time.time() - start)
print elapsed, 'seconds\n', elapsed/60, 'minutes'
```

    1248.54508305 seconds
    20.8090847174 minutes

```python
# formating output
res_lces=pd.DataFrame()

res_lces['value']=lces.iloc[-1,:]
res_lces['std']=lces.iloc[-10:,:].std() #uncertainty evaluated with the last 10 values.

print 'Trajectory length (s):',K*T

print 'Lyapunov exponents estimated:\n'
res_lces
```

    Trajectory length (s): 1800.0
    Lyapunov exponents estimated:

      value
      std

      L1_sys1
      0.928282
      0.000188

      L2_sys1
      -0.009485
      0.000015

      L3_sys1
      -0.022899
      0.000100

      L1_sys2
      -0.036869
      0.000048

      L2_sys2
      -0.038678
      0.000099

      L3_sys2
      -0.053840
      0.000053

      L1_sys3
      -0.086972
      0.000091

      L2_sys3
      -0.089502
      0.000037

      L3_sys3
      -0.102882
      0.000089

      L1_sys4
      -0.105985
      0.000052

      L2_sys4
      -0.127952
      0.000124

      L3_sys4
      -0.147085
      0.000058

      L1_sys5
      -0.208186
      0.000346

      L2_sys5
      -2.287839
      0.000539

      L3_sys5
      -2.821956
      0.000618

      L1_sys6
      -3.188332
      0.000822

      L2_sys6
      -3.524522
      0.000759

      L3_sys6
      -4.438705
      0.000762

```python
# formating output
res_lces=pd.DataFrame()

res_lces['value']=lces.iloc[-1,:]
res_lces['std']=lces.iloc[-10:,:].std() #uncertainty evaluated with the last 10 values.

print 'Trajectory length (s):',K*T

print 'Lyapunov exponents estimated:\n'
res_lces
```

    Trajectory length (s): 600.0
    Lyapunov exponents estimated:

      value
      std

      L1_sys1
      0.839880
      0.000897

      L2_sys1
      -0.009161
      0.000071

      L3_sys1
      -0.024226
      0.000303

      L1_sys2
      -0.035543
      0.000252

      L2_sys2
      -0.040627
      0.000220

      L3_sys2
      -0.054106
      0.000128

      L1_sys3
      -0.074724
      0.000132

      L2_sys3
      -0.092128
      0.000213

      L3_sys3
      -0.108468
      0.000166

      L1_sys4
      -0.115435
      0.000316

      L2_sys4
      -0.133499
      0.000289

      L3_sys4
      -0.160768
      0.000288

      L1_sys5
      -0.307822
      0.001355

      L2_sys5
      -2.249560
      0.001411

      L3_sys5
      -2.889055
      0.001976

      L1_sys6
      -3.135599
      0.002135

      L2_sys6
      -3.509492
      0.002054

      L3_sys6
      -4.338741
      0.000649

```python
lces.plot(title="Convergence plot $\epsilon=$"+i_coup)
```

```python

```

    Trajectory length (s): 600.0
    Lyapunov exponents estimated:

      value
      std

      L1_sys1
      0.959724
      0.000734

      L2_sys1
      -0.010805
      0.000106

      L3_sys1
      -0.031558
      0.000456

      L1_sys2
      -0.036475
      0.000143

      L2_sys2
      -0.039169
      0.000282

      L3_sys2
      -0.053893
      0.000144

      L1_sys3
      -0.080425
      0.000117

      L2_sys3
      -0.096136
      0.000128

      L3_sys3
      -0.103768
      0.000143

      L1_sys4
      -0.103964
      0.000317

      L2_sys4
      -0.133490
      0.000157

      L3_sys4
      -0.165214
      0.000479

      L1_sys5
      -0.188835
      0.000286

      L2_sys5
      -2.261038
      0.001665

      L3_sys5
      -2.827148
      0.002162

      L1_sys6
      -3.198744
      0.001837

      L2_sys6
      -3.536727
      0.001264

      L3_sys6
      -4.408152
      0.001275

```python
lces.plot(title="Convergence plot $\epsilon=$"+i_coup)
```

# Plotting what it is being simulated

```python
def RKStep(F,Y0,dt):
    """Runge-Kutta integration
    F is the vector field """

    k1F=dt*F(Y0)[0] # ravel because the sympy-numpy function returns [[x1],[y1],[z1]]

    k2F=dt*F(Y0+k1F/2)[0]

    k3F=dt*F(Y0+k2F/2)[0]

    k4F=dt*F(Y0+k3F)[0]

    return Y0+(k1F+2*k2F+2*k3F+k4F)/6.

def gram_schmidt_columns(u):
    """ Returns an orthogonal (and not normalized) base, using
        the Gram-Schmidt method.

        Vectors are considered as the columns of the matrix u.
        """

    w=np.copy(u)
    v=np.empty_like(w)
    N_vectors=u.shape[1] # number of vectors=number of columns of the matrix u.

    # w1
    w[:,0]=u[:,0]
    v[:,0]=w[:,0]/norm(w[:,0])

    # wk, for k>0
    for k in range(1,N_vectors):
        for i in range(k):
            w[:,k]-=np.dot(u[:,k],v[:,i])*v[:,i]
        v[:,k]=w[:,k]/norm(w[:,k])

    return w

def Simulate(F,Vars,Nsys,
        Y0_n,dt_n,T,K,
        Transient=0):

    # State vector (symbolic): None x1 y1 z1 x2 y2 z2 x3 y3 z3
    sys_i=[str(i) for i in np.arange(1,Nsys+1)]

    labels_temp=[j+i+' ' for i in sys_i for j in Vars]
    labels_temp='None '+''.join(labels_temp) # none para que X[1] corresponda ao simbolo x1

    # state variables
    X=symbols(labels_temp, real=True)
    vecX=Matrix(X[1:])

    # Jacobian
    DF=F.jacobian(vecX)

    # Matrix Phi
    Phi=Matrix(np.array(['phi'+str(i)+str(j)
        for i in range(1,Nsys*3+1)
        for j in range(1,Nsys*3+1)]).reshape(Nsys*3,Nsys*3))

    # dPhi/dt
    DPhi=Phi*DF.T

    # Symbolic vector [x ..., y..., z..., phi11, phi12, ...
    vecY0_Phi0=flatten(vecX)+flatten(Phi)

    # Creating the functions (joining F and DPhi)
    f_dphi=lambdify([vecY0_Phi0],(DPhi.T.vec().row_insert(0,F)).T,'numpy') # flattened function [F, DF.Phi]

    # Simulation ##########################

    # Numeric vectors and matrices
    Phi0_n=np.eye(Nsys*3)
    vecY0_Phi0_n=np.hstack((Y0_n,Phi0_n.ravel()))
    yt=vecY0_Phi0_n.copy()

    # Transient
    if Transient>0:
        N_n=int(Transient/dt_n)

        for i in xrange(N_n-1):
            yt=RKStep(f_dphi,yt,dt_n)

    # Permanent regime
    N=int(T/dt_n)
    yt_permanent=np.empty((N,yt.shape[0]))
    yt_permanent[0,:]=yt.copy()
    for i in xrange(N-1):
            yt_permanent[i+1,:]=RKStep(f_dphi,yt_permanent[i,:],dt_n)

    return yt_permanent
```

# Loading modules

```python
#Básicos
%matplotlib inline

#Símbolo / se torna divisão verdadeira (e não apenas a inteira)
from __future__ import division

import numpy as np
import pandas as pd
import scipy.io as sio
import matplotlib.pyplot as plt
import time

# specific
from sympy import *
from sympy.physics.quantum import TensorProduct

import h5py
import networkx as nx

#meus modulos
import daVinci03 as leo
import mssa as mssa
import DySys as ds

init_printing()
```

```python

```


```python
import pymongo
import sympy
from sympy.parsing.sympy_parser import parse_expr
import os
```

```python
# Maybe you have to change mongo url to something else if you're non the DAQ network? See cax.config.
client = pymongo.MongoClient('mongodb://eb:%s@xenon1t-daq.lngs.infn.it:27017/run' % os.environ['MONGO_PASSWORD'])
```

# Check format from purity collection

```python
purity_coll = client['run']['purity']
purity_coll.count()
```

    12

```python
d = purity_coll.find_one()
```

```python
for d in purity_coll.find(sort=(('calculation_time', -1), )):
    print(str(d['calculation_time']), parse_expr(d['function']))
```

    2017-02-15 10:36:32.554000 571.536805205327/(1 + 2.85936983046934e+138*exp(-2.15875349579075e-7*t))
    2017-02-09 11:34:52.728000 726.439274517567/(1 + 2.0260694424652e+84*exp(-1.3126159999747e-7*t))
    2017-02-08 12:40:54.433000 726.439274517567/(1 + 2.0260694424652e+84*exp(-1.3126159999747e-7*t))
    2017-01-12 16:57:05.376000 664.009433008032/(1 + 9.20429790014259e+57*exp(-9.05409401619292e-8*t))
    2016-12-07 15:24:34.858000 3293.34246828997/(1 + 3.61800535059737e+15*exp(-2.29229212119497e-8*t))
    2016-10-19 10:10:51.244000 451.092742358951/(1 + 2.19032421518171e+1011*exp(-1.57823407887515e-6*t))
    2016-09-19 05:55:53.102000 317.29661042819/(1 + 1.05633549046867e+880*exp(-1.37598762559539e-6*t))
    2016-09-19 05:55:53.102000 317.29661042819/(1 + 1.05633549046867e+880*exp(-1.37598762559539e-6*t))
    2016-07-26 10:23:33.859000 323.085761530256/(1 + 2.60535248526874e+1028*exp(-1.61211864700664e-6*t))
    2016-07-26 10:23:33.859000 323.085761530256/(1 + 2.60535248526874e+1028*exp(-1.61211864700664e-6*t))
    2016-07-14 12:11:37.671000 299.124801429751/(1 + 1.22494241740575e+1063*exp(-1.66807062881465e-6*t))
    2016-06-15 08:26:08.173000 9.93103054982358e-672*exp(1.05654787355925e-6*t)

# Try to add drift velocity correction function

```python
import numpy as np
```

```python
data = np.array([
    4,  0.9948,       # Julien
    5,  1.1224,       # Julien
    6,  1.2137,       # Julien
    7,  1.2833,       # Julien
    8,  1.3323,       # Julien
    9,  1.371,        # Jelle
    12, 1.4401,       # Jelle & Julien (same value, well Julien had 1.4402 and I had 1.440...)
    13, 1.456,
    15, 1.482
])

cathode_kv, vdrift_kmpers = data.reshape(-1, 2).T
```

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

```python
vs = np.linspace(0, 20, 100)

deg = len(cathode_kv) - 4
coeffs = np.polyfit(cathode_kv, vdrift_kmpers, deg=deg)
plt.plot(vs, np.polyval(coeffs, vs), label='%dth degree polynomial fit' % deg)
plt.scatter(cathode_kv, vdrift_kmpers, marker='x', c='k', label='Datapoints')

plt.ylim(0.5, 1.7)
plt.legend(loc='lower right')
plt.xlabel("Cathode voltage (kV)")
plt.ylabel("Drift velocity (km/sec)")
```

```python
plt.plot(cathode_kv, 100 * (np.polyval(coeffs, cathode_kv) - vdrift_kmpers)/vdrift_kmpers,
         linestyle='', marker='x')
plt.ylabel('Relative fit error (%)')
plt.xlabel("Cathode voltage kV")
```

Systematic error on drift velocity estimates is around 0.2%, see : https://xecluster.lngs.infn.it/dokuwiki/doku.php?id=xenon:xenon1t:aalbers:drift_and_diffusion#results

so if the fitter error is much less than this, we're ok.

## Make sympy function

```python
v = sympy.Symbol('v')
sp_poly = sympy.Poly.from_list(coeffs.tolist(), gens=v)
```

```python
# Sanity check:
sp_poly.subs(dict(v=7.5))
```

    1.30959670337438

```python
# Try converting to and from serialization format, repeat sanity check
serialized_fit = sympy.srepr(sp_poly)
parse_expr(serialized_fit).subs(dict(v=7.5))
```

    1.30959670337438

## Insert in runs db

```python
drift_coll = client['run']['drift_velocity']
```

```python
from datetime import datetime
```

```python
# Uncomment this, adjust versions and comments to do actual insertion
# drift_coll.insert_one(dict(
#     version='1.0.0',
#     calculation_time=datetime.now(),
#     function=serialized_fit,
#     comment="5th degree polynomial fit to Julien&Jelle's Drift velocity results. Jelle, 1 April 2017."
# ))
```

    <pymongo.results.InsertOneResult at 0x7fd3fc0d7ca8>

examples/Drift velocity to runs db.ipynb


```python
import sys
import numpy as np
import sys
print(sys.version)

from phasor.utilities.ipynb.displays import *
from phasor.utilities.ipynb.filters import *
from phasor.utilities.ipynb.hdf import *
#from YALL.utilities.tabulate import tabulate

from declarative import (
  OverridableObject, mproperty
)

import sympy
from phasor.utilities.ipynb.displays import *
from phasor.utilities.ipynb.ipy_sympy import *
import scipy.linalg
```

    3.5.3 (default, Apr 24 2017, 13:32:13)
    [GCC 6.3.1 20161221 (Red Hat 6.3.1-1)]
    Sympy version:  1.0

```python
alpha = sympy.Matrix(sympy.var('alpha_1, alpha_2'))
a, b, c, d = sympy.var('a, b, c, d', commutative = False)
Sa, Sb, Sc, Sd = sympy.var('Sa, Sb, Sc, Sd', commutative = False)
T = sympy.Matrix([[a,b], [c, d]])
T_s = sympy.Matrix([[1-Sa,-Sb], [-Sc, 1-Sd]])
T

y, yc = sympy.var('y, y_c')
Q = sympy.Matrix([[1,0 * y * c.conjugate()], [-y.conjugate() * c, 1]])
Q_s = sympy.Matrix([[1,y * -Sc.conjugate()], [y.conjugate() * Sc, 1]])

subs_list = [(a, 1 - Sa), (b, -Sb), (c, -Sc), (d, 1-Sd)]
Q_s = Q.subs(subs_list)
T_s = T.subs(subs_list)

display(Q)

T_prime = Q * T
T_prime_s = Q_s * T_s
alpha_prime = Q * alpha

display(alpha_prime)
display(T_prime)
```

```python

```

```python
display(alpha_prime.subs(subs_list))
display(Q_s * T_s)
S_prime = sympy.eye(2) - T_prime_s
S_prime.simplify()
display(S_prime)
```

```python

```

```python

```


## Model Predictive Control (MPC) of a CSTR (Continuous Stirred Tank Reactor)

The MPC formulation can be written as following
$$\begin{equation}
    \begin{aligned}
    \min_{u_k} \quad &  \sum_{k=0}^{T-1} J \left( x_k, u_k \right)\\
    \text{subject to} \quad & \dot{x}(t) = f(t, x(t), {u(t)})\\
    &x(0) = x_0\\
    & t \in \mathcal{T}
    \end{aligned}
\end{equation}$$
where $J$ is a cost function and $T \in \mathcal{T}$ is the _receding horizon_ over which predicted future trajectories are optimized

```python
%load_ext autoreload
%autoreload 2

import sys; sys.path.append(2*'../') # go n dirs back
import matplotlib.pyplot as plt

import torch
from torchcontrol.systems import CSTR
from torchcontrol.cost import IntegralCost
from torchcontrol.controllers import *
from torchcontrol.mpc import TorchMPC
from torchcontrol.plotting import *

```

    The autoreload extension is already loaded. To reload it, use:
      %reload_ext autoreload

The quadcopter model is a suitably modified version of the explicit dynamics update of the library [do-mpc](https://www.do-mpc.com/en/latest/example_gallery/CSTR.html) for batched training in PyTorch. The following accelerations update describes the dynamic model
$$\begin{align}
\dot{C}_{\text{A}} &= F \cdot (C_{\text{A},0} - C_{\text{A}}) - k_1 \cdot C_{\text{A}} - k_3 \cdot C_{\text{A}}^2, \\
\dot{C}_{\text{B}} &= F \cdot C_{\text{B}} + k_1 \cdot C_{\text{A}} - k_2 \cdot C_{\text{B}}, \\
\dot{T}_{\text{R}} &= \frac{k_1 \cdot C_{\text{A}} \cdot H_{\text{R},ab} + k_2 \cdot C_{\text{B}} \cdot  H_{\text{R},bc} + k_3 \cdot C_{\text{A}}^2 \cdot H_{\text{R},ad}} {-\rho \cdot c_p}\\
&+ F \cdot (T_{\text{in}} - T_{\text{R}}) + \frac{K_w \cdot A_{\text{R}} \cdot(T_{\text{K}}-T_{\text{R}})}{\rho \cdot c_p \cdot V_{\text{R}}}, \\
\dot{T}_{\text{K}} &= \frac{\dot{Q} + k_w \cdot A_{\text{R}} \cdot T_{\text{dif}}}{m_k \cdot C_{p,k}},
\end{align}$$

where

$$\begin{align}
k_1 &= \beta \cdot k_{0,\text{ab}} \cdot \exp\left(\frac{-E_{\text{A},\text{ab}}}{T_{\text{R}}+273.15}\right), \\
k_1 &= k_{0,\text{bc}} \cdot \exp \left( \frac{-E_{\text{A},\text{bc}}}{T_{\text{R}}+273.15} \right), \\
k_3 &= k_{0,\text{ad}} \cdot \exp \left( \frac{-\alpha \cdot E_{\text{A},\text{ad}}}{T_{\text{R}}+273.15} \right).
\end{align}$$

```python
?? CSTR
```

    Init signature:  CSTR(*args, alpha=1, beta=1, **kwargs)
    Source:
    class CSTR(ControlledSystemTemplate):
        '''
        Controlled Continuous Stirred Tank Reactor
        Reference: https://www.do-mpc.com/en/latest/example_gallery/CSTR.html
        '''
        def __init__(self, *args, alpha=1, beta=1, **kwargs):
            super().__init__(*args, **kwargs)

            # Parameters
            self.α = alpha # empirical parameter, may vary
            self.β = beta # empirical parameter, may vary
            self.K0_ab = 1.287e12 # K0 [h^-1]
            self.K0_bc = 1.287e12 # K0 [h^-1]
            self.K0_ad = 9.043e9 # K0 [l/mol.h]
            self.R_gas = 8.3144621e-3 # Universal gas constant
            self.E_A_ab = 9758.3*1.00 #* R_gas# [kj/mol]
            self.E_A_bc = 9758.3*1.00 #* R_gas# [kj/mol]
            self.E_A_ad = 8560.0*1.0 #* R_gas# [kj/mol]
            self.H_R_ab = 4.2 # [kj/mol A]
            self.H_R_bc = -11.0 # [kj/mol B] Exothermic
            self.H_R_ad = -41.85 # [kj/mol A] Exothermic
            self.Rou = 0.9342 # Density [kg/l]
            self.Cp = 3.01 # Specific Heat capacity [kj/Kg.K]
            self.Cp_k = 2.0 # Coolant heat capacity [kj/kg.k]
            self.A_R = 0.215 # Area of reactor wall [m^2]
            self.V_R = 10.01 #0.01 # Volume of reactor [l]
            self.m_k = 5.0 # Coolant mass[kg]
            self.T_in = 130.0 # Temp of inflow [Celsius]
            self.K_w = 4032.0 # [kj/h.m^2.K]
            self.C_A0 = (5.7+4.5)/2.0*1.0 # Concentration of A in input Upper bound 5.7 lower bound 4.5 [mol/l]

        def dynamics(self, t, x):
            self.nfe += 1 # increment number of function evaluations
            u = self._evaluate_controller(t, x)

            # States
            C_a = x[..., 0:1]
            C_b = x[..., 1:2]
            T_R = x[..., 2:3]
            T_K = x[..., 3:4]

            # Controller
            F, dQ = u[..., :1], u[..., 1:]

            # Auxiliary variables
            K_1 = self.β * self.K0_ab * torch.exp((-self.E_A_ab)/((T_R+273.15)))
            K_2 =  self.K0_bc * torch.exp((-self.E_A_bc)/((T_R+273.15)))
            K_3 = self.K0_ad * torch.exp((-self.α*self.E_A_ad)/((T_R+273.15)))
            T_dif = T_R - T_K

            # Differential equations
            dC_a = F*(self.C_A0 - C_a) -K_1*C_a - K_3*(C_a**2)
            dC_b = -F*C_b + K_1*C_a - K_2*C_b
            dT_R = ((K_1*C_a*self.H_R_ab + K_2*C_b*self.H_R_bc + K_3*(C_a**2)*self.H_R_ad)/(-self.Rou*self.Cp)) \
                        + F*(self.T_in-T_R) +(((self.K_w*self.A_R)*(-T_dif))/(self.Rou*self.Cp*self.V_R))
            dT_K = (dQ + self.K_w*self.A_R*(T_dif))/(self.m_k*self.Cp_k)
            self.cur_f = torch.cat([dC_a, dC_b, dT_R, dT_K], -1)
            return self.cur_f
    File:           ~/Desktop/torchcontrol/torchcontrol/systems/cstr.py
    Type:           type
    Subclasses:

```python
# Change device according to your configuration
device = torch.device('cuda:0') if torch.cuda.is_available() else torch.device('cpu') # feel free to change :)
device = torch.device('cpu') # override
```

## Cost function
We want the CSTR to reach a concentration of $C_b^\star = 0.6$, while also minimizing the energy consumption and satisfying state boundaries

Note: finding good values for the cost function is hard - many times, it requires patience and lots of trials and error

```python
# Declaring the cost function
x_star = torch.Tensor([0, 0.6, 0, 0]).to(device) # the zeros are dummy values
u_star = torch.Tensor([5, 0]) # Least expensive controller values

P = torch.Tensor([0, 1, 0, 0]).to(device)
Q = torch.Tensor([0, .1, 0, 0]).to(device)
R = torch.Tensor([1e-3, 1e-4]).to(device)
cost_function = IntegralCost(x_star=x_star, u_star=u_star, P=P, Q=Q, R=R)
```

## Initial Conditions
We start with all the variables at 0

```python
# Initial Conditions
ε = .01 # 1% of uncertainty given initial conditions
C_a_0 = 0.8 # This is the initial concentration inside the tank [mol/l]
C_b_0 = 0.5 # This is the controlled variable [mol/l]
T_R_0 = 134.14 #[C]
T_K_0 = 130.0 #[C]
init = torch.Tensor([C_a_0, C_b_0, T_R_0, T_K_0]).to(device)
print(init)
```

    tensor([  0.8000,   0.5000, 134.1400, 130.0000])

## Scaling

```python
##### Scaling since the parameters have very different values
scaling_T_R = 1/100
scaling_T_K = 1/100
scaling_Q_dot = 1/2000
scaling_F = 1/100

# Scale the inputs appropriately for the controller
in_scal = torch.ones(4).to(device)
in_scal[2] = scaling_T_R
in_scal[3] = scaling_T_K
print('Input scaling:\n', in_scal)

out_scal = torch.tensor([[5.,  100.],
                        [-8500, 0.]]).to(device)
print('Output scaling:\n', out_scal)
```

    Input scaling:
     tensor([1.0000, 1.0000, 0.0100, 0.0100])
    Output scaling:
     tensor([[ 5.0000e+00,  1.0000e+02],
            [-8.5000e+03,  0.0000e+00]])

## Controller
We use the neural box-constrained controller with input and output scaling due to the differences in magnitude

```python
# Instantiate drone
cstr = CSTR(None, solver='euler') # dummy controller
controller = BoxConstrainedController(4, 2, input_scaling=in_scal, output_scaling=out_scal, constrained=True)
cstr.u = controller # controller for the drone
```

## Constraints
Since we have constraints on the states, we introduce penalties for out-of-bounds states

```python

# State constraints
lower_bounds = [0.1, 0.1, 50., 50.]
upper_bounds = [2., 2., None, 140.]
penalties =  0.1*torch.ones(4); penalties[3] = 100
print('Lower bounds:\n', lower_bounds, '\nUpper bounds:\n', upper_bounds)
```

    Lower bounds:
     [0.1, 0.1, 50.0, 50.0]
    Upper bounds:
     [2.0, 2.0, None, 140.0]

## MPC initialization

```python
# MPC simulation variables
Δt = 0.005
t0, tf = 0, 0.2
t_span = torch.linspace(t0, tf, int(tf/Δt) + 1).to(device) # define the t span

# MPC simulation variables
steps_nom = 10 # Nominal steps to do between each MPC step
max_iters = 50
eps_accept = 3e-3 # so we 'fix' the iterations to be always maximum
lookahead_steps = 10
lr = 1e-3
weight_decay = 1e-4 # weight decay for adam
x0 = init # initial condition

# Optimizer
opt = torch.optim.Adam(controller.parameters(), lr=lr, weight_decay=weight_decay)

# Instantiate MPC
mpc = TorchMPC(cstr, cost_function, t_span, opt, eps_accept=eps_accept, max_g_iters=max_iters,
            lookahead_steps=lookahead_steps, lower_bounds=lower_bounds,
            upper_bounds=upper_bounds, penalties=penalties).to(device)
```

## MPC simulation
For simulating the MPC we need a system with an accurate solver, e.g. `dopri5` for carrying forward the solutions

```python
cstr_nominal = CSTR(RandConstController((1,1),1,1), solver='dopri5', atol=1e-5, rtol=1e-5)
loss = mpc.forward_simulation(cstr_nominal, x0, t_span, reset=False, reinit_zeros=False)
```

    Steps:   0%|          | 0/40 [00:00<?, ?it/s]

    Starting simulation...

    Steps: 100%|██████████| 40/40 [00:18<00:00,  2.18it/s, cost=0.434, timestamp=0.2, converged=0]

    The simulation has ended!

```python
# Get variables
traj = mpc.trajectory_nominal
controls = mpc.control_inputs
print('Nominal cost:', loss)
```

    Nominal cost: tensor(0.3903)

## Plotting

```python
plot_cstr_trajectories_controls(traj, controls, tf=tf)
```