# Notebook from srcole/qwm
Path: sfn/Poster viewer distribution by SfN theme.ipynb

# Poster viewer distribution by SfN theme

by Scott Cole

This notebook describes the data collected on poster viewing at the 2016 Society for Neuroscience annual meeting. Specifically, we look at the distribution of viewers at each poster and how that varies with the neuroscience theme. This shows that the Development theme is substantially less popular than other themes, such as Cognition.

For more details, see the associated blog post at srcole.github.io._____no_output_____# 0. Default imports_____no_output_____

%config InlineBackend.figure_format = 'retina'
%matplotlib inline

import numpy as np
import scipy as sp
import matplotlib.pyplot as plt
from tools.plt import scatt_corr, unpair_2cond, scatt_2cond

import seaborn as sns
sns.set_style('white')

import pandas as pd_____no_output_____

# 1. Load data_____no_output_____

df = pd.DataFrame.from_csv('./posterviewers_theme.csv')_____no_output_____key_S = 'Session'
key_P = 'Poster number'
key_N = 'Number of people'
key_T = 'Minutes since 1pm'_____no_output_____

# 2. Distributions of viewers for all posters_____no_output_____

# Count missing and withdrawn posters
N_posters = len(df)
print('# of total posters:', N_posters)

N_withdrawn = sum(df['Number of people']=='W')
print('# of withdrawn posters:', N_withdrawn, ',', np.round(100*N_withdrawn/float(N_posters),1),'%')

N_missing = sum(df['Number of people']=='99')
print('# of missing posters:', N_missing, ',', np.round(100*N_missing/float(N_posters),1),'%')

N_empty = sum(df['Number of people']=='0')
print('# of no viewers:', N_empty, ',', np.round(100*N_empty/float(N_posters),1),'%')

# Remove missing and withdrawn posters
df = df[df[key_N]!='W']
df = df[df[key_N]!='99']

# Convert observations to ints
df[key_N] = df[key_N].astype(int)# of total posters: 2579
# of withdrawn posters: 43 , 1.7 %
# of missing posters: 142 , 5.5 %
# of no viewers: 440 , 17.1 %
plt.figure(figsize=(5,3))
bins = range(12)
df.hist(key_N,bins=bins,ax=plt.gca(),normed=True,color='.5')
plt.xlabel('Number of people at a poster')
plt.ylabel('Probability')
plt.title('')
plt.xticks(np.arange(0.5,11.5,2),np.arange(0,11,2))
plt.tight_layout()
# plt.savefig('/gh/fig/sfn/viewerdist_all')_____no_output_____

# 3. Distributions of viewers separated by theme_____no_output_____

# Separate df into groups by Theme
dfs_grouped = {}
obj_grouped = df.groupby('Theme')
ks_Theme = obj_grouped.groups.keys()
for k in ks_Theme:
    dfs_grouped[k] = obj_grouped.get_group(k)

# Remove 'Techniques' because only 2 posters were counted. Rest have at least 156
dfs_grouped.pop('Techniques')
ks_Theme = list(ks_Theme)[:-1]_____no_output_____# Count of number of posters in each theme for each day
for k in ks_Theme:
    print(k,':',len(dfs_grouped[k]))Cognition : 227
Development : 273
Integrative physiology and behavior : 256
Motivation and emotion : 150
Motor systems : 333
Neural excitability, synapses, and glia : 467
Neurodegenerative disorders and injury : 353
Sensory systems : 333
# Calculate probability distribution for each theme
N_themes = len(ks_Theme)
bins = np.arange(-.5,11,1)
N_bins = len(bins)-1
normviewerdist_bytheme = np.zeros((N_themes,N_bins))
for i in range(N_themes):
    normviewerdist_bytheme[i], bin_edges = np.histogram(dfs_grouped[ks_Theme[i]][key_N],bins=bins,density=True)_____no_output_____# Sort themes by average number of people

# Calculate weighted average
weightedavg_bytheme = np.zeros(N_themes)
for i in range(N_themes):
    weightedavg_bytheme[i] = np.average(range(11),weights=normviewerdist_bytheme[i])

# Get ranking of each theme
theme_ranks = sp.stats.rankdata(weightedavg_bytheme).astype(int) - 1

# Sort themes by weighted average
ks_Theme_sort = ['']*N_themes
weightedavg_sort = np.zeros(N_themes)
normviewerdist_sort = np.zeros((N_themes,N_bins))
for i, irank in enumerate(theme_ranks):
    ks_Theme_sort[irank] = ks_Theme[i]
    weightedavg_sort[irank] = weightedavg_bytheme[i]
    normviewerdist_sort[irank] = normviewerdist_bytheme[i]_____no_output_____np.shape(normviewerdist_sort)_____no_output_____# Make a 2d colorplot
from tools.plt import color2d #from the 'srcole/tools' repo
from matplotlib import cm
plt.figure(figsize=(10,6))
color2d(normviewerdist_sort, cmap=cm.viridis,
        clim=[0,.3], cticks = np.arange(0,.31,.05), color_label='Probability',
            plot_title='Poster popularity\nacross SfN 2016 Themes',
            plot_xlabel='Number of viewers', plot_ylabel='Neuroscience Theme',
            plot_xticks_locs=range(N_bins), plot_xticks_labels=range(N_bins),
            plot_yticks_locs=range(N_themes), plot_yticks_labels=ks_Theme_sort,
        interpolation='none', fontsize_minor=14, fontsize_major=19)

# On top, plot the mean
plt.plot(weightedavg_sort,range(8),'k.',ms=15)_____no_output_____

# 4. Significantly more viewers at cognition and systems posters compared to development
Note that this significant difference holds true for each case across all sessions which had a sufficient number of posters for comparison (Sunday PM and Tuesday PM)_____no_output_____

# Compare development and cognition across each session
k1 = 'Development'
k2 = 'Cognition'
sessions_devcog = df['Session'].unique()
print k1,' vs. ',k2
for s in sessions_devcog:
    print 'Session:', s
    df_t1 = dfs_grouped[k1]
    df_t2 = dfs_grouped[k2]
    N1 = df_t1[df_t1[key_S]==s][key_N]
    N2 = df_t2[df_t2[key_S]==s][key_N]
    print 'Number of posters in',k1,':',len(N1)
    print 'Number of posters in',k2,':',len(N2)
    if len(N1) > 5 and len(N2) > 5:
        print sp.stats.ttest_ind(N1, N2)
    else:
        print 'Not enough data for this session'_____no_output_____# Compare development and sensory systems across each session
k1 = 'Development'
k2 = 'Sensory systems'
sessions_devcog = df['Session'].unique()
print k1,' vs. ',k2
for s in sessions_devcog:
    print 'Session:', s
    df_t1 = dfs_grouped[k1]
    df_t2 = dfs_grouped[k2]
    N1 = df_t1[df_t1[key_S]==s][key_N]
    N2 = df_t2[df_t2[key_S]==s][key_N]
    print 'Number of posters in',k1,':',len(N1)
    print 'Number of posters in',k2,':',len(N2)
    if len(N1) > 5 and len(N2) > 5:
        print sp.stats.ttest_ind(N1, N2)
    else:
        print 'Not enough data for this session'_____no_output_____# Compare development and motor systems across each session
k1 = 'Development'
k2 = 'Motor systems'
sessions_devcog = df['Session'].unique()
print k1,' vs. ',k2
for s in sessions_devcog:
    print 'Session:', s
    df_t1 = dfs_grouped[k1]
    df_t2 = dfs_grouped[k2]
    N1 = df_t1[df_t1[key_S]==s][key_N]
    N2 = df_t2[df_t2[key_S]==s][key_N]
    print 'Number of posters in',k1,':',len(N1)
    print 'Number of posters in',k2,':',len(N2)
    if len(N1) > 5 and len(N2) > 5:
        print sp.stats.ttest_ind(N1, N2)
    else:
        print 'Not enough data for this session'_____no_output_____


# Notebook from gadial/qiskit-tutorials
Path: tutorials/noise/8_tomography.ipynb

 Trusted Notebook" width="500 px" align="left">_____no_output_____# Quantum Tomography Overview

***
### Contributors
Gadi Aleksandrowicz (gadia@il.ibm.com), Christopher J. Wood_____no_output_____

import numpy as np
import itertools
import qiskit
from qiskit import QuantumRegister, QuantumCircuit
from qiskit import Aer
import qiskit.ignis.verification.tomography as tomo
from qiskit.quantum_info import state_fidelity_____no_output_____

## The General Theory

Quantum tomography is an experimental procedure to reconstruct a description of part of quantum system from the measurement outcomes of a specific set of experiments. In Qiskit Ignis we are currently concerned with the following two tomography tasks:

1. **Quantum state tomography**: Given a state-preparation circuit that prepares a system in a state, reconstruct a description of the density matrix $\rho$.
2. **Quantum process tomograhpy**: Given a circuit, reconstruct a description of the quantum channel $\mathcal{E}$ that describes the circuit.

In both cases we rely on the assumption that we have access to a large number of identical copies of the system and so can perform several different measures on it.

We can roughly split the tomography process to three stages:
1. Preperation: Add suitable initialization/measurement devices to the quantum system.
2. Experiment: Obtain measurement data from the quantum system.
3. Tomography: Use the obtained data to reconstruct the system's description.

Steps 1 and 2 are related to the quantum system being studied, whereas step 3 is a classical computation which can be carried out on standard computers.

## State Tomography Overview

Quantum state tomography is a method of reconstructing a description of the quantum state of a system from a set of experiments. While the state of ideal quantum system is described by a state-vector the state of an open quantum system (one that may experiences noise or other errors) is given by a density matrix $\rho$. Quantum state tomography aims to reconstruct this density matrix.

To do this we assume that the state $\rho$ can be reliably prepared by a state-preparation circuit, and that itcan be subjected to several measurements with respect to different operators; this data can be used to reconstruct $\rho$ or a close approximation of it by several different methods.

### Definitions

We denote by $\mathcal{X}$ the state space of a closed (ideal) quantum system. In quantum computing this is typically the tensor product of $N$ 2-dimensional (qubit) systems $\mathcal{X} = \mathbb{C^{2N}}$. Valid quantum states $|\psi\rangle \in \mathcal{X}$ are those with norm-1:$|\langle\psi|\psi\rangle|^2 = 1$.

We denote by $L(\mathcal{X})$ the state space of linear maps on $\mathcal{X}$, ($L: \mathcal{X}\rightarrow\mathcal{X}$). The density-matrix for quantum system with state space $\mathcal{X}$ is a linear map $\rho \in L(\mathcal{X})$ that is also positive-semidefinite, and has trace equal to 1:
1. **Unit trace:** $\text{tr}[\rho] = 1$
2. **Positive-semidefinite:**: For all $|\psi\rangle \in \mathcal{X}$, $\langle\psi|\rho|\psi\rangle \ge 0$. This is denoted by $\rho \ge 0$.

### Example: 1-qubit reconstruction using the Pauli basis

Given the Pauli matrices $
I=\left(\begin{array}{cc}
1 & 0\\
0 & 1
\end{array}\right),
X=\left(\begin{array}{cc}
0 & 1\\
1 & 0
\end{array}\right),
Y=\left(\begin{array}{cc}
0 & -i\\
i & 0
\end{array}\right),
Z=\left(\begin{array}{cc}
1 & 0\\
0 & -1
\end{array}\right)$
_____no_output_____

I = np.array([[1,0],[0,1]])
X = np.array([[0,1],[1,0]])
Y = np.array([[0,-1j],[1j,0]])
Z = np.array([[1,0],[0,-1]])_____no_output_____

It is easy to see they constitute an orthonormal basis for $M_2(\mathbb{C})$ with respect to the Hilbert-Schmidt inner product $\left\langle A,B\right\rangle =\frac{1}{2}\text{tr}\left(B^{\dagger}A\right)$_____no_output_____

def HS_product(A,B):
    return 0.5*np.trace(np.conj(B).T @ A)_____no_output_____

And hence,

$$ \rho =\left\langle \rho,I\right\rangle I+\left\langle \rho,X\right\rangle X+\left\langle \rho,Y\right\rangle Y+\left\langle \rho,Z\right\rangle Z = $$

$$=\frac{\text{tr}\left(\rho\right)I+\text{tr}\left(X\rho\right)X+\text{tr}\left(Y\rho\right)Y+\text{tr}\left(Z\rho\right)Z}{2}$$

The values of $\text{tr}\left(X\rho\right), \text{tr}\left(Y\rho\right), \text{tr}\left(Z\rho\right)$ are the expectation values of $X$, $Y$, $Z$, respectively, and can be approximated by repeated measuring in the $X, Y$ and $Z$ bases. Since $\text{tr}\left(\rho\right)=1$ there is no need for additional measurements for the coefficient of $I$.

### Example: 1-qubit Linear inversion

The above method can be rephrased in more general form. First, any hermitian operator $H$ has a spectral decomposition of the form $H=\sum \lambda_i P_i$ where $\lambda_i$ is an eigenvalue of $H$ and $P_i$ is the projection operator to the corresponding eigenspace. For the hermitian operators $X,Y,Z$ whose eigenvalues are 1 and -1 we can therefore write

* $X = P^X_0-P^X_1$
* $Y = P^Y_0-P^Y_1$
* $Z = P^Z_0-P^Z_1$

Where

$$P^X_0=\frac{1}{2}\left(\begin{array}{cc}
1 & 1\\
1 & 1
\end{array}\right), P^X_1=\frac{1}{2}\left(\begin{array}{cc}
1 & -1\\
-1 & 1
\end{array}\right)$$

$$P^Y_0=\frac{1}{2}\left(\begin{array}{cc}
1 & -i\\
i & 1
\end{array}\right), P^Y_1=\frac{1}{2}\left(\begin{array}{cc}
1 & i\\
-i & 1
\end{array}\right)$$

$$P^Z_0=\left(\begin{array}{cc}
1 & 0\\
0 & 0
\end{array}\right), P^Z_1=\left(\begin{array}{cc}
0 & 0\\
0 & 1
\end{array}\right)$$

In the Ignis code, these matrices are defined in **tomography.fitters.utils.pauli_preparation_matrix**. We give an explicit definition here:_____no_output_____

PX0 = 0.5*np.array([[1, 1], [1, 1]])
PX1 = 0.5*np.array([[1, -1], [-1, 1]])

PY0 = 0.5*np.array([[1, -1j], [1j, 1]])
PY1 = 0.5*np.array([[1, 1j], [-1j, 1]])

PZ0 = np.array([[1, 0], [0, 0]])
PZ1 = np.array([[0, 0], [0, 1]])

projectors = [PX0, PX1, PY0, PY1, PZ0, PZ1]_____no_output_____

By Born's rule, $\text{tr}\left(P_{i}^{X}\rho\right)$ is the probability for the outcome $\left|i\right\rangle$ when measuring in the X-basis, and this probability can be estimated directly using repeated meausrements in the X-basis. The $Y$ and $Z$ bases are handled
similarily.

The computation $\text{tr}\left(P_{i}^{X}\rho\right)$ can be replaced by the scalar product $\vec{P}_i^X \cdot \vec{\rho}$ where $\vec{E}$ denotes the vector obtained from the operator $E$ by flattening its matrix (the result vector consists of the first row, then the second row etc.)

Now we can construct a matrix $$M=\left(\begin{array}{c}
\vec{P}_{0}^{X}\\
\vec{P}_{1}^{X}\\
\vec{P}_{0}^{Y}\\
\vec{P}_{1}^{Y}\\
\vec{P}_{0}^{Z}\\
\vec{P}_{1}^{Z}
\end{array}\right)$$

Such that $$M\vec{\rho}=\vec{p}=\left(\begin{array}{c}
p_{\left|0\right\rangle }^{X}\\
p_{\left|1\right\rangle }^{X}\\
p_{\left|0\right\rangle }^{Y}\\
p_{\left|1\right\rangle }^{Y}\\
p_{\left|0\right\rangle }^{Z}\\
p_{\left|1\right\rangle }^{Z}
\end{array}\right)$$

Is the equation relating the density operator to the observed probabilities._____no_output_____

M = np.array([p.flatten() for p in projectors])
M_____no_output_____

Since $M$ can be computed by knowing the operators used in the tomography, and the vector $\vec{p}$ of probabilities can be estimated using the tomography results, all that remains is to solve the equation $M\vec{\rho}=\vec{p}$ for $\vec{\rho}$. If the rank of $M$ is large enough this can be done by multiplying both sides by $M^\dagger$:

$M^\dagger M\vec{\rho} = M^\dagger \vec{p}$

$\vec{\rho} = (M^\dagger M)^{-1} M^\dagger \vec{p}$

In our example, we obtain the matrix
$$(M^\dagger M)^{-1} M^\dagger = \left(\begin{array}{cccccc}
\frac{1}{6} & \frac{1}{6} & \frac{1}{6} & \frac{1}{6} & \frac{4}{6} & -\frac{2}{6}\\
\frac{1}{2} & -\frac{1}{2} & \frac{i}{2} & -\frac{i}{2} & 0 & 0\\
\frac{1}{2} & -\frac{1}{2} & -\frac{i}{2} & \frac{i}{2} & 0 & 0\\
\frac{1}{6} & \frac{1}{6} & \frac{1}{6} & \frac{1}{6} & -\frac{2}{6} & \frac{4}{6}
\end{array}\right)$$_____no_output_____

M_dg = np.conj(M).T
linear_inversion_matrix = np.linalg.inv(M_dg @ M) @ M_dg_____no_output_____

Multiplication by the linear inversion matrix performs the reconstruction stage described earlier to obtain the density operator.

### Example: 2-qubit Linear inversion

For multiple qubit systems the technique of linear inversion remains the same. The projector operators are tensor products of 1-qubit projectors: $6^n$ projectors in total, since we measure according to $3^n$ operators (tensor products of $X,Y,Z$) and each operator has two projectors._____no_output_____

projectors_2 = [np.kron(p1, p2) for (p1, p2) in itertools.product(projectors, repeat = 2)]
M_2 = np.array([p.flatten() for p in projectors_2])
M_dg_2 = np.conj(M_2).T
linear_inversion_matrix_2 = np.linalg.inv(M_dg_2 @ M_2) @ M_dg_2_____no_output_____

We will now attempt to reconstruct the Bell state $\frac{\left|00\right\rangle +\left|11\right\rangle }{\sqrt{2}}$ from simulated tomography results. First, we prepare a quantum circuit which generates this bell state from the input $\left|00\right\rangle$._____no_output_____

q2 = QuantumRegister(2)
bell = QuantumCircuit(q2)
bell.h(q2[0])
bell.cx(q2[0], q2[1])
bell.qasm()_____no_output_____

We now use Ignis' **state_tomography_circuits** procedure which generates the $3^n$ circuits obtained by adding to the bell circuit a measurement according to each of our measurement operators (Pauli by default). Then we execute on a standard simulator._____no_output_____

qst_bell = tomo.state_tomography_circuits(bell, q2)
job = qiskit.execute(qst_bell, Aer.get_backend('qasm_simulator'), shots=5000)_____no_output_____

Now we load the data into the **StateTomographyFitter** which takes results data and can fit to a density matrix_____no_output_____

statefit = tomo.StateTomographyFitter(job.result(), qst_bell)_____no_output_____

Here is the data we loaded into the **StateTomographyFitter** _____no_output_____

statefit.data_____no_output_____

Now use a private function **\_fitter_data** to explicitly extract the probability vector $\vec{p}$ and projector matrix $M$ that satisfy $M\vec{\rho} = \vec{p}$. For typical usage we don't need to expose this data. _____no_output_____

p, M, weights = statefit._fitter_data(True, 0.5)_____no_output_____

Now we use the linear inversion technique to reconstructo $\vec{\rho}$. Since we usually represent density matrices as matrices and not vectors, we use Numpy's **reshape** function to convert $\vec{\rho}$ into $\rho$._____no_output_____

M_dg = np.conj(M).T
linear_inversion_matrix = np.linalg.inv(M_dg @ M) @ M_dg
rho_bell = linear_inversion_matrix @ p
rho_bell = np.reshape(rho_bell, (4, 4))
print(rho_bell)[[ 4.99266667e-01+0.00000000e+00j -2.70000000e-03-6.66666667e-05j
   1.30000000e-03-4.73333333e-03j  5.00000000e-01-3.60000000e-03j]
 [-2.70000000e-03+6.66666667e-05j -6.66666667e-05+0.00000000e+00j
   1.38777878e-17-2.00000000e-04j -1.70000000e-03+5.06666667e-03j]
 [ 1.30000000e-03+4.73333333e-03j  1.38777878e-17+2.00000000e-04j
   6.66666667e-05+0.00000000e+00j -3.00000000e-04-4.66666667e-04j]
 [ 5.00000000e-01+3.60000000e-03j -1.70000000e-03-5.06666667e-03j
  -3.00000000e-04+4.66666667e-04j  5.00733333e-01+0.00000000e+00j]]

To check the quality of our solution, we compute the fidelity between the real quantum state (obtained via simulation by a simulator that can return state vectors) and our calculated $\rho$. The closer the fidelity to 1, the better._____no_output_____

job = qiskit.execute(bell, Aer.get_backend('statevector_simulator'), shots=1)
psi_bell = job.result().get_statevector(bell)
F_bell = state_fidelity(psi_bell, rho_bell, validate=False)
print('Fit Fidelity linear inversion =', F_bell)Fit Fidelity linear inversion = 0.9999999999999999

## Maximum Likelihood

Linear inversion works perfectly on accurate data, but tomography data is never fully accurate. Two obvious obstacles are
1. Since the number of measurements is limited, we do not obtain the probability vector $\vec{p}$ but an approximation.
2. The measurement process might be noisy.

This may result in non-accurate or even self-contradicting $\vec{p}$, and the result of linear inversion might not be a density function at all (e.g. not nonnegative, or trace different than 1).

Since we want to solve the linear problem $A\vec{x}=\vec{p}$ for $x$, we can turn it into an optimization problem by attempting to minimize $\|A\vec{x}-\vec{p}\|_2$ while subjecting $x$ to additional constraints to ensure it is indeed a density matrix. This is done by **state_cvx_fit**.

Another approach is to solve this optimization problem with no further constraints. The result might not be a density operator, i.e. positive semidefinite with trace 1; in this case the algorithm first rescales in order to obtain a density operator. This is done using **state_mle_fit**._____no_output_____

rho_cvx_bell = statefit.fit(method='cvx')
F_bell = state_fidelity(psi_bell, rho_cvx_bell, validate=False)
print('Fit Fidelity CVX fit =', F_bell)

rho_mle_bell = statefit.fit(method='lstsq')
F_bell = state_fidelity(psi_bell, rho_mle_bell, validate=False)
print('Fit Fidelity MLE fit =', F_bell)Fit Fidelity CVX fit = 0.9999582373983514
Fit Fidelity MLE fit = 0.9974494862892956

## Process Tomography Overview

A quantum process, or quantum channel, describes the fixed-time evolution of a density matrix $\rho^\prime = \mathcal{E}(\rho)$. While ideal quantum processes are described by unitary matrix evolution, general quantum process can include the effect of errors in the ideal evolution such as those due to unwanted interaction with an environment system, systematic errors in gate operations, or other error processes.

Quantum process tomography aims to find a description of $\mathcal{E}$ from a set of experiments. Like state tomography, we measure with respect to several different bases. Process tomography also uses several different initial states, as opposed to state tomography where only $\left|0^n\right\rangle$ is used. It can be thought of as performing state-tomography on tomographically complete set of a complete set of input states for the system.

### Definitions

Let $D(\mathcal{X})$ be the set of density-matrices on a quantum system. Quantum channels are maps $\mathcal{E}:L(\mathcal{X})\rightarrow L(\mathcal{X})$ that are *completely positive-trace preserving* (CPTP):

1. **CP:** $\mathcal{E}$ is *completely-positive* if and only if $(\mathcal{I}\otimes\mathcal{E})(\rho)\ge 0$ for all $\rho \ge 0$, where $\mathcal{I}$ is an identity channel on an ancilla system.
2. **TP:** $\mathcal{E}$ is *trace-preserving* if and only if $\text{Tr}[\mathcal{E}(\rho)] = \text{Tr}[\rho]$ for all $\rho$.

Together these two requirements ensure that the output of the quantum process will always be a valid density matrix.

There are numerous representations for a quantum channel $\mathcal{E}$. The simplest way to perform process tomogrpahy is to reconstruct a description of $\mathcal{E}$ known as the *Choi-matrix* or process-matrix. This is because it is analogous to a density matrix $\rho$, and hence the reconstruction is similar to quantum state tomography. Another common representation is the *Kraus* representation which we will also describe and show how it is related to the Choi-matrix. Another common representation is the Pauli Tranfer Matrix (PTM), which is useful for evolution since compositing channels becomes matrix multiplication. For more details about different representations of quantum channels and their relationships see [this paper](https://arxiv.org/abs/1111.6950).

#### Choi-matrix representation

The Choi-matrix description of a quantum channel $\mathcal{E}$ is a unique bipartite matrix $\Lambda_{\mathcal{E}}\in L(\mathcal{X}\otimes\mathcal{X})$ given by the *Choi-Jamiolkowski isomorphism*.
Given a basis $\left\{\left|0\right\rangle, \left|1\right\rangle,\dots, \left|n-1\right\rangle\right\}$ of $\mathcal{X}$, the Choi-matrix $\Lambda_{\mathcal{E}}$ is defined as:

$$\Lambda_\mathcal{E} = \sum_{i,j=0}^{n-1} \left|i\right\rangle\left\langle j\right| \otimes \mathcal{E}(\left|i\right\rangle\left\langle j\right|)$$

Evolution of a quantum state can be described in terms of this matrix by
$$\mathcal{E}(\rho) = \text{Tr}_1[\Lambda_{\mathcal{E}}(\rho^T\otimes\mathbb{1})]$$

where $\text{Tr}_1$ denotes **partial trace**: $\text{Tr}_1[A\otimes B] = \text{Tr}[A]\,B$ and $\mathbb{1}$ is the identity operator on $X$.

In terms of the Choi-matrix, the requirements that $\mathcal{E}$ is CPTP are:

1. **CP:** $\mathcal{E}$ is *completely-positive* if and only if the Choi-matrix is positive-semidefinite ( $\Lambda_{\mathcal{E}} \ge 0$).
2. **TP:** $\mathcal{E}$ is *trace-preserving* if and only if $\text{Tr}_2[\Lambda_{\mathcal{E}}] = \mathbb{id}$.

where $\text{Tr}_2$ denotes **partial trace**: $\text{Tr}_2[A\otimes B] =\text{Tr}[B]\,A$.

#### Kraus representation

Another commonly used description of a quantum channel $\mathcal{E}$ is via **Kraus operators**. A set $\left\{K_1, K_2, \dots, K_t\right\}$ such that $K_i \in L(X)$. In terms of this representation the evolution of a state is given by

$$\mathcal{E}(\rho) = \sum_{i=1}^k K_i\rho K_i^\dagger$$

In terms of the Kraus operators the requirement that $\mathcal{E}$ is CPTP is equivalent to $\sum_{i=1}^k K_i^\dagger K_i = \mathbb{1}$.

This is a common description of quantum processes. In case of a unitary operator $U$, the set $\left\{U\right\}$ is a Kraus operator representation of it,
$$\mathcal{E}(\rho) = U \rho U^{\dagger}$$
Measurements and noises are also commonly given via Kraus operators. Note however that the Kraus representation of a quantum process is not unique.

The Choi-matrix may be constructed from Kraus operators via
$$\Lambda_\mathcal{E} = \sum_{i,j=0}^{n-1}\sum_{l=1}^k \left|i\right\rangle\left\langle j\right| \otimes K_l\left|i\right\rangle\left\langle j\right|K_l^\dagger = \sum_{l=1}^k |K_l\rangle\!\rangle\!\langle\!\langle K_l|$$

where $|A\rangle\!\rangle$ denotes a vectorized matrix, which is a column-vector obtained by stacking the columns of $A$.

To construct a set of Kraus operators from a Choi-matrix we may use the spectral-decomposition of $\Lambda_{\mathcal{E}}$ and define $ |K_l\rangle\!\rangle\ = \sqrt{\lambda_l}|v_l\rangle$, where ${\lambda_l} \ge 0$ and $|v_l\rangle \in \mathcal{X}\otimes\mathcal{X}$ are the eigenvalues and eigenvectors respectively of $\Lambda_{\mathcal{E}} \ge 0$.

#### Pauli-Transfer-Matrix (PTM) representation

Another description of a quantum channel $\mathcal{E}$ is via **PTM**. The elements of the PTM are given as

$$R_{ij} = \frac{1}{d}\text{Tr}[\sigma_i\mathcal{E}(\sigma_j)] $$

where $\sigma$ are the Pauli matrices ($4^n$ possible matrices). The advantage of this method is that composing channels,

$$\mathcal{E}(\rho) = \mathcal{E_1}(\mathcal{E_2}(\rho)) $$

is

$$R = R_1 \cdot R_2 $$

### Process tomography with the Choi matrix

The CP condition on the Choi-matrix ($\Lambda_{\mathcal{E}})\ge 0$ is equivalent to the state-tomography condition that a density matrix is positive-semidefinite. The only difference is that the normalization of the Choi-matrix is not trace-1, but trace given by the dimension of $\mathcal{X}$ ($\text{Tr}[\Lambda_{\mathcal{E}}] = d$). This allows us to use the same fitters from state-tomography to fit the Choi-matrix adjusting for this difference in normalization. *The key difference between state tomography is the TP condition which is an additional constraint we must add to the tomography fitters if we want to ensure the fitted channel is CPTP.*

To see this relationship explicity consider preparing the system in an initial state $\rho$ and measuring. The probability of measurement for a projector $\Pi$ is given by

$$p_{ij} = \text{Tr}[\Lambda_{\mathcal{E}}(\rho^T\otimes\Pi_j)]$$

Using the above equation we can see that the act of measuring the outcome of $\mathcal{E}$ on some initial $\rho$ with some projector $\Pi$ can be seen as the act of measuring $\Lambda_\mathcal{E}$ (when it is considered as a **state** in the space $L(\mathcal{X}\otimes\mathcal{X})$) with a measurement operator $\overline{\rho}\otimes \Pi$, where we have used that $\rho^T=\overline{\rho}$ since $\rho$ is Hermitian.

This gives rise to the following algorithm for process tomography:

1. Obtain a set of initial states $\left\{\rho_1, \dots \rho_{k}\right\}$ and a set of projectors $\left\{P_1, \dots P_{t}\right\}$ such that the set of all projectos $\Pi_{i,j} = \overline{\rho_i}\otimes P_j$ is tomographically complete for $L(\mathcal{X}\otimes\mathcal{X})$.
2. Obtain measurements of $\Lambda_\mathcal{E}$ with $\Pi_{i,j}$ by initializing a system to $\rho_{i}$, applying $\mathcal{E}$ (e.g. via a simulator or the quantum computer to check) and measuring via $P_j$.
3. Pass the results and the description of $\Pi_{i,j}$ to an algorithm for state tomography and obtain $\Lambda_\mathcal{E}$. *Note that to ensure the fitted state is also TP requires adding the additional constraint to the fitter (if supported) that $\text{Tr}_2[\Lambda_{\mathcal{E}}]=\mathbb{1}$.*_____no_output_____


# Notebook from applejwjcat/dispatch
Path: docs/overview/IAC/HandsOn/2-Heat-Diffusion/Experiment.ipynb

# Heat Diffusion_____no_output_____### Update the reposotory_____no_output_____

### Finish the new ``experiments/heat_diffusion``_____no_output_____Look at new directory ``experiment/heat_diffusion``; it is in principle all that is needed to solve the heat diffusion equation in parallel, at any size._____no_output_____

Take a look at ``experiment_mod.f90``; it has initial conditions, and boundary conditions.  All it lacks is the "flesh" in ``SUBROUTINE update``, which is where the solution is to be updated.   This is such a simple case that the "solver" can be implemented directly in the ``update`` subroutine, by adding the statements that compute the diffusion operator.  Use the stencil that in Python would be expressed as_____no_output_____

def laplace(f):
    s=f.shape
    out=np.zeros(s)
    li=np.array([2,2,2])
    ui=s-li-1
    print(li,ui)
    """ For illustration purposes this is written in loop form """
    for i2 in range(li[2],ui[2]+1):
        for i1 in range(li[1],ui[1]+1):
                for i0 in range(li[0],ui[0]+1):
                    out[i0,i1,i2] = f[i0-1,i1  ,i2  ]+f[i0+1,i1  ,i2  ] \
                                  + f[i0  ,i1-1,i2  ]+f[i0  ,i1+1,i2  ] \
                                  + f[i0  ,i1  ,i2-1]+f[i0  ,i1  ,i2+1] \
                                - 6*f[i0,  i1,  i2  ]
    return out_____no_output_____import numpy as np
f=np.zeros((20,20,20))_____no_output_____out=laplace(f)[2 2 2] [17 17 17]

The indices ``li`` and ``ui`` are available in DISPATCH as ``self%li`` and ``self%ui``.  Note that in Python ``range(li,ui+1)`` runs from ``li`` to ``ui`` (inclusive)_____no_output_____### Compile and run the experiment_____no_output_____

When it seems to work ..
### Access and investigate the output
.. using the methods you ppicked up in the ``Star Formation`` case!_____no_output_____

sn=dispatch.snapshot(2)
sn.time_____no_output_____