# Week 3

I hope you're getting the hang of things. Today we're going on with the prinicples of data visualization!

## Overview

Once again, the lecture has three parts:

* First you will watch a video on visualization and solve a couple of exercises.
* After that, we'll be reading about *scientific data visualization*, and the huge number of things you can do with just one variable. Naturally, we'll be answering questions about that book.
* And finally reproducing some of the plots from that book.

## Part 1: Fundamentals of data visualization

Last week we had a small introduction of data visualization. Today, we are going to be a bit more specific on data analysis and visualization. Digging a bit more into the theory with the next video.

*It's important to highlight that these lectures are quite important. We don't have a formal book on data visualization. So the only source of knowledge about the **principles**, **theories**, and **ideas**, that are the foundation for good data viz, comes from the videos*. So watch them 🤓

[](https://www.youtube.com/watch?v=yiU56codNlI)

> *Excercise 1.1:* Questions for the lecture
> * As mentioned earlier, visualization is not the only way to test for correlation. We can (for example) calculate the Pearson correlation. Explain in your own words how the Pearson correlation works and write down it's mathematical formulation. Can you think of an example where it fails (and visualization works)?

Wikipedia: Pearson's correlation coefficient is the covariance of the two variables divided by the product of their standard deviations.
\begin{equation}
  r =
  \frac{ \sum_{i=1}^{n}(x_i-\bar{x})(y_i-\bar{y}) }{%
        \sqrt{\sum_{i=1}^{n}(x_i-\bar{x})^2}\sqrt{\sum_{i=1}^{n}(y_i-\bar{y})^2}}
\end{equation}

it fails (and visualization works)? -> no clue

> * What is the difference between a bar-chart and a histogram?

e.g. https://keydifferences.com/difference-between-histogram-and-bar-graph.html

histogram -> verteilung
bar graph -> displays different categories

> * I mention in the video that it's important to choose the right bin-size in histograms. But how do you do that? Do a Google search to find a criterion you like and explain it.

https://www.statisticshowto.com/choose-bin-sizes-statistics/#:~:text=Choose%20between%205%20and%2020,more%20useful%20with%2020%20bins.&text=For%20example%2C%20if%20you%20have,instead%20of%206%20or%207.

(As a first guess, you can start with Scott’s rule for the bin width w = 3.5σ/√3 n, where σ is the standard deviation for the entire data set and n is the number of points. This rule assumes that the data follows a Gaussian distribution; otherwise, it is likely to give a bin width that is too wide. See the end of this chapter for more information on the standard deviation.)

Ok, now that we've talked a bit about correlation and distributions, we are going to compute/visualize them while also testing some hypotheses along the way. Until now, we have analysed data at an explorative level, but we can use statistics to verify whether relationships between variables are significant. We'll do this in the following exercise.

> *Exercise 1.2:* Hypothesis testing. We will look into correlations between number of steps and BMI, and differences between two data samples (Females vs Males). Follow the steps below for success:
>
> * First, we need to get some data. Download and read the data from the Female group [here](https://raw.githubusercontent.com/suneman/socialdata2022/main/files/data9b_f.csv) and the one from the Male group [here](https://raw.githubusercontent.com/suneman/socialdata2022/main/files/data9b_m.csv).
> * Next, we are going to verify the following hypotheses:
>    1. *H1: there is a statistically significant difference in the average number of steps taken by men and women*. Is there a statistically significant difference between the two groups? What is the difference between their mean number of steps? Plot two histograms to visualize the step-count distributions, and use the criterion you chose in Ex.1.1 to define the right bin-size.
     **Hint** you can use the function `ttest_ind()` from the `scipy.stats` package to test the hypothesis and consider a significance level $\alpha=0.05$.
>    2. *H2: there is a negative correlation between the number of steps and the BMI for women*. We will use Pearson's correlation here. Is there a negative correlation? How big is it?
>    3. *H3: there is a positive correlation between the number of steps and the BMI for men*. Is there a positive correlation? Compare it with the one you found for women.
> * We have now gathered the results. Can you find a possible explanation for what you observed? You don't need to come up with a grand theory about mobility and gender, just try to find something (e.g. theory, news, papers, further analysis etc.) to support your conclusions and write down a couple of sentences.

> *Exercise 1.3:* scatter plots. We're now going to fully visualize the data from the previous exercise.
>
> * Create a scatter plot with both data samples. Use `color='#f6756d'` for one sample and `color='#10bdc3'` for the other sample. The data is in front of you, what do you observe? Take a minute to think about these exercises: what do you think the point is?
    * After answering the questions above, have a look at this [paper](https://genomebiology.biomedcentral.com/track/pdf/10.1186/s13059-020-02133-w.pdf) (in particular, read the *Not all who wander are lost* section).
> * The scatter plot made me think of another point we often overlook: *color-vision impairments*. When visualizing and explaining data, we need to think about our audience:
>    * We used the same colors as in the paper, try to save the figure and use any color-blindness simulator you find on the web ([this](https://www.color-blindness.com/coblis-color-blindness-simulator/) was the first that came out in my browser). Are the colors used problematic? Explain why, and try different types of colors. If you are interested in knowing more you can read this [paper](https://www.tandfonline.com/doi/pdf/10.1179/000870403235002042?casa_token=MAYp78HctgQAAAAA:AZKSHJWuNmoMXD5Dtqln1Sc-xjNwCe6UVDMVEpP95UjTH3O1H-NKRkfYljw2VLSm_zKlN74Da6g).
>    * But, are colors the only option we have? Find an alternative to colors, explain it, and change your scatter plot accordingly.

```python
# 1.2.0 import data

import requests
import pandas as pd
import io

data = pd.DataFrame()

for i in ['f','m']:

    URL = f'https://raw.githubusercontent.com/suneman/socialdata2022/main/files/data9b_{i}.csv'

    urlData = requests.get(URL).content
    rawData = pd.read_csv(io.StringIO(urlData.decode('utf-8')))
    rawData.columns = ['steps', 'bmi']

    if i == 'f':
        rawData['data_set'] = 'female'
    else:
        rawData['data_set'] = 'male'

    rawData = rawData[['data_set', 'steps', 'bmi']]

    if i == 0:
        data = rawData
    else:
        data = pd.concat([data, rawData])

    del i, rawData, URL, urlData
```

```python
# 1.2.1
from scipy.stats import ttest_ind

male = data[data['data_set'] == 'male']
female = data[data['data_set'] == 'female']

male_stats = male.describe()
female_stats = female.describe()

diff_mean_steps = round(male_stats.at['mean', 'steps'] - female_stats.at['mean', 'steps'], 0)

ttest_steps = ttest_ind(male['steps'], female['steps'])
if ttest_steps[1] < 0.05:
    print("There is no significant difference in the average number of steps taken by men and women.")
else:
    print("There is a significant difference in the average number of steps taken by men and women.")

import matplotlib.pyplot as plt

fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(20, 10), sharex=True, sharey=True)

min_steps_male = male['steps'].min()
max_steps_male = male['steps'].max()

axs[0].hist(male['steps'], bins = 5, range = (0, 15000))
axs[0].set_title('Distribution of Steps by Men')
axs[0].set_xlabel('Steps')
axs[0].set_ylabel('Frequency')

min_steps_female = female['steps'].min()
max_steps_female = female['steps'].max()

axs[1].hist(female['steps'], bins = 5, range = (0, 15000))
axs[1].set_title('Distribution of Steps by Women')
axs[1].set_xlabel('Steps')
axs[1].set_ylabel('Frequency')

fig.tight_layout(rect=[0, 0.03, 1, 0.95])
plt.show()
```

```python
# 1.2.2
import scipy

Pearson_correlation_steps_BMI_women = scipy.stats.pearsonr(female['steps'],female['bmi'])[0]
Pearson_correlation_steps_BMI_men = scipy.stats.pearsonr(male['steps'],male['bmi'])[0]

# source:
# https://www.researchgate.net/publication/6747371_The_inverse_relationship_between_number_of_steps_per_day_and_obesity_in_a_population-based_sample_-_The_AusDiab_study

# 1.3
# Create a scatter plot with both data samples. Use `color='#f6756d'` for one <font color=#f6756d>sample</font> and `color='#10bdc3'`

import matplotlib.pyplot as plt

plt.scatter(x=female['steps'], y=female['bmi'], c='#377eb8')
plt.scatter(x=male['steps'], y=male['bmi'], c='#ff7f00')

# good colors to use, so color blind people can still see the results
# CB_color_cycle = ['#377eb8', '#ff7f00', '#4daf4a', '#f781bf', '#a65628', '#984ea3', '#999999', '#e41a1c', '#dede00']
```

## Part 2: Reading about the theory of visualization

Since we can go deeper with the visualization this year, we are going to read the first couple of chapters from [*Data Analysis with Open Source Tools*](http://shop.oreilly.com/product/9780596802363.do) (DAOST). It's pretty old, but I think it's a fantastic resource and one that is pretty much as relevant now as it was back then. The author is a physicist (like Sune) so he likes the way he thinks. And the books takes the reader all the way from visualization, through modeling to computational mining. Anywho - it's a great book and well worth reading in its entirety.

As part of this class we'll be reading the first chapters. Today, we'll read chaper 2 (the first 28 pages) which supports and deepens many of the points we made during the video above.

To find the text, you will need to go to **DTU Learn**. It's under "Course content" $\rightarrow$ "Content" $\rightarrow$ "Lecture 3 reading".

> *Excercise 2*: Questions for DAOST
> * Explain in your own words the point of the jitter plot.

points are plotted and then randomly moved a little, so they do not overlap -> easier to see if point occurs multiple times

> * Explain in your own words the point of figure 2-3. (I'm going to skip saying "in your own words" going forward, but I hope you get the point; I expect all answers to be in your own words).

anchoring points of the bins are important -> same bins, but different statement

> * The author of DAOST (Philipp Janert) likes KDEs (and think they're better than histograms). And we don't. Sune didn't give a detailed explanation in the video, but now that works to our advantage. We'll ask you to think about this and thereby create an excellent exercise: When can KDEs be misleading?

should be combined with a Cumulative Distribution Function to get a better feeling for the significance of certain peaks

> * Sune discussed some strengths of the CDF - there are also weaknesses. Janert writes "CDFs have less intuitive appeal than histograms of KDEs". What does he mean by that?

our eye works really well to spot significant peaks of an histograms or a KDE as it is good with distances, but it struggles more with the changing slope of CDFs

> * What is a *Quantile plot*? What is it good for.

its a technique to graphically find out if a data set follows a distribution ->  plotting their quantiles against each other -> line with 45° slope -> same distribution

> * How is a *Probablity plot* defined? What is it useful for? Have you ever seen one before?

- probability plot for assessing how closely two data sets agree, which plots the two cumulative distribution functions against each other
- P-P plots are vastly used to evaluate the skewness of a distribution.
- nope

> * One of the reasons we like DAOST is that Janert is so suspicious of mean, median, and related summary statistics. Explain why one has to be careful when using those - and why visualization of the full data is always better.

- misleading information -> e.g. shopping cart where 5 pieces cost 1 dollar and one 20 -> mean gives different picture
- skewed distribution -> mean and median give wrong impression

> * Sune loves box plots (but not enough to own one of [these](https://images.app.goo.gl/rpozyRX3xu5oFobt8) 😂). When are box plots most useful?

quick overview of the data + finding outliers

> * The book doesn't mention [violin plots](https://en.wikipedia.org/wiki/Violin_plot). Are those better or worse than box plots? Why?

Violin plots are similar to box plots, except that they also show the probability density of the data at different values, usually smoothed by a kernel density estimator. Typically a violin plot will include all the data that is in a box plot: a marker for the median of the data; a box or marker indicating the interquartile range; and possibly all sample points, if the number of samples is not too high.
-> better more information

## Part 3: *Finally*! Let's create some visualizations

> *Excercise 3.1*: Connecting the dots and recreating plots from DAOST but using our own favorite dataset.
> * Let's make a jitter-plot (that is, code up something like **Figure 2-1** from DAOST from scratch), but based on *SF Police data*. My hunch from inspecting the file is that the police-folks might be a little bit lazy in noting down the **exact** time down to the second. So choose a crime-type and a suitable time interval (somewhere between a month and 6 months depending on the crime-type) and create a jitter plot of the arrest times during a single hour (like 13-14, for example). So let time run on the $x$-axis and create vertical jitter.
> * Last time, we did lots of bar-plots. Today, we'll play around with histograms (creating two crime-data based versions of the plot-type shown in DAOST **Figure 2-2**). I think the GPS data could be fun to see this way.
>   * This time, pick two crime-types with different geographical patterns **and** a suitable time-interval for each (you want between 1000 and 10000 points in your histogram)
>   * Then take the latitude part of the GPS coordinates for each crime and bin the latitudes so that you have around 50 bins across the city of SF. You can use your favorite method for binning. I like `numpy.histogram`. This function gives you the counts and then you do your own plotting.
> * Next up is using the plot-type shown in **Figure 2-4** from DAOST, but with the data you used to create Figure 2.1. To create the kernel density plot, you can either use `gaussian_kde` from `scipy.stats` ([for an example, check out this stackoverflow post](https://stackoverflow.com/questions/4150171/how-to-create-a-density-plot-in-matplotlib)) or you can use [`seaborn.kdeplot`](https://seaborn.pydata.org/generated/seaborn.kdeplot.html).
> * Now grab 25 random timepoints from the dataset (of 1000-10000 original data) you've just plotted and create a version of Figure 2-4 based on the 25 data points. Does this shed light on why I think KDEs can be misleading?

```python
# 3.1 import and preprocessing
import pandas as pd
import numpy as np
from pathlib import Path

# read police incidence reports, historical 2003 to may 2018
police_incidence_reports = pd.read_csv(Path.cwd().parent/"files"/"Police_Department_Incident_Reports__Historical_2003_to_May_2018.csv")
police_incidence_reports = pd.DataFrame(police_incidence_reports)
police_incidence_reports_2003_till_2017 = police_incidence_reports[~(pd.DatetimeIndex(police_incidence_reports['Date']).year > 2017)]
police_incidence_reports_2003_till_2017['Datetime'] = pd.to_datetime(police_incidence_reports_2003_till_2017['Date']+ ' ' +\
    police_incidence_reports_2003_till_2017['Time'])
police_incidence_reports_2003_till_2017.sort_values(['Category', 'Datetime']).reset_index()

larceny_theft = police_incidence_reports_2003_till_2017.loc[police_incidence_reports_2003_till_2017['Category'].isin(['LARCENY/THEFT'])]\
    .sort_values(['Datetime']).reset_index()

```

```python
# 3.1 jitter plot
import seaborn as sns
import matplotlib.pyplot as plt
larceny_theft_january_1st_2003_from_1_till_6_pm = larceny_theft.loc[(larceny_theft['Datetime'] >= '01-01-2003 13:00:00') &\
    (larceny_theft['Datetime'] <= '01-01-2003 17:59:59')]

plt.figure(figsize=(10,10))
ax = sns.stripplot(x=larceny_theft_january_1st_2003_from_1_till_6_pm ['Datetime'])
fig.tight_layout(rect=[0, 0.03, 1, 0.95])
plt.show()
```

```python
# 3.1 histograms

vehicle_theft = police_incidence_reports_2003_till_2017.loc[police_incidence_reports_2003_till_2017['Category']\
    .isin(['VEHICLE THEFT'])].sort_values(['Datetime'])

vehicle_theft_2003_jan_till_jun = vehicle_theft.loc[(vehicle_theft['Datetime'] >= '01-01-2003 00:00:00') & \
    (vehicle_theft['Datetime'] <= '30-06-2003 23:59:59')]

larceny_theft_2003_jan_till_jun = larceny_theft.loc[(larceny_theft['Datetime'] >= '01-01-2003 00:00:00') & \
    (larceny_theft['Datetime'] <= '30-06-2003 23:59:59')]

# plot
fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(20, 10), sharex=True, sharey=True)

min_latitude_larceny = larceny_theft_2003_jan_till_jun['Y'].min()
max_latitude_larceny = larceny_theft_2003_jan_till_jun['Y'].max()

axs[0].hist(larceny_theft_2003_jan_till_jun['Y'], bins = 50, range = (37.7, 37.82))
axs[0].set_title('Distribution of Larceny from January till June 2003')
axs[0].set_xlabel('Larceny')
axs[0].set_ylabel('Frequency')

min_latitude_vehicle_theft = vehicle_theft_2003_jan_till_jun['Y'].min()
max_latitude_vehicle_theft = vehicle_theft_2003_jan_till_jun['Y'].max()

axs[1].hist(vehicle_theft_2003_jan_till_jun['Y'], bins = 50, range = (37.7, 37.82))
axs[1].set_title('Distribution of Vehicle Theft from January till June 2003')
axs[1].set_xlabel('Vehicle Theft')
axs[1].set_ylabel('Frequency')

fig.tight_layout(rect=[0, 0.03, 1, 0.95])
plt.show()

```

```python
# 3.1 kernel density plot with data of jitter plot
plt.figure(figsize=(10,10))
ax = sns.kdeplot(x=larceny_theft_january_1st_2003_from_1_till_6_pm ['Datetime'])
fig.tight_layout(rect=[0, 0.03, 1, 0.95])
plt.show()
```

```python
# 3.1 kernel density plot with latitude data
plt.figure(figsize=(10,10))
ax = sns.kdeplot(x=larceny_theft_2003_jan_till_jun['Y'].sample(n = 25))
fig.tight_layout(rect=[0, 0.03, 1, 0.95])
plt.show()

# at histogram values were e.g. max 37.81, but here you cannot see the cutoff
```

>
> Let's take a break. Get some coffee or water. Stretch your legs. Talk to your friends for a bit. Breathe. Get relaxed so you're ready for the second part of the exercise.

> *Exercise 3.2*. Ok. Now for more plots 😊
> * Now we'll work on creating two versions of the plot in **Figure 2-11**, but using the GPS data you used for your version of Figure 2-2. Comment on the result. It is not easy to create this plot from scracth.
     **Hint:** Take a look at the `scipy.stats.probplot` function.
> * OK, we're almost done, but we need some box plots. Here, I'd like you to use the box plots to visualize fluctuations of how many crimes happen per day. We'll use data from the 15 focus crimes defined last week.
>   * For the full time-span of the data, calulate the **number of crimes per day** within each category for the entire duration of the data.
>   * Create a box-and whiskers plot showing the mean, median, quantiles, etc for all 15 crime-types side-by-side. There are many ways to do this. I like to use [matplotlibs's built in functionality](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.boxplot.html), but you can also achieve good results with [seaborn](https://seaborn.pydata.org/generated/seaborn.boxplot.html) or [pandas](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.boxplot.html).
>   * What does this plot reveal that you can't see in the plots from last time?
> * Also I want to show you guys another interesting use of box plots. To get started, let's calculate another average for each focus-crime, namely what time of day the crime happens. So this time, the distribution we want to plot is the average time-of-day that a crime takes place. There are many ways to do this, but let me describe one way to do it.
    * For datapoint, the only thing you care about is the time-of-day, so discard everything else.
    * You also have to deal with the fact that time is annoyingly not divided into nice units that go to 100 like many other numbers. I can think of two ways to deal with this.
      * For each time-of-day, simply encode it as seconds since midnight.
      * Or keep each whole hour, and convert the minute/second count to a percentage of an hour. So 10:15 $\rightarrow$ 10.25, 8:40 $\rightarrow$ 8.67, etc.
    * Now you can create box-plots to create an overview of *when various crimes occur*. Note that these plot have quite a different interpretation than ones we created in the previous exercise. Cool, right?

```python
# Exercise 3.2 probability plot
from scipy import stats

stats.probplot(x=vehicle_theft_2003_jan_till_jun[vehicle_theft_2003_jan_till_jun['Y'] < 90.0]['Y'], dist=stats.beta, sparams=(40,80,10), plot=plt)

# it feels impossible -> also histograms show that they do not follow any common distribution like a normal distribution

```

```python
# preprocessing data focuscrimes

focuscrimes = pd.DataFrame(['WEAPON LAWS', 'PROSTITUTION', 'DRIVING UNDER THE INFLUENCE', 'ROBBERY', 'BURGLARY', 'ASSAULT', 'DRUNKENNESS', 'DRUG/NARCOTIC', 'TRESPASS', 'LARCENY/THEFT', 'VANDALISM', 'VEHICLE THEFT', 'STOLEN PROPERTY', 'DISORDERLY CONDUCT'])
focuscrimes.columns = ['Category']
focuscrimes = pd.merge(focuscrimes, police_incidence_reports_2003_till_2017, on = 'Category')
focuscrimes.sort_values(by=['Category', 'Datetime'])

```

```python
focuscrimes_by_unique_day = focuscrimes.groupby(['Category', focuscrimes['Datetime'].dt.floor('d')]).agg({'PdId':'count'})\
    .sort_values(by=['Category', 'Datetime']).rename(columns={'PdId':'Counts'}).reset_index()

# focuscrimes by category and weekday
focuscrimes_by_category_weekday = focuscrimes.groupby(['Category', 'DayOfWeek']).agg({'PdId':'count'})\
    .sort_values(by=['Category']).rename(columns={'PdId':'Counts'}).reset_index()

fig, axs = plt.subplots(nrows=7, ncols=2, figsize=(20, 20))

m = 0 # falls n = 1 -> m + 1 und nach plot n wieder auf 0 setzen
n = 0 # 0 oder 1
for i in focuscrimes['Category'].unique():
    if n == 1:
        y = focuscrimes_by_category_weekday.loc[focuscrimes_by_category_weekday['Category'].isin([i])]
        axs[m,n].boxplot(x = y['Counts'])
        axs[m,n].set_title(i)
        axs[m,n].set_ylabel('Crime Count')
        m = m + 1
        n = 0
    else:
        y = focuscrimes_by_category_weekday.loc[focuscrimes_by_category_weekday['Category'].isin([i])]
        axs[m,n].boxplot(x = y['Counts'])
        axs[m,n].set_title(i)
        axs[m,n].set_ylabel('Crime Count')
        n = 1
fig.tight_layout(rect=[0, 0.03, 1, 0.95])
fig.suptitle('Boxplots of the Number of Crimes per Day by Category')
plt.show()
```

```python
# boxplot

focuscrimes['hour_decimal'] = focuscrimes['Datetime'].dt.hour+focuscrimes['Datetime'].dt.minute/60.0
focuscrimes.sort_values(by=['hour_decimal'])

fig, axs = plt.subplots(nrows=7, ncols=2, figsize=(20, 20))

m = 0 # falls n = 1 -> m + 1 und nach plot n wieder auf 0 setzen
n = 0 # 0 oder 1
for i in focuscrimes['Category'].unique():
    if n == 1:
        y = focuscrimes.loc[focuscrimes['Category'].isin([i])]
        axs[m,n].boxplot(x = y['hour_decimal'])
        axs[m,n].set_title(i)
        axs[m,n].set_ylabel('Time of the Day'')
        m = m + 1
        n = 0
    else:
        y = focuscrimes.loc[focuscrimes['Category'].isin([i])]
        axs[m,n].boxplot(x = y['hour_decimal'])
        axs[m,n].set_title(i)
        axs[m,n].set_ylabel('Time of the Day')
        n = 1
fig.tight_layout(rect=[0, 0.03, 1, 0.95])
fig.suptitle('Boxplots of the Number of Crimes per Hour of the Day by Category')
plt.show()
```


```python
# 그래프, 수학 기능 추가
# Add graph and math features
import pylab as py
import numpy as np
import numpy.linalg as nl
# 기호 연산 기능 추가
# Add symbolic operation capability
import sympy as sy

```

```python
sy.init_printing()

```

# 상미분방정식을 위한 오일러법
Euler Method for Ordinary Differntial Equation

[](https://www.youtube.com/watch?v=q87L9R9v274)

## 여러 $(t, x)$ 지점에서의 기울기
Slopes at $(t, x)$ points

다시 한번 주어진 미분 방정식을 생각해 보자.
Let's think about the first order differential equation again.

$$
\left\{
    \begin{align}
        a_0 \frac{d}{dt}x(t)+a_1 x(t)&=0 \\
        x(0)&=x_0 \\
    \end{align}
\right.
$$

미분항을 남기고 나머지를 등호의 오른쪽으로 옮겨 보자.
Let's move terms except the differential to the right side of the equal sign.

$$
a_0 \frac{d}{dt}x(t)=-a_1 x(t)
$$

양변을 $a_0$로 나누어 보자.
Let's divide both sides with $a_0$.

$$
\frac{d}{dt}x(t)=-\frac{a_1}{a_0} x(t)
$$

이 식의 의미를 한번 생각해 보자.
Let's think about the meaning of this equation.

위 미분방정식을 만족시키는 어떤 함수 $x(t)$의 $t=t_i$, $x=x_j$ 점에서의 $t$에 대한 기울기는 다음과 같을 것이라는 의미이다.

This equation indicates that a function $x(t)$ satisfying the differential equation above would have a slope as follows at a point of $t=t_i$ and $x=x_j$.

$$
\left.\frac{d}{dt}x\right|_{\left(t, x\right)=\left(t_i,x_j\right)}=-\frac{a_1}{a_0} x_j
$$

이런식으로 $t$의 변화에 따른 $x$의 기울기 $\frac{d}{dt}x(t)$ 를 모든 $(t, x)$ 점에서 구할 수 있다.

In this way, we can find all the $\frac{d}{dx}x(t)$, slopes of $x$ with respect to the change of $t$ at all $(t, x)$ points.

## 방향장 기울기 시각화
Visualizing the slopes in the [Direction Field](https://en.wikipedia.org/wiki/Direction_field)

다음 예를 생각해 보자.

Let's think about an example as follows.

$$
\left\{
    \begin{align}
        2 \frac{d}{dt}x(t)+ x(t)&=0 \\
        x(0)&=x_0 \\
    \end{align}
\right.
$$

기울기를 계산하는 파이썬 함수를 생각해 보자.
Let's think about a python function calculating the slope.

```python
a_0, a_1 = 2.0, 1.0

def dx_dt(t, x):
    return - a_1 * x / a_0

```

예를 들어 $0 \le t \le 10$, $-6 \le x \le 6$ 인 영역에서 기울기를 그려 보자.

Let's plot slopes within the region of $0 \le t \le 10$ and $-6 \le x \le 6$.

```python
t_slope = py.linspace(0, 10)
x_slope = py.linspace(-6, 6)

```

```python
import ode_plot

ode_plot.ode_slope_1state(dx_dt, x_slope, t_slope)
py.savefig('slopes_t_x.svg')

```

 위와 같이 모든 점에서의 기울기의 그림을 **방향장** 또는 **기울기장** 이라고 한다.

 As above, a plot of the slopes of all points is called a [**Direction Field**](https://en.wikipedia.org/wiki/Direction_field) or **Slope Field**

엄밀해를 겹쳐 그려 보자
Let's overlap the curve of the exact solution.

$$x(t)=x_0 e^{-\frac{a_1}{a_0} t}$$

```python
x_0 = 4.5

def exact(t):
    return x_0 * py.exp((-a_1 / a_0) * t)

x_exact_array = exact(t_slope)

ode_plot.plot_slope_fileds_and_exact_solution(dx_dt, t_slope, x_slope,)

py.savefig('slopes_t_x_exact.svg')

```

$t=0$에서의 $x$의 초기값에 따라 엄밀해가 달라질 수 있음을 기억하자.
Let's remember that the exact solution may vary depending on the initial value of $x$ at $t=0$.

이것을 이용해서 미분방정식의 해 곡선을 구해볼 수 있을까?
Using this, can we find solution curves of a differential equation?

## 오일러법
Euler Method

$\left(t,x\right) = \left(0, x_0\right)$ 에서의 $x(t)$의 기울기를 생각해 보자.

Let's think about the slope of $x(t)$ at $\left(t,x\right) = \left(0, x_0\right)$.

$$
\left.\frac{d}{dt}x\right|_{\left(t, x\right)=\left(0,x_0\right)}=-\frac{a_1}{a_0} x_0=s_0
$$

그렇다면, $(0, x_0)$점을 지나고 기울기가 $s_0=-\frac{a_1}{a_0} x_0$ 인 직선을 생각할 수 있다.

Then, we can think about a line passing through $(0, x_0)$ with the slope of $s_0=-\frac{a_1}{a_0} x_0$.

$$
x=-\frac{a_1}{a_0} x_0 \left(t -  0 \right) + x_0=s_0\left(t -  0 \right) + x_0
$$

이 직선을 따라 $t_1$ 까지 전진해 보자.

Following this line, let's move forward to $t_1$.

$$
t_1=t_0 + \Delta t = 0 + 0.5
$$

```python
# Initial point
t_0 = 0
x_0 = 4.5

# time step
delta_t = 0.5

```

```python
# Slope at the initial point (t_0, x_0) point
s_0 = dx_dt(t_0, x_0)

# Straight line to next time step
t_0_array = py.linspace(0, delta_t)
x_0_array = s_0 * (t_0_array - t_0) + x_0

# (t_1, x_1) point
t_1, x_1 = t_0_array[-1], x_0_array[-1]

```

```python
ode_plot.indicate_initial_point(t_0, x_0)
ode_plot.plot_one_step(t_0_array, x_0_array, 0)
ode_plot.format_incremental_plot()

```

$(t_1, x_1)$ 에서의 기울기 $s_1$ 과 그러한 기울기를 가지고 $\left(t_1, x_1\right)$ 을 지나는 직선은 다음과 같다.

The slope $s_1$ at $\left(t_1, x_1\right)$ and another line with such slope and passing $\left(t_1, x_1\right)$ would be as follows.

$$
\begin{align}
\left.\frac{d}{dt}x\right|_{\left(t, x\right) = \left(t_1,x_1\right)} &=-\frac{a_1}{a_0} x_1=s_1 \\
x & = s_1\left(t - t_1 \right) + x_1
\end{align}
$$

```python
# Slope at (t_1, x_1) point
s_1 = dx_dt(t_1, x_1)

# Straight line to next time step
t_1_array = py.linspace(t_1, t_1 + delta_t)
x_1_array = s_1 * (t_1_array - t_1) + x_1

# (t_2, x_2) point
t_2, x_2 = t_1_array[-1], x_1_array[-1]

```

```python
# Indicate the line from (t_0, x_0) with slope s_0
ode_plot.indicate_initial_point(t_0, x_0)
ode_plot.plot_one_step(t_0_array, x_0_array, 0)

# Indicate the line from (t_1, x_1) with slope s_1
ode_plot.plot_one_step(t_1_array, x_1_array, 1)

ode_plot.format_incremental_plot()

```

엄밀해와 비교해 보자.
Let's compare with the exact solution.

```python
# https://stackoverflow.com/a/9236970
t_array = py.concatenate([t_0_array.tolist(), t_1_array.tolist()], axis=None)
exact = ode_plot.ExactPlotterFirstOrderODE(t_array)

```

```python
# Indicate the line segments
ode_plot.indicate_initial_point(t_0, x_0)
ode_plot.plot_one_step(t_0_array, x_0_array, 0)
ode_plot.plot_one_step(t_1_array, x_1_array, 1)

# plot exact solution
exact.plot()

ode_plot.format_incremental_plot()

```

$t$값이 커 짐에 따라, 엄밀해와 $x_1$의 오차 보다 엄밀해와 $x_2$ 사이의 오차가 커지는 것을 볼 수 있다.

As $t$ increases, the error between $x_2$ and the exact solution is larger than the error between $x_1$ and the exact solution.

## 함수로 변환
Convert to a function

사용상 편리를 위해 함수를 만들어 보자.
To make it easier to use, let's make it a function.

```python
def euler(f, t_array, x_0):
    time_list = [t_array[0]]
    result_list = [x_0]

    x_i = x_0

    for k, t_i in enumerate(t_array[:-1]):
        # time step
        delta_t = t_array[k+1] - t_array[k]

        # slope
        s_i = f(t_i, x_i)

        # x[i + 1]
        x_i_plus_1 = x_i + s_i * delta_t

        time_list.append(t_array[k+1])
        result_list.append(x_i_plus_1)

        x_i = x_i_plus_1

    return time_list, result_list

```

다시 그려 보자.
Let's plot again.

```python
# Time step interval
delta_t = 0.5

# Time array
t_sec_array = np.arange(0, 1 + delta_t*0.5, delta_t)

# Initial state
x_0 = 4.5

# *** new function ***
t_euler_out, x_euler_out = euler(dx_dt, t_sec_array, x_0)
# *** new function ***

py.plot(t_euler_out, x_euler_out, 'o-')

for k in range(len(t_euler_out)):
    ode_plot.text_xy_k(t_euler_out[k], x_euler_out[k], k)

# Indicate the exact solution
exact.plot()

ode_plot.format_incremental_plot()

```

## $\Delta t$ 간격의 영향
Influence of $\Delta t$ interval

오차를 줄일 수 있는 좋은 방법이 없을까?
Is there a good way to reduce the error?

$\Delta t=0.5$ 를 $\Delta t=0.1$로 한번 줄여 보자.
Let's make $\Delta t=0.5$ to $\Delta t=0.1$.

```python
# Time step interval & Time array
delta_t = 0.1
t_sec_array = np.arange(0, 1 + delta_t*0.5, delta_t)

# Initial state
x_0 = 4.5

# *** new function ***
t_euler_out_01, x_euler_out_01 = euler(dx_dt, t_sec_array, x_0)

```

```python
py.plot(t_euler_out, x_euler_out, 'o-', label='$\Delta t=0.5$')
py.plot(t_euler_out_01, x_euler_out_01, '.-', label='$\Delta t=0.1$')

# Indicate the exact solution
exact.plot()

ode_plot.format_incremental_plot()

```

$\Delta t$ 간격을 줄이면 오차에 어떤 영향을 미쳤는가?
How did reducing $\Delta t$ interval influence the error?

### 근사해와 방향장
Approximate solutions and direction fields

해를 방향장과 겹쳐 그려 보자.

Let's overlap the solutions and the direction field.

$t$와 $x$의 범위

Ranges of $t$ and $x$

```python
t_slopes = py.linspace(0, 6)
x_slopes = py.linspace(0, 6)

```

초기값
Initial value

$x(t_0)$

```python
x_0 = 4.5

```

$
\Delta t = 0.5
$ (sec)

```python
delta_t_05 = 0.5
t_05_sec = np.arange(t_slopes[0], t_slopes[-1] + delta_t_05*0.5, delta_t_05)
t_out_05, x_out_05 = euler(dx_dt, t_05_sec, x_0)

```

$
\Delta t = 0.1
$ (sec)

```python
delta_t_01 = 0.1
t_01_sec = np.arange(t_slopes[0], t_slopes[-1] + delta_t_01*0.5, delta_t_01)
t_out_01, x_out_01 = euler(dx_dt, t_01_sec, x_0)

```

이제 그려 보자.
Now let's plot.

```python
# Slopes at each (t, x) points
ode_plot.ode_slope_1state(dx_dt, x_slopes, t_slopes)

py.plot(t_out_05, x_out_05, 'o-', label='$\Delta t=0.5$')
py.plot(t_out_01, x_out_01, 'o-', label='$\Delta t=0.1$')

# plot exact solution
exact = ode_plot.ExactPlotterFirstOrderODE(t_slopes)
exact.plot()

# Aspect ratio
py.axis('equal')

# xy limits
py.xlim(left=t_slopes[0], right=t_slopes[-1])
py.ylim(bottom=x_slopes[0], top=x_slopes[-1])

py.legend(loc=1, fontsize='xx-large');

```

$i$번째 점과 $i+1$번째 점 사이에서 $\frac{d}{dt}x(t)$는 계속 변화하고 있으나, 오일러법은 해당 구간에서의 기울기가 $\frac{d}{dt}x(t_i)$인 것으로 가정한다.

Between the $i$th and $i+1$st points, $\frac{d}{dt}x(t)$ continuously changes.  However, the Euler Method assumes that the slope is fixed at $\frac{d}{dt}x(t_1)$ within the interval.

그렇다면, $t_i \le t \le t_{i+1}$ 사이에서 대표적인 $\frac{d}{dt}x$ 값은 어떤 값이 좋을 것인가?

If so, within $t_i \le t \le t_{i+1}$, which value of $\frac{d}{dt}x$ would be representative?

## Scipy

과학기술 계산 라이브러리인 사이파이 `scipy` 의 `scipy.integrate` 를 통해서 다수의 상미분 방정식 솔버 solver 를 제공한다.

As a scientific computation library, `scipy` has ODE solvers in `scipy.integrate`.

```python
import scipy.integrate as si

```

```python
sol = si.solve_ivp(dx_dt, (t_out_01[0], t_out_01[-1]), [x_0], t_eval=t_out_01)

```

엄밀해, 오일러법 결과와 비교해보자.

Let's compare with the exact solution and the result of the Euler's method.

```python
py.plot(sol.t, sol.y[0, :], 'o', label='solve_ivp')
py.plot(t_out_01, x_out_01, '.-', label='$\Delta t=0.1$')

# plot exact solution
exact = ode_plot.ExactPlotterFirstOrderODE(t_slopes)
exact.plot()
py.grid(True)
py.xlabel('t(sec)')
py.ylabel('y(t)')
py.legend(loc=0);

```

판다스 `pandas` 를 이용해 표 형태로도 살펴볼 수 있을 것이다.

The `pandas`would enable observing in a table form.

```python
import pandas as pd

df = pd.DataFrame(
    data={
        'euler':x_out_01,
        'solve_ivp':sol.y[0, :],
        'exact':exact.exact(py.array(t_out_01))
    },
    index=pd.Series(t_out_01, name='t(sec)'),
    columns=['exact', 'euler', 'solve_ivp']
)

```

열 연산으로 엄밀해에 대한 오차를 구해보자.

Let's calculate the error against the exact solution.

```python
df['euler_error'] = df.euler - df.exact
df['solve_ivp_error'] = df.solve_ivp - df.exact

```

표 형태
Table form

```python
pd.set_option('display.max_rows', 10)
df

```

각종 통계
Statistics

```python
df.describe()

```

이 경우, 오일러법의 오차에 대한 의견은?

In this case, what do you think about the error of the Euler method?

```python
import numpy.linalg as nl

nl.norm(df.euler_error), nl.norm(df.solve_ivp_error),

```

## 연습 문제
Exercise

도전과제 1: 다음 미분방정식의 해곡선을 전진 오일러법으로 구하시오.

Try This 1: Find a solution curve of the following differential equation using the forward Euler method.

$$
\begin{cases}
\begin{align}
a_0\frac{d}{dt}x(t) + a_1 x(t) &= 0 \\
x(0)&=x_0
\end{align}
\end{cases}
$$

```python

```

도전과제 2: 다음 미분방정식의 해곡선을 전진 오일러법으로 구하시오.

Try This 2: Find a solution curve of the following differential equation using the forward Euler method.

$$
\begin{cases}
\begin{align}
a_0 \frac{d}{dt}x(t) + a_1 x(t) &= b_0 t \\
x(0)&=0
\end{align}
\end{cases}
$$

```python

```

도전과제 3: 다음 미분방정식의 해곡선을 전진 오일러법으로 구하시오.

Try This 3: Find a solution curve of the following differential equation using the forward Euler method.

$$
\begin{cases}
\begin{align}
a_0 \frac{d}{dt}x(t) + a_1 x(t) &= b_0 sin(t) \\
x(0)&=x_0
\end{align}
\end{cases}
$$

```python

```

도전과제 4: 다음 미분방정식의 해곡선을 전진 오일러법으로 구하시오.

Try This 4: Find a solution curve of the following differential equation using the forward Euler method. [[link](https://en.wikipedia.org/wiki/Bernoulli_differential_equation)]

$1 \le x \le 10$

$$
\begin{cases}
\begin{align}
\frac{d}{dx}y(x) - \frac{2}{x}y(x) &= -x^2y^2 \\
y(1)&=1
\end{align}
\end{cases}
$$

```python

```

## 예상 자료형
Type hints

References : [[1](https://docs.python.org/3/library/typing.html)], [[2](https://stackoverflow.com/a/54610845)]

타입 힌트는 변수의 예상 자료형을 예시하여 (개발자 또는) 개발용 소프트웨어 등을 돕기 위한 것이다.

Type hints are to present expected data types to help (developers or) development software.

```python
def dx_dt_type_hints(t:float, x:float) -> float:
    return - a_1 / a_0 * x

```

예를 들어 위 함수는 실수형 매개변수 `t`, 실수형 매개변수 `x` 를 받아들여 계산한 결과를 실수형으로 반환할 것이라는 의미이다.

For example, the function above would accept two `float` arguments of `t` and `x` to return the calculated result in  `float`.

매개변수 `t` 가 실수형 또는 정수형으로 예상된다면, 다음과 같이  `typing` 모듈의 `Union`으로 표시할 수 있다.

If the argument `t`is expected to be `float` or `int`, we can indicate as follows using `Union` of the `typing` module.

```python
import typing

def dx_dt_type_hints(t:typing.Union[float, int], x:float) -> float:
    return - a_1 / a_0 * x

```

`euler( )`와 같이 함수 또는 list를 받아들이는 경우는 다음과 같이 표시할 수 있을 것이다.

If arguments are a function or a list as `euler( )`, following cell would be possible.

```python
import typing

import numpy as np

Scalar = typing.Union[float, int]
Time = Scalar
TimeList = typing.Union[typing.List[Time], typing.Tuple[Time]]
State = typing.Union[Scalar, typing.List[Scalar], typing.Tuple[Scalar], np.ndarray]
StateList = typing.Union[typing.List[State], typing.Tuple[State]]
SlopeFunction = typing.Callable[[Time, State], State]

def euler(f:SlopeFunction, t_array:TimeList, x_0, State) -> typing.Tuple[TimeList, StateList]:

    time_list = [t_array[0]]
    result_list = [x_0]

    x_i = x_0

    for k, t_i in enumerate(t_array[:-1]):
        # time step
        delta_t = t_array[k+1] - t_array[k]

        # slope
        s_i = f(t_i, x_i)

        # x[i + 1]
        x_i_plus_1 = x_i + s_i * delta_t

        time_list.append(t_array[k+1])
        result_list.append(x_i_plus_1)

        x_i = x_i_plus_1

    return time_list, result_list

```

## 중첩함수와 클로져
Nested function and Closure

내장 함수는 다른 함수 안에 정의된 함수이다.
A nested function is a function defined another function.

```python
def function_with_a_nested_function(t_list, x_list, a=0.5, b=2):

    def nested_function(t_i, x_i):

        return a * t_i + b * x_i

    y_list = []

    for t_j, x_j in zip(t_list, x_list):
        y_list.append(nested_function(t_j, x_j))

    return y_list

```

```python
t_list = [0, 1, 2]
x_list = [0.1, 0.2, 0.3]
function_with_a_nested_function(t_list, x_list)

```

**클로져**는 이러한 *내장 함수가 함수의 반환값*인 경우라고 할 수 있다.

A **closure** can be a nested function returned from a function.

```python
def function_returning_a_closure(a=0.5, b=2):

    def this_will_be_a_closure(t_i, x_i):

        return a * t_i + b * x_i

    return this_will_be_a_closure # no ( )

```

```python
this_is_a_closure = function_returning_a_closure()

t_list_closure = [0, 1, 2]
x_list_closure = [0.1, 0.2, 0.3]
y_list_closure = []

for t_j, x_j in zip(t_list_closure, x_list_closure):
    y_list_closure.append(this_is_a_closure(t_j, x_j))

y_list_closure

```

예를 들어 미분방정식의 기울기를 계산하는 함수를 closure 로 생성하는 것도 가능할 것이다.

For example, we may think about a function returning a closure calculating the slope for the differential equation.

```python
def get_slope_function_LODE1(a0, a1):
    minus_a1_over_a0 = -a1 / a0
    def slope_LODE1(t, x):
        return minus_a1_over_a0 * x
    return slope_LODE1

```

```python
dx_dt_closure = get_slope_function_LODE1(a_0, a_1)

```

```python
# Slopes at each (t, x) points
ode_plot.ode_slope_1state(dx_dt, x_slopes, t_slopes)

py.plot(t_out_05, x_out_05, 'o-', label='$\Delta t=0.5$')
py.plot(t_out_01, x_out_01, 'o-', label='$\Delta t=0.1$')

# plot exact solution
exact = ode_plot.ExactPlotterFirstOrderODE(t_slopes)
exact.plot()

# Aspect ratio
py.axis('equal')

# xy limits
py.xlim(left=t_slopes[0], right=t_slopes[-1])
py.ylim(bottom=x_slopes[0], top=x_slopes[-1])

py.legend(loc=1, fontsize='xx-large');

```

아래 셀은 함수 `dx_dt()` 와 `dx_dt_closure()`의 결과를 비교한다.

Following cell compares results from functions `dx_dt()` and `dx_dt_closure()`

```python
import numpy as np
import numpy.testing as nt

slope_dx_dt = np.array([dx_dt(t_k, x_k) for t_k, x_k in zip(t_out_01, x_out_01)])
slope_dx_dt_closure = np.array([dx_dt_closure(t_k, x_k) for t_k, x_k in zip(t_out_01, x_out_01)])

nt.assert_array_almost_equal(slope_dx_dt, slope_dx_dt_closure, err_msg="Closure results seem different.")

```

## Final Bell
마지막 종

```python
# stackoverfow.com/a/24634221
import os
os.system("printf '\a'");

```

```python

```


# Exact Ising Model simulation

## How to simulate time evolution at zero time
---

### Contributors:
Alba Cervera-Lierta from *QUANTIC group* at Barcelona Supercomputing Center (BSC).

### *Abstract*

*In this notebook, we will simulate exactly the magnetization and the time evolution of a $n=4$ spin chain with Ising-type interaction by constructing a quantum circuit that diagonalizes its dynamics. This circuit can be implemented in a real device using only five 2-qubit gates which, in terms of the available gate set, correspond to 12 CNOT gates and 22 one-qubit gates.*

*Note: this notebook was runned on 03/24/2018 at 3:45 p.m. (CET)*

## Introduction

One of the challenges in the field of quantum information is solving strongly-correlated many-body systems. The identification of the entanglement structure as well as the time and temperature evolution of some observables have direct applications in many fields such as condensed matter physics or quantum chemistry.

Direct approaches of these systems are usually intractable due to the strong correlations between particles or the exponential growing of the complexity as more particles are taking into account. On the other hand, a huge variety of analytical and numerical techniques have been proposed in the last century; for instance, Tensor Networks [[1](https://arxiv.org/abs/1306.2164)] is one of the most used in the last years due to the variety of applications. But, although could be very accurate, are still approximations to the quantum states of the systems studied.

However, sometimes we are lucky and there exist a technique that allows us to solve some models _exactly_; these are the so-called _exactly-solvable models_ and most of them have a long tradition in statistical mechanics. Some examples are the 1-_d_ and 2-_d_ Ising Model, _XY_ model and the Bethe ansatz to solve Heisenberg-type models.

As there exist an analytic method to diagonalize and, therefore, solve these models, what if we can construct a quantum circuit that implement these methods and _disentangle_ certain kind of hamiltonians? This idea was proposed by Vestraete, Cirac and Latorre [[2](https://arxiv.org/abs/0804.1888)]: they designed a circuit that disentangles the _XY_ hamiltonian. As a consequence, one have access not only to the ground state of the system, but also to all its spectrum: this is fundamental to understand and study all properties of the model, including time and temperature evolution.

With the emergence of quantum computers such as IBMQ, the real implementation of these circuits become possible. In this notebook, the most simple circuit will be programmed and implemented: a $n=4$ transverse Ising spin chain. We will compute the magnetization of the ground state and the time evolution (at zero time!) of some spin state.

## The transverse Ising model

One of the most simple models to describe an interacting spin chain is the transverse Ising model, which is a particular case of the $XY$ model. It describes an spin-1/2 chain with nearest-neightbour interaction with an external magnetic field. The hamiltonian of this model is usually written as

$$H=\sum_{i=1}^{n}\sigma^{x}_{i}\sigma^{x}_{i+1} + \lambda\sum_{i=1}^{n}\sigma^{z}_{i},$$

where $\lambda$ is the strength of the external magnetic field, and $\sigma$ are the Pauli matrices

$$\sigma^{x}=\left(\begin{array}{cc}0&1\\1&0\end{array}\right),\qquad \sigma^{y}=\left(\begin{array}{cc}0&-i\\i&0\end{array}\right),\qquad \sigma^z{}=\left(\begin{array}{cc}1&0\\0&-1\end{array}\right).$$

This model shows a quantum phase transition at some critical point $\lambda=\lambda_{c}$: for $\lambda<\lambda_{c}$ the spins are in a disordered paramagnetic phase (spins desire to be antiparalel $\uparrow\downarrow\uparrow\downarrow\cdots$) and for $\lambda>\lambda_{c}$ the spins are in an ordered ferromagnetic phase (spins desire to be in the same direction, $\uparrow\uparrow\uparrow\uparrow\cdots$ or $\downarrow\downarrow\downarrow\downarrow\cdots$). For infinite chains, $\lambda_{c}=1$. For finite chains with periodic boundary conditions, i.e. spin $n$ interacts with spin 1, the critical point is lesser than 1, but approaches to the value of infinite chain as $n$ increases. The same happens with the entropy: the bipartite entropy peaks around critical point, which moves to $\lambda=1$ as the size of the chain increases and scales logarithmically following the conformal scaling law $S\sim \frac{c}{6}\log n$ with central charge $c=1/2$ [[3](https://arxiv.org/abs/0906.1499)].

### Magnetization

One natural measure to observe the quantum phase transition is the magnetization, which corresponds to the expected value of the spin operator

$$\langle S_{i}\rangle = \frac{\hbar}{2}\langle\sigma_{i}\rangle $$

As in our model the magnetic field is applied in the $z$ direction, we will measure the transverse magnetization, i.e. $M\propto\langle\sigma_{z}\rangle$ (moreover, we will take natural units and set $\hbar=1$).

The eigenvectors of $\sigma_{z}$ operator are

$$\begin{eqnarray}\sigma_{z}|\uparrow\rangle &=& +1|\uparrow\rangle, \qquad \sigma_{z}|\downarrow\rangle &=& -1|\downarrow\rangle \end{eqnarray},$$

and we will identify them with the computational basis:

$$\begin{eqnarray}|0\rangle&\equiv&|\uparrow\rangle=\left(\begin{array}{c}1\\0\end{array}\right), \qquad
|1\rangle&\equiv&|\downarrow\rangle=\left(\begin{array}{c}0\\1\end{array}\right) \end{eqnarray}.$$

What should we expect from the magnetization as $\lambda$ increases? Let's set $\lambda=0$: all spins interact with their first neighbors according the above hamiltonian, i.e. in the $x$ direction, which is orthonormal to the $z$ direction. Following the above convention for the computational basis, the eigenstates of $\sigma_{x}$ are

$$\begin{array}{l l}
\sigma_{x}|\rightarrow\rangle=+1|\rightarrow\rangle, & |\rightarrow\rangle=\frac{1}{\sqrt{2}}\left(|\uparrow\rangle+|\downarrow\rangle\right)=\frac{1}{\sqrt{2}}\left(|0\rangle+|1\rangle\right), \\
\sigma_{x}|\leftarrow\rangle=-1|\leftarrow\rangle, & |\leftarrow\rangle=\frac{1}{\sqrt{2}}\left(-|\uparrow\rangle+|\downarrow\rangle\right)=\frac{1}{\sqrt{2}}\left(-|0\rangle+|1\rangle\right).
\end{array}$$

Then, for no external magnetic field, the spins point in the $x$ direction ($\rightarrow\rightarrow\rightarrow\cdots$ or $\leftarrow\leftarrow\leftarrow\cdots$) and as the magnetization is the expected value of $\sigma_{z}$ (the mean value of +1 spin states and -1 spin states), it is essentially zero. But when $\lambda$ appears, it introduces a direction in the orientation of spins, and they start flipping in order to align with the external field. This phenomena increase the magnetization until all spins are aligned: as many spins are considered, the transition is more abrupt.

At the end, computing the magnetization is computing the expected value of $\sigma_{z}$, _i.e._ $\langle\sigma_{z}\rangle=\langle\psi|\sigma_{z}|\psi\rangle$. As we have assigned $+1$ the magnetization of $|0\rangle$ state and $-1$ the magnetization of the $|1\rangle$ state, for a $N$ spin state

$$ \langle\sigma_{z}\rangle = \frac{1}{N}\sum_{i=1}^{2^N}p_{i}\langle e_{i}|\sigma_{z}|e_{i}\rangle=\frac{1}{N}\sum_{i=1}^{2^N}p_{i}(n^{0}_{i}-n^{1}_{i})=\frac{1}{N}\sum_{i=1}^{2^N}p_{i}(N-2n^{1}_{i}), $$

where $n_{i}^{0}(n_{i}^{1})$ are the number of zeros(ones) in the computational basis state $|e_{i}\rangle$ with probability $p_{i}$  $(|e_{i}\rangle=|0\cdots00\rangle,|0\cdots01\rangle,|0\cdots10\rangle,...,|1\cdots11\rangle)$. Then, it will be useful to define a subroutine to count the number of ones:

```python
#Import dependencies
from matplotlib import pyplot as plt
import numpy as np
from qiskit import Aer
from qiskit import *
import qiskit.tools.jupyter
from mpl_toolkits.axes_grid1 import make_axes_locatable
from qiskit.visualization import *
from qiskit.ignis.mitigation.measurement import complete_meas_cal,CompleteMeasFitter

%matplotlib inline
%qiskit_version_table
```

Version Information

Qiskit Software	Version

Qiskit	0.19.6

Terra	0.14.2

Aer	0.5.2

Ignis	0.3.3

Aqua	0.7.3

IBM Q Provider	0.7.2

System information

Python	3.7.6 (default, Jan  8 2020, 20:23:39) [MSC v.1916 64 bit (AMD64)]

OS	Windows

CPUs	2

Memory (Gb)	7.886066436767578

Wed Aug 12 16:55:27 2020 Hora de Verão de GMT

```python
def digit_sum(n):
    num_str = str(n)
    sum = 0
    for i in range(0, len(num_str)):
        sum += int(num_str[i])
    return sum
```

## Exact solution for  transverse Ising model

The exact solution of Ising model (and its generalization, the $XY$ model) was found by Lieb, Schultz and Mattis in 1961 [[4](https://www.sciencedirect.com/science/article/pii/0003491661901154)] and later, the solution for the transverse Ising model, i.e. with the introduction of external magnetic field, by Katsura in 1962 [[5](https://journals.aps.org/pr/abstract/10.1103/PhysRev.127.1508)]. The method contains 3 steps:

1) Jordan-Wigner Transformation

2) Fourier Transform

3) Bogoliubov Transformation

We will summarize how to implement these steps to finally obtain a unitary transformation that disentangles the Ising hamiltonian, i.e. diagonalize the hamiltonian.

### Finite size chain

The recipe of the exact solution for the transverse Ising model that we will introduce will solve the model as it was an infinite chain with periodic boundary conditions. As this will not be true when we will design the circuit (we work with a finite number of qubits), we should introduce an extra term in order to correctly map the periodic boundary conditions between spins to fermionic degrees of freedom after doing the Jordan-Wigner transformation. This term will be suppressed in the large $n$ limit, but will introduce some finite size effects in our result, as we will only consider 4 spins. Taking this into account, the transverse Ising hamiltonian reads

$$H=\sum_{i=1}^{n}\sigma^{x}_{i}\sigma^{x}_{i+1} + \lambda\sum_{i=1}^{n}\sigma^{z}_{i} + \sigma_{1}^{y}\sigma_{2}^{z}\cdots\sigma_{n-1}^{z}\sigma_{n}^{y},$$

### Jordan-Wigner Transformation

This transformation maps the spin operators into fermionic modes. It is defined as

$$
c_{j}=\left(\prod_{l<j}\sigma_{l}^{z}\right)\frac{\sigma_{j}^{x}-i\sigma_{j}^{y}}{2}, \qquad
c_{j}^\dagger=\left(\prod_{l<j}\sigma_{l}^{z}\right)\frac{\sigma_{j}^{x}+i\sigma_{j}^{y}}{2},
$$

where $c_{j}^\dagger$ and $c_{j}$ are the fermionic creation and annihilation operators acting on the vacuum state $|\Omega_{c}\rangle$ and satisfy

$$ \{c_{i},c_{j}\}=0, \qquad \{c_{i},c_{j}^\dagger\}=\delta_{ij}, \qquad c_{i}|\Omega_{c}\rangle=0.$$

Then, the hamiltonian become

$$ H_{c}=\frac{1}{2}\sum_{i=1}^{n}\left(c_{i+1}^{\dagger}c_{i}+c_{i}^{\dagger}c_{i+1}+c_{i}^{\dagger}c_{i+1}^{\dagger}+c_{i}c_{i+1}\right)+\lambda\sum_{i=1}^{n}c_{i}^{\dagger}c_{i}.$$

Thus, this transformation takes a state of spin-1/2 particles and turns it into a fermionic state. In terms of the wave function

$$ |\Psi\rangle=\sum_{i_{1},i_{2},\cdots,i_{n}=0,1}\psi_{i_{1},i_{2},\cdots,i_{n}}|i_{1},i_{2},\cdots,i_{n}\rangle \xrightarrow{J.W.} |\Psi\rangle_{c}=\sum_{i_{1},i_{2},\cdots,i_{n}=0,1}\psi_{i_{1},i_{2},\cdots,i_{n}}(c_{1}^\dagger)^{i_1}(c_{2}^\dagger)^{i_2}\cdots(c_{n}^\dagger)^{i_n}|\Omega_{c}\rangle.$$

Notice that the coefficients $\psi_{i_{1},i_{2},\cdots,i_{n}}$ do not change! Then, we will not need to implement any gates over our register to do this step. This is a huge simplification. The only thing we should take care is that any further swap will carry a minus sign if it is implemented between two occupied modes, following the rules of the anticommutation relations written above. To be more specific, any crossed line in our circuit, if needed, should be implemented with a fermionic SWAP gate:

$$ fSWAP=\left(\begin{array}{cccc}1&0&0&0\\0&0&1&0\\0&1&0&0\\0&0&0&-1\end{array}\right). $$

```python
def fswap():
    circuit = QuantumCircuit(2, name="FSWAP")
    circuit.cx(0, 1)
    circuit.cx(1, 0)
    circuit.cx(0, 1)
    circuit.h(1)
    circuit.cx(0, 1)
    circuit.h(1)

    return circuit
fswap_circ=fswap()
fswap_circ.draw(output="mpl")
```

But how do we know that this is in fact the circuit we want? One way is to make all the calculations by hand, the other way is to use Qiskit's `unitary_simulator`. But first we also define a function to help us visualize things:

```python
def get_plot(circ):
    simulator=Aer.get_backend("unitary_simulator")
    unitary=execute(circ,simulator).result().get_unitary()
    unitary_real=np.real(unitary)
    unitary_imag=np.imag(unitary)

    unitary_real[np.where(np.abs(unitary_real)<1e-10)]=0
    unitary_imag[np.where(np.abs(unitary_imag)<1e-10)]=0

    fig=plt.figure(figsize=(12,8))
    ax1=fig.add_subplot(121)
    ax2=fig.add_subplot(122)

    im1=ax1.imshow(unitary_real, cmap="Blues", vmin=-1,vmax=1)
    im2=ax2.imshow(unitary_imag,cmap="Blues",vmin=-1,vmax=1)

    divider1 = make_axes_locatable(ax1)
    divider2 = make_axes_locatable(ax2)
    cax1 = divider1.append_axes('right', size='5%', pad=0.05)
    cax2 = divider2.append_axes('right', size='5%', pad=0.05)

    fig.colorbar(im1, cax=cax1, orientation='vertical')
    fig.colorbar(im2, cax=cax2, orientation='vertical')

    for (j,i),label in np.ndenumerate(unitary_real):
        try:
            if label<0:
                ax1.text(i,j,float(str(label)[:6]),ha='center',va='center')
            else:
                ax1.text(i,j,float(str(label)[:5]),ha='center',va='center')
        except:
            ax1.text(i,j,label,ha='center',va='center')

    for (j,i),label in np.ndenumerate(unitary_imag):
        try:
            ax2.text(i,j,float(str(label)[:5]),ha='center',va='center')
        except:
            ax2.text(i,j,label,ha='center',va='center')

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

```python
get_plot(fswap_circ)
```

### Fourier Transform

The Fourier transformation exploits translational invariance and takes $H_c$ into a momentum space hamiltonian $H_b$:

$$ \begin{eqnarray}
b_{k}&=&\frac{1}{\sqrt{n}}\sum_{j=1}^{n}e^{i\frac{2\pi}{n}jk}c_{j}, \qquad b_{k}^{\dagger}&=&\frac{1}{\sqrt{n}}\sum_{j=1}^{n}e^{-i\frac{2\pi}{n}jk}c_{j}^{\dagger}, \qquad k=-\frac{n}{2}+1,\cdots,\frac{n}{2}.
\end{eqnarray}$$

If $n=2^m$ for some integer $m$, it is possible to decompose the Fourier transform into two parallel Fourier transforms between the even and odd sites [[6](https://arxiv.org/abs/1310.7605)]. In fact a nice way to look at it (as sugested by [[6](https://arxiv.org/abs/1310.7605)]) is to first recall the classical algorithm for the fast fourier transform, first discovered by Carl Friedrich Gauss in 1805, and later popularized by  James Cooley of IBM and John Tukey of Princeton on a paper published in 1965 reinventing the algorithm and describing how to perform it conveniently on a computer, where we break down the whole fourier transform in terms of 2 qubit gates and add a so-called twiddle factor with a $Z$ rotation. We can then take a closer look:

$$ \frac{1}{\sqrt{n}}\sum_{j=0}^{n-1}e^{i\frac{2\pi}{n}jk}c_{j} = \frac{1}{\sqrt{n/2}}\sum_{j'=0}^{n/2-1}e^{i\frac{2\pi}{n/2}j'k}c_{2j'} + \frac{e^{i\frac{2\pi k}{n}}}{\sqrt{n/2}}\sum_{j'=0}^{n/2-1}e^{i\frac{2\pi}{n/2}j'k}c_{2j'+1}. $$

Then $b_{k}$ and $b_{k}^{\dagger}$ can be written as

$$ b_{k}\equiv \frac{1}{\sqrt{2}}\left(b_{k_{even}} + e^{i\frac{2\pi k}{n}}b_{k_{odd}}\right), \qquad b_{k}^{\dagger}\equiv \frac{1}{\sqrt{2}}\left(b^{\dagger}_{k_{even}} + e^{i\frac{2\pi k}{n}}b^{\dagger}_{k_{odd}}\right). $$

Generically, this transformation can be implemented with the unitary two qubit gates

$$ F_{2}=\left(\begin{array}{cccc} 1&0&0&0\\ 0& \frac{1}{\sqrt{2}}& \frac{1}{\sqrt{2}} &0\\
0& \frac{1}{\sqrt{2}}& -\frac{1}{\sqrt{2}} &0\\ 0&0&0& -1\end{array}\right) $$

and a one qubit gate which implements de phase-delay that takes care of the so-called twiddle-factor $e^{\frac{2\pi ik}{n}}$

$$ \left(\begin{array}{cc}1&0\\0& e^{\frac{2\pi ik}{n}} \end{array}\right). $$

All together, the Fourier transform gate become
$$ F^{n}_{k}=\left(\begin{array}{cccc} 1&0&0&0\\ 0& \frac{1}{\sqrt{2}}& \frac{1}{\sqrt{2}} &0\\
0& \frac{e^{-\frac{2\pi ik}{n}}}{\sqrt{2}}& -\frac{e^{-\frac{2\pi ik}{n}}}{\sqrt{2}} &0\\ 0&0&0& -e^{-\frac{2\pi ik}{n}}\end{array}\right). $$

As we will restrict to $n=4$ spins, then the Fourier transform gates that we will need are only

$$ F_0\equiv F^{4}_{0}=F_{2}=\left(\begin{array}{cccc} 1&0&0&0\\ 0& \frac{1}{\sqrt{2}}& \frac{1}{\sqrt{2}} &0\\
0& \frac{1}{\sqrt{2}}& -\frac{1}{\sqrt{2}} &0\\ 0&0&0& -1\end{array}\right), \qquad
F_1\equiv F^4_1=\left( S^{\dagger}\otimes\mathbb{I}\right)F_{2}\left(\begin{array}{cccc} 1&0&0&0\\ 0& \frac{1}{\sqrt{2}}& \frac{1}{\sqrt{2}} &0\\
0& \frac{-i}{\sqrt{2}}& \frac{i}{\sqrt{2}} &0\\ 0&0&0& i\end{array}\right). $$

```python
def F():
    """Here we define the 2qubit fermionic fourier transform"""
    circuit = QuantumCircuit(2, name="F")
    circuit.cx(1, 0)
    circuit.sdg(1)
    circuit.h(1)
    circuit.tdg(1)
    circuit.cx(0, 1)
    circuit.t(1)
    circuit.h(1)
    circuit.s(1)
    circuit.cx(1, 0)
    circuit.h(0)
    circuit.cx(1, 0)
    circuit.h(0)

    return circuit
F().draw(output="mpl")
```

```python
get_plot(F()) #For the F0
```

```python
qc=F()
qc.sdg(1)
get_plot(qc)
```

After Fourier transformation, the hamiltonian become

$$ H_{b}=\sum_{i=0}^{n-1}\left[2\left(\lambda-\cos\left(\frac{2\pi k}{n}\right)\right)b_{k}^{\dagger}b_{k}+i\sin\left(\frac{2\pi k}{n}\right)\left(b_{-k}^{\dagger}b_{k}^{\dagger}+b_{-k}b_{k}\right)\right]. $$

### Bogoliubov transformation

The last step is to decouple the modes with opposite momentum. This is done using the Bogoliubov transformation, which in particular for Ising model is

$$ \begin{eqnarray}
a_{k}&=&\cos\left(\frac{\theta_{k}}{2}\right)b_{k} - i\sin\left(\frac{\theta_{k}}{2}\right)b_{-k}^\dagger ,\quad a_{k}^\dagger &=&\cos\left(\frac{\theta_{k}}{2}\right)b_{k}^\dagger + i\sin\left(\frac{\theta_{k}}{2}\right)b_{-k}, \quad \theta_{k}=-\arccos\left(\frac{\lambda-\cos\left(\frac{2\pi k}{n}\right)}{\sqrt{\left(\lambda-\cos\left(\frac{2\pi k}{n}\right)\right)^2+\sin^2\left(\frac{2\pi k}{n}\right)}}\right)
\end{eqnarray}.$$

Then, we finally arrive to the diagonal hamiltonian

$$H_{a}=\sum_{k}\omega_{k}a_{k}^{\dagger}a_{k},$$

where $\omega_{k}=\sqrt{\left(\lambda-\cos\left(\frac{2\pi k}{n}\right)\right)^2+\sin^2\left(\frac{2\pi k}{n}\right)}$.

This transformation is between the modes of opposite momentum and can be implemented using the unitary gate

$$U_{Bog}\equiv B_{k}^{n}=\left(\begin{array}{cccc}\cos\left(\frac{\theta_{k}}{2}\right)&0&0&i\sin\left(\frac{\theta_{k}}{2}\right)\\0&1&0&0\\0&0&1&0\\i\sin\left(\frac{\theta_{k}}{2}\right)&0&0&\cos\left(\frac{\theta_{k}}{2}\right)\end{array}\right).$$

where $RX$, $RY$ and $RZ$ are the rotation gates ($RZ(\theta)=U_{1}(-\theta)$ and $RY(\theta)=U_{3}(\theta,0,0)$) and the above decomposition of the controlled-RX gate has been obtained with Barenco _et al._ method [[7](https://arxiv.org/abs/quant-ph/9503016)].

For $n=4$, we have only two Bogoliubov gates, $B_{0}^{4}$ and $B_{1}^{4}$:

$$ B_{0}^{4}\equiv B_{0}=\left\{\begin{array}{cc} \left(\begin{array}{cccc}1&0&0&0\\0&1&0&0\\0&0&1&0\\0&0&0&1\end{array}\right) & \mathrm{for \ \lambda\geq1}\\ \left(\begin{array}{cccc}0&0&0&-i\\0&1&0&0\\0&0&1&0\\-i&0&0&0\end{array}\right) & \mathrm{for \ \lambda\leq1}\end{array}\right., \qquad
B_{1}^{4}\equiv B_{1}=\left(\begin{array}{cccc}\cos\left(\frac{1}{2}\arccos\left(\frac{\lambda}{\sqrt{1+\lambda^2}}\right)\right)&0&0& i\sin\left(\frac{1}{2}\arccos\left(\frac{\lambda}{\sqrt{1+\lambda^2}}\right)\right)\\0&1&0&0\\0&0&1&0\\ i\left(\frac{1}{2}\arccos\left(\frac{\lambda}{\sqrt{1+\lambda^2}}\right)\right)&0&0&\cos\left(\frac{1}{2}\arccos\left(\frac{\lambda}{\sqrt{1+\lambda^2}}\right)\right)\end{array}\right).$$

Notice that for $\lambda\geq 1$, $B_{0}$ does nothing (as it is the identity), and for $\lambda\leq 1$ it only affects qubits in the state $|00\rangle$ and $|11\rangle$. Then, as the Bogoliubov transformation will be the first operation to entangle again the state into the Ising basis, depending on the initial state, we can remove this gate.

```python
def bog(lam, k=1):
    a = "B_"+str(k)
    circuit = QuantumCircuit(2, name=a)
    n=4
    th = -np.arccos((lam-np.cos(2*np.pi*k/n)) /np.sqrt((lam-np.cos(2*np.pi*k/n))**2+np.sin(2*np.pi*k/n)**2))

    circuit.x(1)
    circuit.cx(1, 0)
    # controlled RX
    circuit.u1(-np.pi/2, 1)
    circuit.u3(th/2, 0., 0., 1)
    circuit.cx(0, 1)
    circuit.u3(-th/2, 0., 0., 1)
    circuit.cx(0, 1)
    circuit.u1(np.pi/2, 1)
    # ----------
    circuit.cx(1, 0)
    circuit.x(1)

    return circuit

bog(0.5,k=1).draw(output="mpl")
```

```python
get_plot(bog(0.5,k=1))
```

## Disentangle gate

What we have done is describing all steps to construct a disentangling gate

$$ H = U_{dis}\tilde{H}U_{dis}^{\dagger}, $$

where $H$ is in this case the Ising hamiltonian and $\tilde{H}$ is a non interacting hamiltonian, _i.e._ can be written as $\tilde{H}=\sum_{i=1}^{n}\epsilon_{i}\sigma^{z}_{i}$. The eigenstates of $\tilde{H}$ are product states, so they are very easy to prepare in a quantum computer. Then, applying the $U_{dis}$ gate over these states, one obtains the Ising spectrum:

$$ \begin{eqnarray}
\tilde{H}|\psi_{i}\rangle &=& \epsilon_{i}|\psi_{i}\rangle, \longrightarrow
%\underbrace{U_{dis}\tilde{H}U_{dis}^{\dagger}}_{H}U_{dis}|\psi_{i}\rangle&=&\epsilon_{i}U_{dis}U_{dis}^{\dagger}U_{dis}|\psi_{i}\rangle, \\
H\underbrace{U_{dis}|\psi_{i}\rangle}_{|\varphi_{i}\rangle}&=&\epsilon_{i}\underbrace{U_{dis}|\psi_{i}\rangle}_{|\varphi_{i}\rangle},
\end{eqnarray}$$

where $|\psi_{i}\rangle$ are the eigenstates of $\tilde{H}$, $|\varphi_{i}\rangle$ are the eigenstates of Ising hamiltonian and $\epsilon_{i}$ the corresponding energies that can be computed from $\omega_{k}$ of the $H_{a}$. For instance, the ground state energy is

$$ \epsilon_{0}=-\sum_{k=-n/2+1}^{n/2}\omega_{k}= -\left(2\sqrt{1+\lambda^2}+\lambda+1+|\lambda-1|\right), $$

and the energies of the excited states can be obtained applying $a^{\dagger}_{k}$ over the ground state.

As we have seen, to disentangle the Ising hamiltonian we need to apply first a Fourier transform followed by a Bogoliubov transformation. So, starting from a _disentangled_ state, _i.e._ a product state, to transform it again to an Ising eigenstate, we need to implement the gates in the inverse order:

$$ \tilde{H}\equiv H_{a} \xrightarrow{U_{Bog}} H_{b} \xrightarrow{U_{FT}} H_{c} \longrightarrow H. $$

Then

$$ U_{dis}=U_{FT}U_{Bog}. $$

## Final circuit

Notice that, if even and odd qubits are connected, it is not necessary to introduce the $fSWAP$ gates. This will be the case if we implement the above circuit in the ibmqx5 device, since the qubit architecture allows the connectivity of qubits in a square.

### Ground state $n=4$ Ising model

Due to finite size effects, the ground state in the diagonal basis depends on $\lambda$ value:

$$ |\psi\rangle_{g.s}=\left\{\begin{array}{cc} |0001\rangle & \mathrm{for \ \lambda<1}\\ |0000\rangle & \mathrm{for \ \lambda>1} \end{array}\right. $$

As the qubits are initialized in the $|0\rangle$ state, then it is not necessary to apply any gate to prepare the circuit for the simulations of $\lambda>$ and only an $X$ gate over last qubit for $\lambda<1$.

```python
def get_circ(lam, barriers=True, with_initial=True):
    circuit = QuantumCircuit(4, 4)

    if with_initial:
        if lam < 1:
            circuit.x(3)

    circuit.append(bog(lam, k=1), [0, 1])

    circuit.append(F(), [0, 1])
    circuit.rz(-2*np.pi/4*1, 0)  # twiddle factor
    circuit.append(F(), [2, 3])
    if barriers:
        circuit.barrier()
    circuit.append(fswap(), [1, 2])
    circuit.append(F(), [0, 1])
    circuit.append(F(), [2, 3])
    circuit.append(fswap(), [1, 2])

    if barriers:
        circuit.barrier()
    circuit.measure(0, 0)
    circuit.measure(1, 1)
    circuit.measure(2, 2)
    circuit.measure(3, 3)

    return circuit

get_circ(0.25).draw(output="text", fold=2000, vertical_compression="high", initial_state=True, cregbundle=True)
```

        ┌──────┐┌────┐┌───────────┐ ░           ┌────┐           ░ ┌─┐
q_0: |0>┤0     ├┤0   ├┤ RZ(-pi/2) ├─░───────────┤0   ├───────────░─┤M├─────────
        │  B_1 ││  F │└───────────┘ ░ ┌────────┐│  F │┌────────┐ ░ └╥┘┌─┐
q_1: |0>┤1     ├┤1   ├──────────────░─┤0       ├┤1   ├┤0       ├─░──╫─┤M├──────
        └──────┘├────┤              ░ │  FSWAP │├────┤│  FSWAP │ ░  ║ └╥┘┌─┐
q_2: |0>────────┤0   ├──────────────░─┤1       ├┤0   ├┤1       ├─░──╫──╫─┤M├───
         ┌───┐  │  F │              ░ └────────┘│  F │└────────┘ ░  ║  ║ └╥┘┌─┐
q_3: |0>─┤ X ├──┤1   ├──────────────░───────────┤1   ├───────────░──╫──╫──╫─┤M├
         └───┘  └────┘              ░           └────┘           ░  ║  ║  ║ └╥┘
 c: 0 4/════════════════════════════════════════════════════════════╩══╩══╩══╩═
                                                                    0  1  2  3

We are now ready to simulate our system. We start with the qasm simulator:

```python
mag_sim=[]
lam_values=np.linspace(0,2,15)
qasm=Aer.get_backend("qasm_simulator")
shots=2048
for lam in lam_values:
    qc = get_circ(lam)
    result = execute(qc, backend=qasm, shots=shots).result()
    res = result.get_counts()
    r1 = list(res.keys())
    r2 = list(res.values())
    M = 0
    for j in range(0, len(r1)):
        M = M+(4-2*digit_sum(r1[j]))*r2[j]/shots
    mag_sim.append(M/4)
print("Done")
```

    Done

```python
def exact(lam):
    if lam < 1:
        return lam/(2*np.sqrt(1+lam**2))
    if lam > 1:
        return 1/2+lam/(2*np.sqrt(1+lam**2))
    return None

vexact = np.vectorize(exact)
l = np.arange(0.0, 2.0, 0.01)

fig=plt.figure(figsize=(10,6))
ax=fig.add_subplot(111)

ax.plot(l, vexact(l), linestyle="solid", color="black", label="Exact")
ax.scatter(lam_values, mag_sim, marker="^", s=80, c="red", label="QASM simulator")

ax.set_xlabel(r'$\lambda$')
ax.set_ylabel(r'$\langle \sigma_z \rangle$')
ax.set_title('Magnetization of the ground state of n=4 Ising spin chain')
ax.legend()
ax.grid()
plt.show()
```

#### Real device

Now we turn our attention to a real device. By the time of writing the available devices are:

1. ibmq_5_yorktown (formerly known as ibmqx2) -- 5 qubits -- QV 8
2. ibmq_16_melbourne -- 15 qubits -- QV 8
3. ibmq_vigo -- 5 qubits -- QV 16
4. ibmq_ourense -- 5 qubits -- QV 8
5. ibmq_burlington -- 5 qubits -- QV 8 (Retiring on Aug. 31)
6. ibmq_essex -- 5 qubits -- QV 8 (Retiring on Aug. 31)
7. ibmq_london -- 5 qubits -- QV 8 (Retiring on Aug. 31)
8. ibmq_armonk -- 1 qubit -- Pulse access
9. ibmq_valencia -- 5 qubits -- QV 16
10. ibmq_santiago -- 5 qubits -- QV 32 (Access starting Aug. 7)

Therefore, to motivate our study on Quantum Computation, we will make use of the device with the highest quantum volume, so we can expect great results for a NISQ device.

```python
provider=IBMQ.load_account()
provider.backends()
```

    [<IBMQSimulator('ibmq_qasm_simulator') from IBMQ(hub='ibm-q', group='open', project='main')>,
     <IBMQBackend('ibmqx2') from IBMQ(hub='ibm-q', group='open', project='main')>,
     <IBMQBackend('ibmq_16_melbourne') from IBMQ(hub='ibm-q', group='open', project='main')>,
     <IBMQBackend('ibmq_vigo') from IBMQ(hub='ibm-q', group='open', project='main')>,
     <IBMQBackend('ibmq_ourense') from IBMQ(hub='ibm-q', group='open', project='main')>,
     <IBMQBackend('ibmq_valencia') from IBMQ(hub='ibm-q', group='open', project='main')>,
     <IBMQBackend('ibmq_london') from IBMQ(hub='ibm-q', group='open', project='main')>,
     <IBMQBackend('ibmq_burlington') from IBMQ(hub='ibm-q', group='open', project='main')>,
     <IBMQBackend('ibmq_essex') from IBMQ(hub='ibm-q', group='open', project='main')>,
     <IBMQBackend('ibmq_armonk') from IBMQ(hub='ibm-q', group='open', project='main')>,
     <IBMQBackend('ibmq_santiago') from IBMQ(hub='ibm-q', group='open', project='main')>]

```python
device=provider.get_backend("ibmq_santiago")
# device
```

Below you can see the qubit coupling map for our device. Remember that the system we started with assumed periodic boundary conditions, therefore the best possible qubit configuration is "ring-like". However, as can be seen below, our device does not have that coupling, however due to the high gate fidelity and/or low error rates it's a worthy trade off.

Note: Currently a device that would satisfy the ideal configuration would be IBMQ 16 Melbourne.

```python
plot_gate_map(device, figsize=(3,3))
```

First let's unroll our circuit:

```python
qc=get_circ(0.25, barriers=False)
print("Operations: ", qc.decompose().count_ops())
print("Total number of gates: ", qc.decompose().size())
print("Depth: ", qc.decompose().depth())
qc.decompose().draw(output="mpl",vertical_compression="high", initial_state=True, cregbundle=True)
```

Note the high amount of gates and depth that our "raw" circuit has. Can we make it better?

```python
circuit_transpiled = transpile(get_circ(0.25, barriers=False), backend=device, optimization_level=3)
print("Operations: ", circuit_transpiled.count_ops())
print("Total number of gates: ", circuit_transpiled.size())
print("Depth: ", circuit_transpiled.depth())
print("Number of gates removed: ", qc.decompose().size()-circuit_transpiled.size())
circuit_transpiled.draw(output="mpl",vertical_compression="high", initial_state=True, cregbundle=True)
```

Having optimized our circuit, we move on to the next part: error mitigation. When running on an actual device, errors are inevitable, however we can try to mitigate some of the measurement errors as is explained [here](https://qiskit.org/textbook/ch-quantum-hardware/measurement-error-mitigation.html). Notice also that it requires $2^N$ gates to get a calibration matrix. Since here we only have 4 qubits it is fairly easy to do so, but for greater systems it may be expensive.

```python
#get the mitigation matrix
%qiskit_job_watcher
meas_calibs, state_labels = complete_meas_cal(qr=QuantumRegister(4)) #first we get the measurement calibration circuits: one for each basis state->2^n circuits
cal_results = execute(meas_calibs,
                      backend=device,
                      shots=4096).result()

meas_fitter = CompleteMeasFitter(cal_results, state_labels)
```

    Accordion(children=(VBox(layout=Layout(max_width='710px', min_width='710px')),), layout=Layout(max_height='500…

    <IPython.core.display.Javascript object>

```python
def get_plot_matrix(matrix):

    fig=plt.figure(figsize=(10,10))
    ax1=fig.add_subplot(111)

    divider = make_axes_locatable(ax1)
    cax = divider.append_axes('right', size='5%', pad=0.05)
    im = ax1.imshow(matrix, cmap="Blues", vmin=0,vmax=1)
    fig.colorbar(im, cax=cax, orientation='vertical')

    for (j,i),label in np.ndenumerate(matrix):
        try:
            if label<0:
                ax1.text(i,j,float(str(label)[:5]),ha='center',va='center')
            else:
                ax1.text(i,j,float(str(label)[:4]),ha='center',va='center')
        except:
            ax1.text(i,j,label,ha='center',va='center')

    plt.show()

get_plot_matrix(meas_fitter.cal_matrix)
```

Notice how the calibration matrix is almost diagonal! This demonstrates the quality of measurement of the device.

```python
circuits=[]
lam=np.linspace(0,2,20)
for i in lam:
    circuits.append(transpile(get_circ(i,barriers=False), backend=device, optimization_level=3))

# jobs_dev=execute(circuits,backend=device,shots=2048)
jobs_dev=device.retrieve_job("5f327675252dfd001a34a3b7")
```

```python
def get_M(res):
    r1 = list(res.keys())
    r2 = list(res.values())
    shots=np.sum(r2)
    M = 0
    for j in range(0, len(r1)):
        M = M+(4-2*digit_sum(r1[j]))*r2[j]/shots
    return M

N = 20

mag_sim=[]
mag_dev = np.zeros((2,N))
lam = np.linspace(0, 2, N)

for i in range(len(lam)):
    lam_value = lam[i]
    qc = get_circ(lam_value)
    result = execute(qc, backend=Aer.get_backend("qasm_simulator"), shots=2048).result().get_counts()
    result_dev=jobs_dev.result().get_counts(i)
    result_mitigated = meas_fitter.filter.apply(result_dev)

    mag_sim.append(get_M(result)/4)
    mag_dev[0, i] = get_M(result_dev)/4
    mag_dev[1, i] = get_M(result_mitigated)/4
print("Done")
```

    Done

```python
def exact(lam):
    if lam < 1:
        return lam/(2*np.sqrt(1+lam**2))
    if lam > 1:
        return 1/2+lam/(2*np.sqrt(1+lam**2))
    return None

fig = plt.figure(figsize=(10, 6))
ax1 = fig.add_subplot(111)

vexact = np.vectorize(exact)
l = np.arange(0.0, 2.0, 0.01)
ax1.scatter(lam, mag_sim, c="blue",label="Simulation")
ax1.scatter(lam, mag_dev[0, :],marker="x", c="red", label="Raw data")
ax1.scatter(lam, mag_dev[1, :], c="red", label="Mitigated")

ax1.plot(l, vexact(l), 'k', label='exact')
ax1.set_xlabel(r'$\lambda$')
ax1.set_ylabel(r'$\langle \sigma_z \rangle$')
ax1.set_title("IBMQ 16 Melbourne results")
ax1.legend()
ax1.grid()

plt.show()
```

The jump in the magnetization is due to the finite size effect. If we consider more and more spins, this jump decrease it until desappear in the limit $n\rightarrow \infty$.

## Time evolution

Schrödinger equation describes the time evolution of a quantum system:

$$ i\hbar\frac{d}{dt}|\Psi(t)\rangle=H(t)|\Psi(t)\rangle. $$

Then, the time evolution of a state if the hamiltonian is not time-dependent, _i.e._ $H\neq H(t)$, is

$$|\Psi(t)\rangle=U(t,t_{0})|\Psi(t_{0})\rangle \quad \mathrm{with} \quad U(t,t_{0})=e^{-i(t-t_{0})H}.$$

For simplicity, we will set $t_{0}=0$ and $|\Psi(t_{0})\rangle\equiv|\Psi_{0}\rangle$.

Using the stationary states basis (eigenstates of the hamiltonian),

$$ |\Psi(t)\rangle=\sum_{n}e^{-iE_{n}(t-t_{0})}|E_{n}\rangle\langle E_{n}|\Psi_{0}\rangle. $$

We can now check what is the time evolution of an operator

$$ \langle\Psi(t)|\mathcal{O}|\Psi(t)\rangle= \sum_{n}\sum_{m}e^{-it(E_{n}-E_{m})} \langle\Psi_{0}|E_{m}\rangle\langle E_{m}|\mathcal{O}|E_{n}\rangle\langle E_{n}|\Psi_{0}\rangle.$$

* If $|\Psi_{0})\rangle$ is an stationary state, then $\langle\Psi(t)|\mathcal{O}|\Psi(t)\rangle=\sum_{n}\langle E_{n}|\mathcal{O}|E_{n}\rangle$ and it is constant in time.
* If $[H,\mathcal{O}]=0$, then $\mathcal{O}$ has the same eigenstates as $H$ and $\langle\Psi(t)|\mathcal{O}|\Psi(t)\rangle=\langle \Psi_{0}|\mathcal{O}|\Psi_{0}\rangle$ and it is a constant of motion.

Then, if none of the above cases are fulfilled, the expectation value of $\mathcal{O}$ will oscillate according to the frequencies $(E_{n}-E_{m})$.

For the case of magnetization and the transverse Ising model, as $[H,\hat{\sigma}_{z}]\neq0$, we can observe a time evolution of an operator if we choose $|\Psi_{0}\rangle$ not being a stationary state.

### Time evolution operator

We can take advantage of our circuit to implement the time evolution in a very easy way: we only need to compute the time evolution operator over our diagonalized hamiltonian and then apply the $U_{dis}$ gate to transform it into the Ising state. As the eigenstates of $\tilde{H}$ are the states in the computational basis, the time evolution operation consists only to add the corresponding phase $e^{-itE_{n}}$ over the chosen state:

1) We choose some initial state written in terms of the $\tilde{H}$ basis $$ |\Psi_{0}\rangle = \sum_{i}c_{i}|\psi_{i}\rangle. $$ A linear combination of eigenstates is also an eigenstate, so $|\Psi_{0}\rangle$ is a stationary state of $\tilde{H}$.

2) We apply the time evolution operator over this state $$ |\Psi(t)\rangle=e^{-it\tilde{H}}|\Psi_{0}\rangle= \sum_{i}c_{i}e^{-it\epsilon_{i}}|\psi_{i}\rangle. $$

If we stop here and compute the magnetization, it will be constant, as we are evolving a stationary state... in terms of $\tilde{H}$ basis, but $|\Psi_{0}\rangle$ will not be necessarily an eigenstate of the Ising hamiltonian!

3) We implement the $U_{dis}$ gate to transform the state to the Ising basis $$ |\Phi(t)\rangle= U_{dis}|\Psi(t)\rangle= U_{dis}\sum_{i}c_{i}e^{-it\epsilon_{i}}|\psi_{i}\rangle. $$

Now, $|\Phi(t)\rangle$ is not, in general, an eigenstate of the Ising hamiltonian, so we expect to observe an oscillating time evolution of magnetization.

The key point here is that to implement the time evolution over a computational state is very easy, as we only need to add a phase over the qubits involve. Let's see a particular example.

### Example: time evolution of $|\uparrow\uparrow\uparrow\uparrow\rangle$ state

We want to observe the time evolution of the state of all spins align in the positive direction of $\sigma_{z}$. This is not an eigenstate of Ising hamiltonian, since "all spins aligned" is a degenerate state (all spins up or all spins down). First we need to know what is the corresponding state in the diagonal basis (remember that we choose $|\uparrow\rangle\equiv|0\rangle$):

$$ |\psi_{0}\rangle=U^{\dagger}_{dis}|0000\rangle=\cos\phi|0000\rangle+i\sin\phi|1100\rangle , \qquad \phi=\frac{1}{2}\arccos\left(\frac{\lambda}{\sqrt{1+\lambda^2}}\right).$$

Secondly, we compute the time evolution of this state, _i.e._ we add the corresponding phases $e^{-it\epsilon_{i}}$, where $\epsilon_{i}$ are the energies of the states $|0000\rangle$ and $|1100\rangle$:

$$ |\psi(t)\rangle=e^{-it(-2(\lambda+\sqrt{1+\lambda^2}))}\cos\phi|0000\rangle+ie^{-it2(\lambda-\sqrt{1+\lambda^2})}\sin\phi|1100\rangle = e^{2it(\lambda+\sqrt{1+\lambda^2}))}\left(\cos\phi|00\rangle+ ie^{4it\sqrt{1+\lambda^2}}\sin\phi|11\rangle\right)\otimes|00\rangle,$$

where we can forget the first exponential since it only introduces a phase.

Notice that to prepare this state is only necessary to make a rotation on the first qubit followed by a CNOT; the other two qubits are prepared in the $|0\rangle$ state, which is very convenient.
Once we have $|\psi(t)\rangle$, we can apply $U_{dis}$ and compute the expected value of the magnetization.

#### QASM Simulator

```python
def Initial_time(t,lam):
    qc=QuantumCircuit(4,4)
    qc.u3(np.arccos(lam/np.sqrt(1+lam**2)),np.pi/2.+4*t*np.sqrt(1+lam**2),0.,0)
    qc.cx(0,1)

    return qc

def get_circ_time(time,lam):
    qc=Initial_time(time,lam)
    qc=qc.compose(get_circ(lam,barriers=False, with_initial=False))

    return qc
```

```python
#gather the circuits
circuits=[] #It will have shape (3,15)
mag_time_sim=[]
lam_values=[0.5,0.9,1.8]
time=np.linspace(0,2,15)

for lam in lam_values:
    circuits1=[]
    for t in time:
        circuits1.append(get_circ_time(t,lam))
    circuits.append(circuits1)
```

```python
#Set your jobs for the backend
shots = 1024
qasm = Aer.get_backend("qasm_simulator")

jobs_total=[]
for i in range(len(circuits)):
    jobs_total.append(execute(circuits[i],backend=qasm, shots=shots))
print("Done")
```

    Done

```python
#Data analysis
mag_time_sim=[]
for i in range(len(jobs_total)):
    results=jobs_total[i].result()
    mag_t=[]
    for t in range(len(time)):
        mag_t.append(get_M(results.get_counts(t))/4)
    mag_time_sim.append(mag_t)
print("Done")
```

    Done

```python
#Plotting
def exact_time(lam,tt):
    Mt=(1 + 2*lam**2 + np.cos(4*tt*np.sqrt(1 + lam**2)))/(2 + 2*lam**2)
    return Mt
vexact_t = np.vectorize(exact_time)

time_exact=np.linspace(0,2,200)

fig=plt.figure(figsize=(10,5))
ax=fig.add_subplot(111)

ax.scatter(time,mag_time_sim[0], marker="^", s=100, c="green")
# ax.plot(time,mag_time_sim[0], linestyle="dashed", color="green")
ax.plot(time_exact,vexact_t(lam_values[0],time_exact),color="green",label=r'$\lambda={}$'.format(lam_values[0]))

ax.scatter(time,mag_time_sim[1], marker="^", s=100, c="red")
ax.plot(time_exact,vexact_t(lam_values[1],time_exact),color="red",label=r'$\lambda={}$'.format(lam_values[1]))

ax.scatter(time,mag_time_sim[2], marker="^", s=100, c="blue")
ax.plot(time_exact,vexact_t(lam_values[2],time_exact),color="blue",label=r'$\lambda={}$'.format(lam_values[2]))

ax.set_xlabel('Time')
ax.set_ylabel('$<\sigma_{z}>$')
ax.set_title('Time evolution |↑↑↑↑> state')
ax.legend()
ax.grid()
plt.show()
```

#### Real device: IBMQ Santiago

```python
#Get the error mitigation matrix
meas_calibs, state_labels = complete_meas_cal(qr=QuantumRegister(4)) #first we get the measurement calibration circuits: one for each basis state->2^n circuits
cal_results = execute(meas_calibs,
                      backend=device,
                      shots=4096).result()

meas_fitter = CompleteMeasFitter(cal_results, state_labels)
print("Done")
```

    Done

```python
#gather the circuits
circuits=[] #It will have shape (3,15)
mag_time_sim=[]
lam_values=[0.5,0.9,1.8]
time=np.linspace(0,2,15)

for lam in lam_values:
    circuits1=[]
    for t in time:
        circuit_transpiled=transpile(get_circ_time(t,lam),backend=device,optimization_level=3)
        circuits1.append(circuit_transpiled)
    circuits.append(circuits1)
print("Done")
```

    Done

```python
#Set your jobs for the backend
shots = 1024

jobs_total=[]
for i in range(len(circuits)):
    jobs_total.append(execute(circuits[i],backend=device, shots=shots))
print("Done")
```

    Done

```python
#Data analysis
mag_time=[]
mag_time_mitigated=[]
for i in range(len(jobs_total)):
    results=jobs_total[i].result()
    mag_t=[]
    mag_t_mitigated=[]
    for t in range(len(time)):
        mag_t.append(get_M(results.get_counts(t))/4)
        mag_t_mitigated.append(get_M(meas_fitter.filter.apply(results.get_counts(t)))/4)
    mag_time.append(mag_t)
    mag_time_mitigated.append(mag_t_mitigated)
print("Done")
```

    Done

```python
#Plotting
from matplotlib.gridspec import GridSpec

def exact_time(lam,tt):
    Mt=(1 + 2*lam**2 + np.cos(4*tt*np.sqrt(1 + lam**2)))/(2 + 2*lam**2)
    return Mt
vexact_t = np.vectorize(exact_time)

time_exact=np.linspace(0,2,200)

fig = plt.figure(figsize=(16,7))
gs = GridSpec(3, 2, width_ratios=[0.7,0.3], hspace=0.1, wspace=0.1)
ax1 = fig.add_subplot(gs[:,0])
ax2 = fig.add_subplot(gs[0,1])
ax3 = fig.add_subplot(gs[1,1])
ax4 = fig.add_subplot(gs[2,1])

ax2.set_xticklabels([])
ax3.set_xticklabels([])

ax1.scatter(time,mag_time_mitigated[0], marker="+", s=120, c="green")
ax1.plot(time,mag_time_mitigated[0], linestyle="dashed", color="green")
ax1.plot(time_exact,vexact_t(lam_values[0],time_exact),color="green",label=r'$\lambda={}$'.format(lam_values[0]))

ax1.scatter(time,mag_time_mitigated[1], marker="+", s=120, c="red")
ax1.plot(time,mag_time_mitigated[1], linestyle="dashed", color="red")
ax1.plot(time_exact,vexact_t(lam_values[1],time_exact),color="red",label=r'$\lambda={}$'.format(lam_values[1]))

ax1.scatter(time,mag_time_mitigated[2], marker="+", s=120, c="blue")
ax1.plot(time,mag_time_mitigated[2], linestyle="dashed", color="blue")
ax1.plot(time_exact,vexact_t(lam_values[2],time_exact),color="blue",label=r'$\lambda={}$'.format(lam_values[2]))

ax1.set_xlabel('Time')
ax1.set_ylabel('$<\sigma_{z}>$')
ax1.set_title('Time evolution |↑↑↑↑> state')
ax1.legend()
ax1.grid()

#---------Individual plots---------#
ax2.scatter(time,mag_time_mitigated[0], marker="+", s=120, c="green")
ax2.plot(time,mag_time_mitigated[0], linestyle="dashed", color="green")
ax2.plot(time_exact,vexact_t(lam_values[0],time_exact),color="green",label=r'$\lambda={}$'.format(lam_values[0]))
ax2.legend()
ax2.grid()

ax3.scatter(time,mag_time_mitigated[1], marker="+", s=120, c="red")
ax3.plot(time,mag_time_mitigated[1], linestyle="dashed", color="red")
ax3.plot(time_exact,vexact_t(lam_values[1],time_exact),color="red",label=r'$\lambda={}$'.format(lam_values[1]))
ax3.legend()
ax3.grid()

ax4.scatter(time,mag_time_mitigated[2], marker="+", s=120, c="blue")
ax4.plot(time,mag_time_mitigated[2], linestyle="dashed", color="blue")
ax4.plot(time_exact,vexact_t(lam_values[2],time_exact),color="blue",label=r'$\lambda={}$'.format(lam_values[2]))
ax4.legend()
ax4.grid()

ax4.set_xlabel('Time')

fig.suptitle("IBMQ Santiago | Mitigated data")
plt.show()
```

***
### *References*

[1] R. Orús, *A Practical Introduction to Tensor Networks: Matrix Product States and Projected Entangled Pair States*, Annals of Physics **349**, 117-158 (2014), [arXiv:1306.2164](https://arxiv.org/abs/1306.2164).

[2] F. Verstraete, J. I. Cirac and J. I. Latorre, *Quantum circuits for strongly correlated quantum systems*, Phys. Rev. A **79**, 032316 (2009), [arXiv:0804.1888](https://arxiv.org/abs/0804.1888).

[3] J. I. Latorre and A. Riera, *A short review on entanglement in quantum spin systems*, J. Phys. A: Math. Theor. **42**, 504002 (2009), [arXiv:0906.1499](https://arxiv.org/abs/0906.1499).

[4] E. Lieb, T. Schultz and D. Mattis, *Two soluble models of an antiferromagnetic chain*, [Annals of Physics **16**, Issue 3, 407-466 (1961)](https://www.sciencedirect.com/science/article/pii/0003491661901154).

[5] S. Katsura, *Statistical Mechanics of the Anisotropic Linear Heisenberg Model*, [Phys. Rev. **127**, 1508 (1962)](https://journals.aps.org/pr/abstract/10.1103/PhysRev.127.1508).

[6] A. J. Ferris, *Fourier transform of fermionic systems and the spectral tensor network*, Phys. Rev. Lett. **113**, 010401 (2014), [arXiv:1310.7605](https://arxiv.org/abs/1310.7605).

[7] A. Barenco, C. H. Bennett, R. Cleve, D. P. DiVincenzo, N. Margolus, P. Shor, T. Sleator, J. Smolin, H. Weinfurter, *Elementary gates for quantum computation*, Phys.Rev. A **52**, 3457 (1995), [arXiv:950316](https://arxiv.org/abs/quant-ph/9503016).