# 4. Quantum computation

Quantum computation is a new way of doing computation which differs from the classical way. Classical computations can be implemented in several ways, the most successful one today is the circuit model of computation. This is how elements in modern computers are designed. In the circuit model, a computation is made by taking a string of bits as inputs, doing certain operations on them and giving a new string of bits as output. In the current paradigm, these operations are logical operations that follow Boole's logic. It was proved that one needs to be able to carry out a limited set of operation (namely "NOT gate", and "AND gate") in order to implement any operation (addition, multiplication, division, ... ) by a combination of operation from this set. This fundamental set of gates is called an "elementary set of gates" and is all that is required to be able to do any computation. Similarly to classical computation, quantum computation can be done using the circuit model of computation. In this case, bits are replaced by qubits and logic gates must be substituted with quantum gates which can operate on qubits while keeping intact their special quantum properties. Quantum circuits must be reversible due to the reversibility inherent in the laws of quantum mechanics.
A reversible circuit allows you to run the computation backwards and retrieve the inputs given the outputs. Classical computation can also be implemented using reversible gates but there are disadvantages with regards to the circuit size and complexity. Thus, modern computer are built with "irreversible" logic (which means it's impossible to run the computation backwards, see truth table of the "AND" gate for example) and this is the reason why the generate heat! In order to have reversible quantum gates, one must implement these gates using, what is called a "unitary operation", that is an operation which preserve the sum of the probabilities of seeing each of the measurable values of the qubits. Although, the probability of seeing any single outcome can change, their sum will always add up to one.

## 4.1 The qubit

In quantum computation the objects of the computation are quantum objects called qubits. Similarly to bits, when qubits are measured they can only take two values: $\lvert 0 \rangle$, $\lvert 1 \rangle $.
Where the brackets around the number points to the fact that these are quantum objects (see Dirac's notation).
In linear algebra representation, the state of a qubit is a vector in a two-dimensional Hilbert space. One of the possible basis for this space is the so-called computational basis which is formed by the eigenvector of the Pauli Z matrix (more details below), the $\lvert 0 \rangle$ and $\lvert 1 \rangle $ states. In matrix form, they can be written as

$$
 \lvert 0 \rangle =
        \begin{pmatrix}
        1 \\
        0
        \end{pmatrix}
$$

$$
 \lvert 1 \rangle =
        \begin{pmatrix}
        0 \\
        1
        \end{pmatrix}
$$

A generic vector $\lvert \psi \rangle $ in the Hilbert space can then be constructed as a linear combination of the basis vectors

$$
 \lvert \psi \rangle =  \alpha \lvert 0 \rangle + \beta \lvert 1 \rangle =
        \begin{pmatrix}
        \alpha \\
        \beta
        \end{pmatrix}
$$

where $\alpha$ and $\beta$ are two complex numbers.

### Superposition

Differently from the regular bits stored in a computer, which can either take the value of $"0"$ or $"1"$, during a computation qubits can be in a state $\lvert \psi \rangle$ which is a superposition of $\lvert 0 \rangle$ and $\lvert 1 \rangle$:

\begin{equation}
\lvert \psi \rangle = \alpha \lvert 0 \rangle + \beta \lvert 1 \rangle ,
\end{equation}

where $\alpha$ and $\beta$ are related to the probability of obtaining the corresponding outcome $\lvert 0 \rangle$ or $\lvert 1 \rangle$ when the qubit is measured to learn its value.

\begin{eqnarray}
\text{P}(\text{qubit state} = 0) = \lvert \alpha \rvert^{2} \\ \notag
\text{P}(\text{qubit state} = 1) = \lvert \beta \rvert^{2}
\end{eqnarray}

Which means that the value of the qubit is not determined until it is measured. This is a counter-intuitive property of quantum mechanical objects. A qubit in a superposition of different states will behave as if it possess properties of all the states in the superposition. However, when measured, the qubit will be in one of the states of the superposition, with a probability given by the modulo square of the coefficient of the corresponding state.

### Multi-qubit state

In quantum computation, one is generally interested in doing operations on a qubit register which contains a collection of qubits. To denote the state of an $n$ qubit register, one of the following equivalent notations is used:

\begin{equation}
\lvert 0 \rangle_1 \otimes  \lvert 1 \rangle_2 \otimes ...  \otimes \lvert 0 \rangle_n \equiv \lvert 0, 1, ..., 0 \rangle \equiv \lvert 01...0 \rangle
\end{equation}

Where each of the zeros or ones correspond to the state of one of the qubit in the register and the index counts the qubit's number. The linear algebra meaning of all these notations (although less and less explicit going from left to right) is that the Hilbert space containing the state of the multi-qubit system is a tensor product $\otimes$ of the Hilbert spaces of the single qubits and the state of the system is a Dirac ket vector in this tensor product space. That is, the state of the system is a tensor product of the state of the single qubits. So, if the Hilbert space of one qubit has dimension $2$, the Hilbert space of an $n$-qubit register has dimension $2^n$

As an example, let us consider the case of $n=2$ qubits. The matrix form of the basis of the 4-dimensional Hilbert space is given by the tensor product of the basis vectors of each of the single qubit spaces.
From the definition of tensor product between vectors given in Chapter 2, we have

$$
 \lvert 00 \rangle =
        \begin{pmatrix}
        1 \\
        0
        \end{pmatrix} \otimes
        \begin{pmatrix}
        1 \\
        0
        \end{pmatrix} =
        \begin{pmatrix}
        1 \cdot 1 \\
        1 \cdot 0 \\
        0 \cdot 1 \\
        0 \cdot 0
        \end{pmatrix} =
        \begin{pmatrix}
        1 \\
        0 \\
        0 \\
        0
        \end{pmatrix}
$$

$$
 \lvert 01 \rangle =
        \begin{pmatrix}
        1 \\
        0
        \end{pmatrix} \otimes
        \begin{pmatrix}
        0 \\
        1
        \end{pmatrix} =
        \begin{pmatrix}
        1 \cdot 0 \\
        1 \cdot 1 \\
        0 \cdot 0 \\
        0 \cdot 1
        \end{pmatrix} =
        \begin{pmatrix}
        0 \\
        1 \\
        0 \\
        0
        \end{pmatrix}
$$

$$
 \lvert 10 \rangle =
        \begin{pmatrix}
        0 \\
        1
        \end{pmatrix} \otimes
        \begin{pmatrix}
        1 \\
        0
        \end{pmatrix} =
        \begin{pmatrix}
        0 \cdot 1 \\
        0 \cdot 0 \\
        1 \cdot 1 \\
        1 \cdot 0
        \end{pmatrix} =
        \begin{pmatrix}
        0 \\
        0 \\
        1 \\
        0
        \end{pmatrix}
$$

$$
 \lvert 11 \rangle =
        \begin{pmatrix}
        0 \\
        1
        \end{pmatrix} \otimes
        \begin{pmatrix}
        0 \\
        1
        \end{pmatrix} =
        \begin{pmatrix}
        0 \cdot 0 \\
        0 \cdot 1 \\
        1 \cdot 0 \\
        1 \cdot 1
        \end{pmatrix} =
        \begin{pmatrix}
        0 \\
        0 \\
        0 \\
        1
        \end{pmatrix}
$$

The extension to $n$ qubits is then straightforward, although tedious.

### Entanglement

Another interesting property of qubits, which departs from the classical world, is that they can be entangled. If qubits are entangled with each other, the value taken by each of them is strictly related to the value taken by the other. The correlations between the values of entangled qubits is so strong that it cannot be described by classical probability theory. This is one of the most peculiar features of quantum mechanics.
In a way, entangled qubits lose their identity as individual objects, as their properties now depend on the properties of the other qubits with which they are entangled. It's not possible to separate an entangled system in independent parts and it must be treated as a new unit (in quantum mechanical terms, the state of the whole system is not separable, it cannot be factorized as product state of the state of individual systems).
To see the difference between an entangled state and a non-entangle state, consider the following two possibilities for a two-qubit state:
<ol>
    <li> $\frac{1}{\sqrt{2}} \left( \lvert 0 \rangle_1 \otimes \lvert 0 \rangle_2 \right) +  \frac{1}{\sqrt{2}} \left( \lvert 0 \rangle_1 \otimes \lvert 1 \rangle_2 \right) $ </li>

    <li> $\frac{1}{2} \left( \lvert 0 \rangle_1 \otimes \lvert 0 \rangle_2 \right) + \frac{1}{2} \left( \lvert 1 \rangle_1 \otimes \lvert 1 \rangle_2 \right)$ </li>
</ol>

The state shown in 1. can be manipulated so that it will look like the tensor product of the state of the first qubit and the state of the second qubit. In fact, we can rewrite 1. as: $ \lvert 0 \rangle_1 \otimes \frac{1}{\sqrt{2}} \left( \lvert 0 \rangle_2 + \lvert 1 \rangle_2 \right) $. Thus, we can say that the first qubit is in the state $\lvert 0 \rangle_1$ and the second qubit is in an equal superposition of $\lvert 0 \rangle_2$ and $\lvert 1 \rangle_2$.
The same procedure cannot be done on the two-qubit state shown in 2. which cannot be manipulated so that the two-qubit state looks like a tensor product of the state of the first qubit and the state of the second qubit. Therefore, one cannot say anything about the state of a single qubit in state 2. but one has always to talk about the joint state of the two qubits. If one of them has value $"0"$ the other one will have value $"0"$ as well, and if one qubit has value $"1"$, the other qubit will have value $"1"$ too. This purely quantum correlation between the states of a system is what is called quantum entanglement.

## 4.2 Quantum gates

Quantum gates implement operations on qubits by keeping intact their quantum properties. They correspond to operators acting on the qubit register. Furthermore, they allow some of those properties to arise, for example by putting qubits in a superposition of states or entangling them with each other.
It was shown that, similarly to the classical case, it is only really necessary to be able to perform a finite set of quantum gates on the qubits to implement any possible operation on them by combining these particular quantum gates. The minimum set of quantum gates needed for computation is then called a "universal set of quantum gates".

Let's see some quantum gates:

### 4.2.1 One-qubit gates

These gates act on one of the qubits in the qubit register, leaving all other qubits unaffected.

#### Identity gate
The identity gate I leaves the state of a qubit unchanged. In matrix form this is just the identity matrix in a two-dimensional vector space

$$\text{1. Identity gate.}$$

$$
    \hat{I}  =
        \begin{pmatrix}
        1 & 0\\
        0 & 1
        \end{pmatrix}
$$

and its action on the state of a qubit is

$$
    \hat{I} \lvert 0 \rangle =
        \begin{pmatrix}
        1 & 0\\
        0 & 1
        \end{pmatrix}
        \begin{pmatrix}
        1 \\
        0
        \end{pmatrix} =
        \begin{pmatrix}
        1 \\
        0
        \end{pmatrix} = \lvert 0 \rangle
$$

$$
    \hat{I} \lvert 1 \rangle =
        \begin{pmatrix}
        1 & 0\\
        0 & 1
        \end{pmatrix}
        \begin{pmatrix}
        0 \\
        1
        \end{pmatrix} =
        \begin{pmatrix}
        0 \\
        1
        \end{pmatrix} = \lvert 1 \rangle
$$

#### Pauli X gate
The X gate flips the state of a qubit from $\lvert 0 \rangle \rightarrow \lvert 1 \rangle $ and $\lvert 1 \rangle \rightarrow \lvert 0 \rangle $. It is the quantum analog of the NOT gate and it is sometimes referred to as the "bit-flip gate". When considering the Bloch sphere representation of a qubit, the X gate correspond to a $\pi$ rotation around the y-axis.

$$\text{2. X gate.}$$

In matrix representation, the X gate is

$$
    \hat{X}  =
        \begin{pmatrix}
        0 & 1\\
        1 & 0
        \end{pmatrix}
$$

Its action on a qubit is

$$
    \hat{X} \lvert 0 \rangle =
        \begin{pmatrix}
        0 & 1\\
        1 & 0
        \end{pmatrix}
        \begin{pmatrix}
        1 \\
        0
        \end{pmatrix} =
        \begin{pmatrix}
        0 \\
        1
        \end{pmatrix} = \lvert 1 \rangle
$$

$$
    \hat{X} \lvert 1 \rangle =
        \begin{pmatrix}
        0 & 1\\
        1 & 0
        \end{pmatrix}
        \begin{pmatrix}
        0 \\
        1
        \end{pmatrix} =
        \begin{pmatrix}
        1 \\
        0
        \end{pmatrix} = \lvert 0 \rangle
$$

#### Pauli Z gate
The Z gate flips the phase of a qubit if it is in the $\lvert 1 \rangle$ state. This correspond to a $\pi$ rotation around the z-axis.

$$\text{3. Z gate.}$$

In matrix representation, the Z gate is

$$
\hat{Z}  =
\begin{pmatrix}
1 & 0\\
0 & -1
\end{pmatrix}
$$

The effect on the state of a qubit is

$$
    \hat{Z} \lvert 0 \rangle =
        \begin{pmatrix}
        1 & 0\\
        0 & -1
        \end{pmatrix}
        \begin{pmatrix}
        1 \\
        0
        \end{pmatrix} =
        \begin{pmatrix}
        1 \\
        0
        \end{pmatrix} = \lvert 0 \rangle
$$

$$
    \hat{Z} \lvert 1 \rangle =
        \begin{pmatrix}
        1 & 0\\
        0 & -1
        \end{pmatrix}
        \begin{pmatrix}
        0 \\
        1
        \end{pmatrix} =
        \begin{pmatrix}
        0 \\
        -1
        \end{pmatrix} = - \lvert 1 \rangle
$$

#### Pauli Y gate
The Y gate flips the state of a qubit and its phase and is sometimes called the " bit- and phase-flip gate".

$$\text{4. Y gate.}$$

The definition of the Y gate in matrix form is

$$
\hat{Y}  =
\begin{pmatrix}
0 & -i\\
i & 0
\end{pmatrix}
$$

The effect of the Y gate on the state of a qubit is

$$
    \hat{Y} \lvert 0 \rangle =
        \begin{pmatrix}
        0 & -i\\
        i & 0
        \end{pmatrix}
        \begin{pmatrix}
        1 \\
        0
        \end{pmatrix} =
        \begin{pmatrix}
        0 \\
        i
        \end{pmatrix} = i \lvert 1 \rangle
$$

$$
    \hat{Y} \lvert 1 \rangle =
        \begin{pmatrix}
        0 & -i\\
        i & 0
        \end{pmatrix}
        \begin{pmatrix}
        0 \\
        1
        \end{pmatrix} =
        \begin{pmatrix}
        -i \\
        0
        \end{pmatrix} = -i \lvert 0 \rangle
$$

As the name of the Y gate suggest, the Y gate itself is not an independent gate but it is the combination of the X and Z gate. In particular, let us consider

$$
    i\hat{X}\hat{Z}  =
        i \begin{pmatrix}
        0 & 1\\
        1 & 0
        \end{pmatrix}
        \begin{pmatrix}
        1 & 0\\
        0 & -1
        \end{pmatrix} =
        i \begin{pmatrix}
        0 & -1\\
        1 & 0
        \end{pmatrix} =
        \begin{pmatrix}
        0 & -i\\
        i & 0
        \end{pmatrix} = Y
$$

#### Hadamard gate
The Hadamard gate H puts a qubit in an equal superposition of $\lvert 0 \rangle$ and $\lvert 1 \rangle$.

$$\text{5. Hadamard gate.}$$

Its matrix representation is

$$
\hat{H}  =
\frac{1}{\sqrt{2}}
\begin{pmatrix}
1 & 1\\
1 & -1
\end{pmatrix}
$$

Acting with the Hadamard gate on a qubit gives

$$
    \hat{H} \lvert 0 \rangle =
        \frac{1}{\sqrt{2}}
        \begin{pmatrix}
        1 & 1\\
        1 & -1
        \end{pmatrix}
        \begin{pmatrix}
        1 \\
        0
        \end{pmatrix} =
        \frac{1}{\sqrt{2}} \begin{pmatrix}
        1 \\
        1
        \end{pmatrix} = \frac{1}{\sqrt{2}} \left( \lvert 0 \rangle + \lvert 1 \rangle \right)
$$

$$
    \hat{H} \lvert 1 \rangle =
        \frac{1}{\sqrt{2}}
        \begin{pmatrix}
        1 & 1\\
        1 & -1
        \end{pmatrix}
        \begin{pmatrix}
        0 \\
        1
        \end{pmatrix} =
        \frac{1}{\sqrt{2}} \begin{pmatrix}
        1 \\
        -1
        \end{pmatrix} = \frac{1}{\sqrt{2}} \left( \lvert 0 \rangle - \lvert 1 \rangle \right)
$$

#### Rotations
Rotations around an axis ($R_x, R_y, R_z$): Rotates qubit state $\lvert \psi \rangle = \alpha \lvert 0 \rangle + \beta \lvert 1 \rangle $, by changing the coefficients $\alpha, \beta$ in a way that depends on the angle of rotation.

$$\text{6. Rotation gate around the $x$, $y$ and $z$ axis.}$$

In matrix form

$$
\hat{R}_x(\theta) =
\begin{pmatrix}
\cos(\theta/2) & -i\sin(\theta/2)\\
-i\sin(\theta/2) & \cos(\theta/2)
\end{pmatrix}
$$

$$
\hat{R}_y(\theta) =
\begin{pmatrix}
\cos(\theta/2) & \sin(\theta/2)\\
\sin(\theta/2) & \cos(\theta/2)
\end{pmatrix}
$$

$$
\hat{R}_z(\theta) =
\begin{pmatrix}
1 & 0 \\
0 & e^{i \theta}
\end{pmatrix}
$$

Their action is

 $$ \hat{R}_x(\theta) \lvert \psi \rangle =
       \begin{pmatrix}
        \cos(\theta/2) & -i\sin(\theta/2)\\
        -i\sin(\theta/2) & \cos(\theta/2)
        \end{pmatrix}
        \begin{pmatrix}
        \alpha \\
        \beta
        \end{pmatrix} =
        \begin{pmatrix}
        \cos(\theta/2) \alpha -i\sin(\theta/2) \beta\\
        -i\sin(\theta/2) \alpha + \cos(\theta/2) \beta
        \end{pmatrix} = \left( \cos(\theta/2) \alpha -i\sin(\theta/2) \right) \lvert 0 \rangle + \left( -i\sin(\theta/2) \alpha + \cos(\theta/2) \beta \right)  \lvert 1 \rangle$$

$$ \hat{R}_y(\theta) \lvert \psi \rangle =
       \begin{pmatrix}
        \cos(\theta/2) & \sin(\theta/2)\\
        \sin(\theta/2) & \cos(\theta/2)
        \end{pmatrix}
        \begin{pmatrix}
        \alpha \\
        \beta
        \end{pmatrix} =
        \begin{pmatrix}
        \cos(\theta/2) \alpha + \sin(\theta/2) \beta\\
        \sin(\theta/2) \alpha + \cos(\theta/2) \beta
        \end{pmatrix} = \left( \cos(\theta/2) \alpha + \sin(\theta/2) \right) \lvert 0 \rangle + \left( \sin(\theta/2) \alpha + \cos(\theta/2) \beta \right)  \lvert 1 \rangle$$

$$ \hat{R}_z(\theta) \lvert \psi \rangle =
       \begin{pmatrix}
        1 & 0 \\
        0 & e^{i \theta}
        \end{pmatrix}
        \begin{pmatrix}
        \alpha \\
        \beta
        \end{pmatrix} =
        \begin{pmatrix}
         \alpha \\
         e^{i \theta} \beta
        \end{pmatrix} = \alpha \lvert 0 \rangle +  e^{i \theta} \beta   \lvert 1 \rangle $$

### 4.2.2 Multi-qubit gates

$$\text{7. Two single-qubit gates. $X$ on the first qubit and $I$ on the second qubit.}$$

If we are dealing with a qubit register that contains more than a single qubit, the operators on the whole register can be obtained by taking the tensor product of the operators acting on each qubit. To avoid lengthy calculations, we work out a few examples for a two-qubit register. We have shown above the form of the basis vector of a two-qubit register.
As an example, consider the matrix representation of the X gate on the first qubit is

$$
\hat{X} \otimes \hat{I} =
\begin{pmatrix}
0 & 1 \\
1 & 0
\end{pmatrix} \otimes
\begin{pmatrix}
1 & 0 \\
0 & 1
\end{pmatrix} =
\begin{pmatrix}
0 \cdot 1 & 0 \cdot 0 & 1 \cdot 1 & 1 \cdot 0 \\
0 \cdot 0 & 0 \cdot 1 & 1 \cdot 0 & 1 \cdot 1 \\
1 \cdot 1 & 1 \cdot 0 & 0 \cdot 1 & 0 \cdot 0 \\
1 \cdot 0 & 1 \cdot 1 & 0 \cdot 0 & 0 \cdot 1
\end{pmatrix} =
\begin{pmatrix}
0 & 0 & 1 & 0\\
0 & 0 & 0 & 1\\
1 & 0 & 0 & 0\\
0 & 1 & 0 & 0
\end{pmatrix}
$$

The action of this operator on a two-qubit register will be

$$
\left( \hat{X} \otimes \hat{I} \right) \lvert 00 \rangle =
\begin{pmatrix}
0 & 0 & 1 & 0\\
0 & 0 & 0 & 1\\
1 & 0 & 0 & 0\\
0 & 1 & 0 & 0
\end{pmatrix}
\begin{pmatrix}
1 \\
0\\
0\\
0
\end{pmatrix} =
\begin{pmatrix}
0 \\
0\\
1\\
0
\end{pmatrix} =
\lvert 10 \rangle
$$

Exactly as expected. If we now want to combine different type of single qubit gates, acting on the two-qubit register, we only need to calculate the tensor product between these operators to find what is their matrix representation.

### 4.2.3 Two-qubit gates

#### Control-NOT gate

The most reknown two-qubit gate is the control-not gate or CNOT (CX) gate. The CX gate flips the state of a qubit (called $target$) conditionally on the state of another qubit (called $control$).

$$\text{8. CNOT gate. The first qubit is the control and the second is the target.}$$

The matrix representation of the CX gate is the following

$$
\hat{C}X_{12} =
\begin{pmatrix}
1 & 0 & 0 & 0\\
0 & 1 & 0 & 0\\
0 & 0 & 0 & 1\\
0 & 0 & 1 & 0
\end{pmatrix}
$$

Let us see the effect of acting with the CX gate on a two-qubit register. In the matrix form shown above, the first qubit will be the control qubit and the second qubit will be the target qubit.
If the control qubit is in the state $\lvert 0 \rangle$, nothing is done to the target qubit. If the control qubit is in state $\lvert 1 \rangle$, the X gate (bit-flip) is applied to the target qubit.

$$
\hat{C}X_{12} \lvert 00 \rangle =
\begin{pmatrix}
1 & 0 & 0 & 0\\
0 & 1 & 0 & 0\\
0 & 0 & 0 & 1\\
0 & 0 & 1 & 0
\end{pmatrix}
\begin{pmatrix}
1 \\
0 \\
0 \\
0
\end{pmatrix} =
\begin{pmatrix}
1 \\
0 \\
0 \\
0
\end{pmatrix} = \lvert 00 \rangle
$$

$$
\hat{C}X_{12} \lvert 01 \rangle =
\begin{pmatrix}
1 & 0 & 0 & 0\\
0 & 1 & 0 & 0\\
0 & 0 & 0 & 1\\
0 & 0 & 1 & 0
\end{pmatrix}
\begin{pmatrix}
0 \\
1 \\
0 \\
0
\end{pmatrix} =
\begin{pmatrix}
0 \\
1 \\
0 \\
0
\end{pmatrix} = \lvert 01 \rangle
$$

$$
\hat{C}X_{12} \lvert 10 \rangle =
\begin{pmatrix}
1 & 0 & 0 & 0\\
0 & 1 & 0 & 0\\
0 & 0 & 0 & 1\\
0 & 0 & 1 & 0
\end{pmatrix}
\begin{pmatrix}
0 \\
0 \\
1 \\
0
\end{pmatrix} =
\begin{pmatrix}
0 \\
0 \\
0 \\
1
\end{pmatrix} = \lvert 11 \rangle
$$

$$
\hat{C}X_{12} \lvert 11 \rangle =
\begin{pmatrix}
1 & 0 & 0 & 0\\
0 & 1 & 0 & 0\\
0 & 0 & 0 & 1\\
0 & 0 & 1 & 0
\end{pmatrix}
\begin{pmatrix}
0 \\
0 \\
0 \\
1
\end{pmatrix} =
\begin{pmatrix}
0 \\
0 \\
1 \\
0
\end{pmatrix} = \lvert 10 \rangle
$$

There is also another way to write the action of the CNOT gate in Dirac's notation:

$$\hat{C}X_{12} \lvert x, y \rangle = \lvert x, x \oplus y \rangle  $$

where $x,y= \{ 0,1 \}$. So that:

$$\hat{C}X_{12} \lvert 0, 0 \rangle = \lvert 0, 0 \oplus 0 \rangle = \lvert 0, 0 \rangle  $$

$$\hat{C}X_{12} \lvert 0, 1 \rangle = \lvert 0, 1 \oplus 0 \rangle = \lvert 0, 1 \rangle  $$

$$\hat{C}X_{12} \lvert 1, 0 \rangle = \lvert 0, 0 \oplus 1 \rangle = \lvert 1, 1 \rangle$$

$$\hat{C}X_{12} \lvert 1, 1 \rangle = \lvert 1, 1 \oplus 1 \rangle = \lvert 1, 0 \rangle  $$

## Problems

<ol>

<li>
Consider the generic state of a qubit $\lvert \psi \rangle = \alpha \lvert 0 \rangle + \beta \lvert 1 \rangle  $. Give the values of $\alpha$ and $\beta$ (normalized to $\lvert \alpha \rvert^2 + \lvert \beta \rvert^2 = 1$) to represent the following ket-vectors
<ol>
<li>
$ \lvert 0 \rangle $
</li>

<li>
$ \lvert 1 \rangle $
</li>

<li>
equal superposition of $\lvert 0 \rangle $ and $\lvert 1 \rangle$
</li>
</ol>
</li>

<li>
Find the basis for the Hilbert space of three qubits (it has dimension 8) using the tensor product of the computational basis of the Hilbert space of a qubit.
</li>

<li>
Given a qubit in the state $\lvert \psi \rangle = \frac{\sqrt{2}}{\sqrt{6}} \lvert 0 \rangle + \frac{\sqrt{4}}{\sqrt{6}} \lvert 1 \rangle$, Calculate:

<ol>
<li>
$ \hat{X} \lvert \psi \rangle $
</li>

<li>
$\hat{Z} \lvert \psi \rangle$
</li>

<li>
$\hat{X} \lvert \psi \rangle$
</li>

<li>
$\hat{Y} \lvert \psi \rangle$
</li>

<li>
$\hat{H} \lvert \psi \rangle$
</li>

</ol>
</li>

<li>
Calculate the following multi-qubit operators in matrix form

<ol>
<li>
$ X \otimes X $
</li>

<li>
$ H \otimes H $
</li>

<li>
$ H \otimes Z $
</li>
</ol>
</li>

<li>
Given a qubit in the state $\lvert \psi \rangle = \frac{\sqrt{2}}{\sqrt{6}} \lvert 00 \rangle + \frac{\sqrt{2}}{\sqrt{6}} \lvert 01 \rangle + \frac{\sqrt{i}}{\sqrt{6}} \lvert 10 \rangle + \frac{\sqrt{1}}{\sqrt{6}} \lvert 11 \rangle$, Calculate $\hat{C}X_{12} \lvert \psi \rangle$, where the first qubit is the control qubit and the second qubit is the target qubit.
</li>

</ol>

## References

[1] R. Feynman, Simulating Physics with Computers, International Journal of Theoretical
Physics, Vol. 21, nos. 6/7, pp. 467{488 (1982).

[2] M. A. Nielsen, and I. L. Chuang, 2000, Quantum Computation
and Quantum Information (Cambridge University Press, Cambridge).

[3] A. Barenco et al., Phys. Rev. A 52, 3457 (1995).


###### Content under Creative Commons Attribution license CC-BY 4.0, code under MIT license (c)2014 L.A. Barba, G.F. Forsyth, C.D. Cooper.

# Spreading out

We're back! This is the fourth notebook of _Spreading out: parabolic PDEs,_  Module 4 of the course [**"Practical Numerical Methods with Python"**](https://openedx.seas.gwu.edu/courses/course-v1:MAE+MAE6286+2017/about).

In the [previous notebook](https://nbviewer.jupyter.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/04_spreadout/04_03_Heat_Equation_2D_Explicit.ipynb), we solved a 2D problem for the first time, using an explicit scheme. We know explicit schemes have stability constraints that might make them impractical in some cases, due to requiring a very small time step. Implicit schemes are unconditionally stable, offering the advantage of larger time steps; in [notebook 2](https://nbviewer.jupyter.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/04_spreadout/04_02_Heat_Equation_1D_Implicit.ipynb), we look at the 1D implicit solution of diffusion. Already, that was quite a lot of work: setting up a matrix of coefficients and a right-hand-side vector, while taking care of the boundary conditions, and then solving the linear system. And now, we want to do implicit schemes in 2D—are you ready for this challenge?

## 2D Heat conduction

We already studied 2D heat conduction in the previous lesson, but now we want to work out how to build an implicit solution scheme. To refresh your memory, here is the heat equation again:

$$
\begin{equation}
\frac{\partial T}{\partial t} = \alpha \left(\frac{\partial^2 T}{\partial x^2} + \frac{\partial^2 T}{\partial y^2} \right)
\end{equation}
$$

Our previous solution used a Dirichlet boundary condition on the left and bottom boundaries, with $T(x=0)=T(y=0)=100$, and a Neumann boundary condition with zero flux on the top and right edges, with $q_x=q_y=0$.

$$
\left( \left.\frac{\partial T}{\partial y}\right|_{y=0.1} = q_y \right) \quad \text{and} \quad \left( \left.\frac{\partial T}{\partial x}\right|_{x=0.1} = q_x \right)
$$

Figure 1 shows a sketch of the problem set up for our hypothetical computer chip with two hot edges and two insulated edges.

####   Figure 1: Simplified microchip problem setup.

### Implicit schemes in 2D

An implicit discretization will evaluate the spatial derivatives at the next time level, $t^{n+1}$, using the unknown values of the solution variable. For the 2D heat equation with central difference in space, that is written as:

$$
\begin{equation}
    \begin{split}
        & \frac{T^{n+1}_{i,j} - T^n_{i,j}}{\Delta t} = \\
        & \quad \alpha \left( \frac{T^{n+1}_{i+1, j} - 2T^{n+1}_{i,j} + T^{n+1}_{i-1,j}}{\Delta x^2} + \frac{T^{n+1}_{i, j+1} - 2T^{n+1}_{i,j} + T^{n+1}_{i,j-1}}{\Delta y^2} \right) \\
    \end{split}
\end{equation}
$$

This equation looks better when we put what we *don't know* on the left and what we *do know* on the right. Make sure to work this out yourself on a piece of paper.

$$
\begin{equation}
    \begin{split}
        & -\frac{\alpha \Delta t}{\Delta x^2} \left( T^{n+1}_{i-1,j} + T^{n+1}_{i+1,j} \right) + \left( 1 + 2 \frac{\alpha \Delta t}{\Delta x^2} + 2 \frac{\alpha \Delta t}{\Delta y^2} \right) T^{n+1}_{i,j} \\
& \quad \quad \quad -\frac{\alpha \Delta t}{\Delta y^2} \left( T^{n+1}_{i,j-1} + T^{n+1}_{i,j+1} \right) = T^n_{i,j} \\
    \end{split}
\end{equation}
$$

To make this discussion easier, let's assume that the mesh spacing is the same in both directions and $\Delta x=\Delta y = \delta$:

$$
\begin{equation}
-T^{n+1}_{i-1,j} - T^{n+1}_{i+1,j} + \left(\frac{\delta^2}{\alpha \Delta t} + 4 \right) T^{n+1}_{i,j} - T^{n+1}_{i,j-1}-T^{n+1}_{i,j+1} = \frac{\delta^2}{\alpha \Delta t}T^n_{i,j}
\end{equation}
$$

Just like in the one-dimensional case, $T_{i,j}$ appears in the equation for $T_{i-1,j}$, $T_{i+1,j}$, $T_{i,j+1}$ and $T_{i,j-1}$, and we can form a linear system to advance in time. But, how do we construct the matrix in this case? What are the $(i+1,j)$, $(i-1,j)$, $(i,j+1)$, and $(i,j-1)$ positions in the matrix?

With explicit schemes we don't need to worry about these things. We can lay out the data just as it is in the physical problem.  We had an array `T` that was a 2-dimensional matrix. To fetch the temperature in the next node in the $x$ direction $(T_{i+1,j})$ we just did `T[j,i+1]`, and likewise in the $y$ direction $(T_{i,j+1})$ was in `T[j+1,i]`. In implicit schemes, we need to think a bit harder about how the data is mapped to the physical problem.

Also, remember from the [notebook on 1D-implicit schemes](https://nbviewer.jupyter.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/04_spreadout/04_02_Heat_Equation_1D_Implicit.ipynb) that the linear system had $N-2$ elements? We applied boundary conditions on nodes $i=0$ and $i=N-1$, and they were not modified by the linear system. In 2D, this becomes a bit more complicated.

Let's use Figure 1, representing a set of grid nodes in two dimensions, to guide the discussion.

####   Figure 2: Layout of matrix elements in 2D problem

Say we have the 2D domain of size $L_x\times L_y$ discretized in $n_x$ and $n_y$ points. We can divide the nodes into boundary nodes (empty circles) and interior nodes (filled circles).

The boundary nodes, as the name says, are on the boundary. They are the nodes with indices $(i=0,j)$, $(i=n_x-1,j)$, $(i,j=0)$, and $(i,j=n_y-1)$, and boundary conditions are enforced there.

The interior nodes are not on the boundary, and the finite-difference equation acts on them. If we leave the boundary nodes aside for the moment, then the grid will have $(n_x-2)\cdot(n_y-2)$ nodes that need to be updated on each time step. This is the number of unknowns in the linear system. The matrix of coefficients will have $\left( (n_x-2)\cdot(n_y-2) \right)^2$ elements (most of them zero!).

To construct the matrix, we will iterate over the nodes in an x-major order: index $i$ will run faster. The order will be

*  $(i=1,j=1)$
*  $(i=2,j=1)$ ...
*  $(i=nx-2,j=1)$
*  $(i=1,j=2)$
*  $(i=2,j=2)$ ...
*  $(i=n_x-2,j=n_y-2)$.

That is the ordering represented by dotted line on Figure 1. Of course, if you prefer to organize the nodes differently, feel free to do so!

Because we chose this ordering, the equation for nodes $(i-1,j)$ and $(i+1,j)$ will be just before and after $(i,j)$, respectively. But what about $(i,j-1)$ and $(i,j+1)$? Even though in the physical problem they are very close, the equations are $n_x-2$ places apart! This can tie your head in knots pretty quickly.

_The only way to truly understand it is to make your own diagrams and annotations on a piece of paper and reconstruct this argument!_

### Boundary conditions

Before we attempt to build the matrix, we need to think about boundary conditions. There is some bookkeeping to be done here, so bear with us for a moment.

Say, for example, that the left and bottom boundaries have Dirichlet boundary conditions, and the top and right boundaries have Neumann boundary conditions.

Let's look at each case:

**Bottom boundary:**

The equation for $j=1$ (interior points adjacent to the bottom boundary) uses values from $j=0$, which are known. Let's put that on the right-hand side of the equation. We get this equation for all points across the $x$-axis that are adjacent to the bottom boundary:

$$
\begin{equation}
    \begin{split}
        -T^{n+1}_{i-1,1} - T^{n+1}_{i+1,1} + \left( \frac{\delta^2}{\alpha \Delta t} + 4 \right) T^{n+1}_{i,1} - T^{n+1}_{i,j+1} \qquad & \\
        = \frac{\delta^2}{\alpha \Delta t} T^n_{i,1} + T^{n+1}_{i,0} & \\
    \end{split}
\end{equation}
$$

**Left boundary:**

Like for the bottom boundary, the equation for $i=1$ (interior points adjacent to the left boundary) uses known values from $i=0$, and we will put that on the right-hand side:

$$
\begin{equation}
    \begin{split}
        -T^{n+1}_{2,j} + \left( \frac{\delta^2}{\alpha \Delta t} + 4 \right) T^{n+1}_{1,j} - T^{n+1}_{1,j-1} - T^{n+1}_{1,j+1} \qquad & \\
        = \frac{\delta^2}{\alpha \Delta t} T^n_{1,j} + T^{n+1}_{0,j} & \\
    \end{split}
\end{equation}
$$

**Right boundary:**

Say the boundary condition is $\left. \frac{\partial T}{\partial x} \right|_{x=L_x} = q_x$. Its finite-difference approximation  is

$$
\begin{equation}
    \frac{T^{n+1}_{n_x-1,j} - T^{n+1}_{n_x-2,j}}{\delta} = q_x
\end{equation}
$$

We can write $T^{n+1}_{n_x-1,j} = \delta q_x + T^{n+1}_{n_x-2,j}$ to get the finite difference equation for $i=n_x-2$:

$$
\begin{equation}
    \begin{split}
        -T^{n+1}_{n_x-3,j} + \left( \frac{\delta^2}{\alpha \Delta t} + 3 \right) T^{n+1}_{n_x-2,j} - T^{n+1}_{n_x-2,j-1} - T^{n+1}_{n_x-2,j+1} \qquad & \\
        = \frac{\delta^2}{\alpha \Delta t} T^n_{n_x-2,j} + \delta q_x & \\
    \end{split}
\end{equation}
$$

Not sure about this? Grab pen and paper! _Please_, check this yourself. It will help you understand!

**Top boundary:**

Neumann boundary conditions specify the derivative normal to the boundary: $\left. \frac{\partial T}{\partial y} \right|_{y=L_y} = q_y$. No need to repeat what we did for the right boundary, right? The equation for $j=n_y-2$ is

$$
\begin{equation}
    \begin{split}
        -T^{n+1}_{i-1,n_y-2} - T^{n+1}_{i+1,n_y-2} + \left( \frac{\delta^2}{\alpha \Delta t} + 3 \right) T^{n+1}_{i,n_y-2} - T^{n+1}_{i,n_y-3} \qquad & \\
        = \frac{\delta^2}{\alpha \Delta t} T^n_{i,n_y-2} + \delta q_y & \\
    \end{split}
\end{equation}
$$

So far, we have then 5 possible cases: bottom, left, right, top, and interior points. Does this cover everything? What about corners?

**Bottom-left corner**

At $T_{1,1}$ there is a Dirichlet boundary condition at $i=0$ and $j=0$. This equation is:

$$
\begin{equation}
    \begin{split}
        -T^{n+1}_{2,1} + \left( \frac{\delta^2}{\alpha \Delta t} + 4 \right) T^{n+1}_{1,1} - T^{n+1}_{1,2} \qquad & \\
        = \frac{\delta^2}{\alpha \Delta t} T^n_{1,1} + T^{n+1}_{0,1} + T^{n+1}_{1,0} & \\
    \end{split}
\end{equation}
$$

**Top-left corner:**

At $T_{1,n_y-2}$ there is a Dirichlet boundary condition at $i=0$ and a Neumann boundary condition at $i=n_y-1$. This equation is:

$$
\begin{equation}
    \begin{split}
        -T^{n+1}_{2,n_y-2} + \left( \frac{\delta^2}{\alpha \Delta t} + 3 \right) T^{n+1}_{1,n_y-2} - T^{n+1}_{1,n_y-3} \qquad & \\
        = \frac{\delta^2}{\alpha \Delta t} T^n_{1,n_y-2} + T^{n+1}_{0,n_y-2} + \delta q_y & \\
    \end{split}
\end{equation}
$$

**Top-right corner**

At $T_{n_x-2,n_y-2}$, there are Neumann boundary conditions at both $i=n_x-1$ and $j=n_y-1$. The finite difference equation is then

$$
\begin{equation}
    \begin{split}
        -T^{n+1}_{n_x-3,n_y-2} + \left( \frac{\delta^2}{\alpha \Delta t} + 2 \right) T^{n+1}_{n_x-2,n_y-2} - T^{n+1}_{n_x-2,n_y-3} \qquad & \\
        = \frac{\delta^2}{\alpha \Delta t} T^n_{n_x-2,n_y-2} + \delta(q_x + q_y) & \\
    \end{split}
\end{equation}
$$

**Bottom-right corner**

To calculate $T_{n_x-2,1}$ we need to consider a Dirichlet boundary condition to the bottom and a Neumann boundary condition to the right. We will get a similar equation to the top-left corner!

$$
\begin{equation}
    \begin{split}
        -T^{n+1}_{n_x-3,1} + \left( \frac{\delta^2}{\alpha \Delta t} + 3 \right) T^{n+1}_{n_x-2,1} - T^{n+1}_{n_x-2,2} \qquad & \\
        = \frac{\delta^2}{\alpha \Delta t} T^n_{n_x-2,1} + T^{n+1}_{n_x-2,0} + \delta q_x & \\
    \end{split}
\end{equation}
$$

Okay, now we are actually ready. We have checked every possible case!

### The linear system

Like in the previous lesson introducing implicit schemes, we will solve a linear system at every time step:

$$
[A][T^{n+1}_\text{int}] = [b]+[b]_{b.c.}
$$

The coefficient matrix now takes some more work to figure out and to build in code. There is no substitute for you working this out patiently on paper!

The structure of the matrix can be described as a series of diagonal blocks, and lots of zeros elsewhere. Look at Figure 3, representing the block structure of the coefficient matrix, and refer back to Figure 2, showing the discretization grid in physical space. The first row of interior points, adjacent to the bottom boundary, generates the matrix block labeled $A_1$. The top row of interior points, adjacent to the top boundary generates the matrix block labeled $A_3$. All other interior points in the grid generate similar blocks, labeled $A_2$ on Figure 3.

####   Figure 3: Sketch of coefficient-matrix blocks.

####   Figure 4: Grid points corresponding to each matrix-block type.

The matrix block $A_1$ is

The block matrix $A_2$ is

The block matrix $A_3$ is

Vector $T^{n+1}_\text{int}$ contains the temperature of the interior nodes in the next time step. It is:

$$
\begin{equation}
T^{n+1}_\text{int} = \left[
\begin{array}{c}
T^{n+1}_{1,1}\\
T^{n+1}_{2,1} \\
\vdots \\
T^{n+1}_{n_x-2,1} \\
T^{n+1}_{2,1} \\
\vdots \\
T^{n+1}_{n_x-2,n_y-2}
\end{array}
\right]
\end{equation}
$$

Remember the x-major ordering we chose!

Finally, the right-hand side is
\begin{equation}
[b]+[b]_{b.c.} =
\left[\begin{array}{c}
\sigma^\prime T^n_{1,1} + T^{n+1}_{0,1} + T^{n+1}_{1,0} \\
\sigma^\prime T^n_{2,0} + T^{n+1}_{2,0} \\
\vdots \\
\sigma^\prime T^n_{n_x-2,1} + T^{n+1}_{n_x-2,0} + \delta q_x \\
\sigma^\prime T^n_{1,2} + T^{n+1}_{0,2} \\
\vdots \\
\sigma^\prime T^n_{n_x-2,n_y-2} + \delta(q_x + q_y)
\end{array}\right]
\end{equation}

where $\sigma^\prime = 1/\sigma = \delta^2/\alpha \Delta t$. The matrix looks very ugly, but it is important you understand it! Think about it. Can you answer:
   * Why a -1 factor appears $n_x-2$ columns after the diagonal? What about $n_x-2$ columns before the diagonal?
   * Why in row $n_x-2$ the position after the diagonal contains a 0?
   * Why in row $n_x-2$ the diagonal is $\sigma^\prime + 3$ rather than $\sigma^\prime + 4$?
   * Why in the last row the diagonal is $\sigma^\prime + 2$ rather than $\sigma^\prime + 4$?

If you can answer those questions, you are in good shape to continue!

Let's write a function that will generate the matrix and right-hand side for the heat conduction problem in the previous notebook. Remember, we had Dirichlet boundary conditions in the left and bottom, and zero-flux Neumann boundary condition on the top and right $(q_x=q_y=0)$.

Also, we'll import `scipy.linalg.solve` because we need to solve a linear system.

```python
import numpy
from scipy import linalg
```

```python
def lhs_operator(M, N, sigma):
    """
    Assembles and returns the implicit operator
    of the system for the 2D diffusion equation.
    We use a Dirichlet condition at the left and
    bottom boundaries and a Neumann condition
    (zero-gradient) at the right and top boundaries.

    Parameters
    ----------
    M : integer
        Number of interior points in the x direction.
    N : integer
        Number of interior points in the y direction.
    sigma : float
        Value of alpha * dt / dx**2.

    Returns
    -------
    A : numpy.ndarray
        The implicit operator as a 2D array of floats
        of size M*N by M*N.
    """
    A = numpy.zeros((M * N, M * N))
    for j in range(N):
        for i in range(M):
            I = j * M + i  # row index
            # Get index of south, west, east, and north points.
            south, west, east, north = I - M, I - 1, I + 1, I + M
            # Setup coefficients at corner points.
            if i == 0 and j == 0:  # bottom-left corner
                A[I, I] = 1.0 / sigma + 4.0
                A[I, east] = -1.0
                A[I, north] = -1.0
            elif i == M - 1 and j == 0:  # bottom-right corner
                A[I, I] = 1.0 / sigma + 3.0
                A[I, west] = -1.0
                A[I, north] = -1.0
            elif i == 0 and j == N - 1:  # top-left corner
                A[I, I] = 1.0 / sigma + 3.0
                A[I, south] = -1.0
                A[I, east] = -1.0
            elif i == M - 1 and j == N - 1:  # top-right corner
                A[I, I] = 1.0 / sigma + 2.0
                A[I, south] = -1.0
                A[I, west] = -1.0
            # Setup coefficients at side points (excluding corners).
            elif i == 0:  # left side
                A[I, I] = 1.0 / sigma + 4.0
                A[I, south] = -1.0
                A[I, east] = -1.0
                A[I, north] = -1.0
            elif i == M - 1:  # right side
                A[I, I] = 1.0 / sigma + 3.0
                A[I, south] = -1.0
                A[I, west] = -1.0
                A[I, north] = -1.0
            elif j == 0:  # bottom side
                A[I, I] = 1.0 / sigma + 4.0
                A[I, west] = -1.0
                A[I, east] = -1.0
                A[I, north] = -1.0
            elif j == N - 1:  # top side
                A[I, I] = 1.0 / sigma + 3.0
                A[I, south] = -1.0
                A[I, west] = -1.0
                A[I, east] = -1.0
            # Setup coefficients at interior points.
            else:
                A[I, I] = 1.0 / sigma + 4.0
                A[I, south] = -1.0
                A[I, west] = -1.0
                A[I, east] = -1.0
                A[I, north] = -1.0
    return A
```

```python
def rhs_vector(T, M, N, sigma, Tb):
    """
    Assembles and returns the right-hand side vector
    of the system for the 2D diffusion equation.
    We use a Dirichlet condition at the left and
    bottom boundaries and a Neumann condition
    (zero-gradient) at the right and top boundaries.

    Parameters
    ----------
    T : numpy.ndarray
        The temperature distribution as a 1D array of floats.
    M : integer
        Number of interior points in the x direction.
    N : integer
        Number of interior points in the y direction.
    sigma : float
        Value of alpha * dt / dx**2.
    Tb : float
        Boundary value for Dirichlet conditions.

    Returns
    -------
    b : numpy.ndarray
        The right-hand side vector as a 1D array of floats
        of size M*N.
    """
    b = 1.0 / sigma * T
    # Add Dirichlet term at points located next
    # to the left and bottom boundaries.
    for j in range(N):
        for i in range(M):
            I = j * M + i
            if i == 0:
                b[I] += Tb
            if j == 0:
                b[I] += Tb
    return b
```

The solution of the linear system $(T^{n+1}_\text{int})$ contains the temperatures of the interior points at the next time step in a 1D array. We will also create a function that will take the values of $T^{n+1}_\text{int}$ and put them in a 2D array that resembles the physical domain.

```python
def map_1d_to_2d(T_1d, nx, ny, Tb):
    """
    Maps a 1D array of the temperature at the interior points
    to a 2D array that includes the boundary values.

    Parameters
    ----------
    T_1d : numpy.ndarray
        The temperature at the interior points as a 1D array of floats.
    nx : integer
        Number of points in the x direction of the domain.
    ny : integer
        Number of points in the y direction of the domain.
    Tb : float
        Boundary value for Dirichlet conditions.

    Returns
    -------
    T : numpy.ndarray
        The temperature distribution in the domain
        as a 2D array of size ny by nx.
    """
    T = numpy.zeros((ny, nx))
    # Get the value at interior points.
    T[1:-1, 1:-1] = T_1d.reshape((ny - 2, nx - 2))
    # Use Dirichlet condition at left and bottom boundaries.
    T[:, 0] = Tb
    T[0, :] = Tb
    # Use Neumann condition at right and top boundaries.
    T[:, -1] = T[:, -2]
    T[-1, :] = T[-2, :]
    return T
```

And to advance in time, we will use

```python
def btcs_implicit_2d(T0, nt, dt, dx, alpha, Tb):
    """
    Computes and returns the distribution of the
    temperature after a given number of time steps.

    The 2D diffusion equation is integrated using
    Euler implicit in time and central differencing
    in space, with a Dirichlet condition at the left
    and bottom boundaries and a Neumann condition
    (zero-gradient) at the right and top boundaries.

    Parameters
    ----------
    T0 : numpy.ndarray
        The initial temperature distribution as a 2D array of floats.
    nt : integer
        Number of time steps to compute.
    dt : float
        Time-step size.
    dx : float
        Grid spacing in the x and y directions.
    alpha : float
        Thermal diffusivity of the plate.
    Tb : float
        Boundary value for Dirichlet conditions.

    Returns
    -------
    T : numpy.ndarray
        The temperature distribution as a 2D array of floats.
    """
    # Get the number of points in each direction.
    ny, nx = T0.shape
    # Get the number of interior points in each direction.
    M, N = nx - 2, ny - 2
    # Compute the constant sigma.
    sigma = alpha * dt / dx**2
    # Create the implicit operator of the system.
    A = lhs_operator(M, N, sigma)
    # Integrate in time.
    T = T0[1:-1, 1:-1].flatten()  # interior points as a 1D array
    I, J = int(M / 2), int(N / 2)  # indices of the center
    for n in range(nt):
        # Compute the right-hand side of the system.
        b = rhs_vector(T, M, N, sigma, Tb)
        # Solve the system with scipy.linalg.solve.
        T = linalg.solve(A, b)
        # Check if the center of the domain has reached T = 70C.
        if T[J * M + I] >= 70.0:
            break
    print('[time step {}] Center at T={:.2f} at t={:.2f} s'
          .format(n + 1, T[J * M + I], (n + 1) * dt))
    # Returns the temperature in the domain as a 2D array.
    return map_1d_to_2d(T, nx, ny, Tb)
```

Remember, we want the function to tell us when the center of the plate reaches $70^\circ C$.

##### Dig deeper

For demonstration purposes, these functions are very explicit. But you can see a trend here, right?

Say we start with a matrix with `1/sigma+4` in the main diagonal, and `-1` on the 4 other corresponding diagonals. Now, we have to modify the matrix only where the boundary conditions are affecting. We saw the impact of the Dirichlet and Neumann boundary condition on each position of the matrix, we just need to know in which position to perform those changes.

A function that maps `i` and `j` into `row_number` would be handy, right? How about `row_number = (j-1)*(nx-2)+(i-1)`? By feeding `i` and `j` to that equation, you know exactly where to operate on the matrix. For example, `i=nx-2, j=2`, which is in row `row_number = 2*nx-5`, is next to a Neumann boundary condition: we have to substract one out of the main diagonal (`A[2*nx-5,2*nx-5]-=1`), and put a zero in the next column (`A[2*nx-5,2*nx-4]=0`). This way, the function can become much simpler!

Can you use this information to construct a more general function `lhs_operator`? Can you make it such that the type of boundary condition is an input to the function?

## Heat diffusion in 2D

Let's recast the 2D heat conduction from the previous notebook, and solve it with an implicit scheme.

```python
# Set parameters.
Lx = 0.01  # length of the plate in the x direction
Ly = 0.01  # length of the plate in the y direction
nx = 21  # number of points in the x direction
ny = 21  # number of points in the y direction
dx = Lx / (nx - 1)  # grid spacing in the x direction
dy = Ly / (ny - 1)  # grid spacing in the y direction
alpha = 1e-4  # thermal diffusivity

# Define the locations along a gridline.
x = numpy.linspace(0.0, Lx, num=nx)
y = numpy.linspace(0.0, Ly, num=ny)

# Compute the initial temperature distribution.
Tb = 100.0  # temperature at the left and bottom boundaries
T0 = 20.0 * numpy.ones((ny, nx))
T0[:, 0] = Tb
T0[0, :] = Tb
```

We are ready to go!

```python
# Set the time-step size based on CFL limit.
sigma = 0.25
dt = sigma * min(dx, dy)**2 / alpha  # time-step size
nt = 300  # number of time steps to compute

# Compute the temperature along the rod.
T = btcs_implicit_2d(T0, nt, dt, dx, alpha, Tb)
```

    [time step 257] Center at T=70.00 at t=0.16 s

And plot,

```python
from matplotlib import pyplot
%matplotlib inline
```

```python
# Set the font family and size to use for Matplotlib figures.
pyplot.rcParams['font.family'] = 'serif'
pyplot.rcParams['font.size'] = 16
```

```python
# Plot the filled contour of the temperature.
pyplot.figure(figsize=(8.0, 5.0))
pyplot.xlabel('x [m]')
pyplot.ylabel('y [m]')
levels = numpy.linspace(20.0, 100.0, num=51)
contf = pyplot.contourf(x, y, T, levels=levels)
cbar = pyplot.colorbar(contf)
cbar.set_label('Temperature [C]')
pyplot.axis('scaled', adjustable='box');
```

Try this out with different values of `sigma`! You'll see that it will always give a stable solution!

Does this result match the explicit scheme from the previous notebook? Do they take the same amount of time to reach $70^\circ C$ in the center of the plate? Now that we can use higher values of `sigma`, we need fewer time steps for the center of the plate to reach $70^\circ C$! Of course, we need to be careful that `dt` is small enough to resolve the physics correctly.

---
###### The cell below loads the style of the notebook

```python
from IPython.core.display import HTML
css_file = '../../styles/numericalmoocstyle.css'
HTML(open(css_file, 'r').read())
```

<link href='http://fonts.googleapis.com/css?family=Alegreya+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Arvo:400,700,400italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=PT+Mono' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Shadows+Into+Light' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Nixie+One' rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Source+Code+Pro' rel='stylesheet' type='text/css'>
<style>

@font-face {
    font-family: "Computer Modern";
    src: url('http://mirrors.ctan.org/fonts/cm-unicode/fonts/otf/cmunss.otf');
}

#notebook_panel { /* main background */
    background: rgb(245,245,245);
}

div.cell { /* set cell width */
    width: 750px;
}

div #notebook { /* centre the content */
    background: #fff; /* white background for content */
    width: 1000px;
    margin: auto;
    padding-left: 0em;
}

#notebook li { /* More space between bullet points */
    margin-top:0.8em;
}

/* draw border around running cells */
div.cell.border-box-sizing.code_cell.running {
    border: 1px solid #111;
}

/* Put a solid color box around each cell and its output, visually linking them*/
div.cell.code_cell {
    background-color: rgb(256,256,256);
    border-radius: 0px;
    padding: 0.5em;
    margin-left:1em;
    margin-top: 1em;
}

div.text_cell_render{
    font-family: 'Alegreya Sans' sans-serif;
    line-height: 140%;
    font-size: 125%;
    font-weight: 400;
    width:600px;
    margin-left:auto;
    margin-right:auto;
}

/* Formatting for header cells */
.text_cell_render h1 {
    font-family: 'Nixie One', serif;
    font-style:regular;
    font-weight: 400;
    font-size: 45pt;
    line-height: 100%;
    color: rgb(0,51,102);
    margin-bottom: 0.5em;
    margin-top: 0.5em;
    display: block;
}

.text_cell_render h2 {
    font-family: 'Nixie One', serif;
    font-weight: 400;
    font-size: 30pt;
    line-height: 100%;
    color: rgb(0,51,102);
    margin-bottom: 0.1em;
    margin-top: 0.3em;
    display: block;
}

.text_cell_render h3 {
    font-family: 'Nixie One', serif;
    margin-top:16px;
    font-size: 22pt;
    font-weight: 600;
    margin-bottom: 3px;
    font-style: regular;
    color: rgb(102,102,0);
}

.text_cell_render h4 {    /*Use this for captions*/
    font-family: 'Nixie One', serif;
    font-size: 14pt;
    text-align: center;
    margin-top: 0em;
    margin-bottom: 2em;
    font-style: regular;
}

.text_cell_render h5 {  /*Use this for small titles*/
    font-family: 'Nixie One', sans-serif;
    font-weight: 400;
    font-size: 16pt;
    color: rgb(163,0,0);
    font-style: italic;
    margin-bottom: .1em;
    margin-top: 0.8em;
    display: block;
}

.text_cell_render h6 { /*use this for copyright note*/
    font-family: 'PT Mono', sans-serif;
    font-weight: 300;
    font-size: 9pt;
    line-height: 100%;
    color: grey;
    margin-bottom: 1px;
    margin-top: 1px;
}

.CodeMirror{
    font-family: "Source Code Pro";
    font-size: 90%;
}

.alert-box {
    padding:10px 10px 10px 36px;
    margin:5px;
}

.success {
    color:#666600;
    background:rgb(240,242,229);
}
</style>


# 07 Uncertainty Analysis

accuracy of pressure transducer $\pm 0.25 \%$ FS or $\pm 0.25 \%$ of reading.

Full scale 0-100 bar:

$\pm 0.25 \%$ of reading, and I read 1 bar, $u_P = 0.0025 \times P$, ie, 0.0025 bar.  If $P = 10$ bar, $u_P = 0.025$ bar.

$\pm 0.25 \%$ FS (full scale), independent of $P$ measured, $u_P = 0.25\% \times full scale$.  ie $u_P = 100\times 0.0025 = 0.25$ bar.  If I read 1 bar, $u_P = 0.25 \times P$ = 25\%.  If $P = 10$ bar, $u_P = 0.25$ bar = 2.5\% of reading.

accuracy includes: linearity, hysterisis, and repeatability

Linearity: Perfect sensor is a $0^th$ order sensor: $y(t) = K F(t)$: if $K$ is a constant: linear response of the sensor.  If $K$ is not constant: response is not linear anymore.  Need to calibrate, very, very carefully.

Hysterisis: Sensitivity to direction of excitation, ie loading vs unloading of sensor.

Repeatability:

$y(t) = K F(t) + a$: $ a$ 0 offset.  due to drift, thermal effects, etc.  We remove it by ``zeroing'' the sensor, ie taking value at 0 input.

## Elemental Uncertainties
Uncertainty due to elemental contributors, $u_i$:
\begin{align}
u_{e} = \sqrt{\sum_i u_i^2}
\end{align}
If you have several components
\begin{align}
u_{overall} & = \sqrt{\sum_{comp} u_{comp}^2}
\end{align}

### Pressure Regulator Calibration

It is desired to use an Equilibar pressure regulator QPV in gas flow experiment.  The specifications for this regulator are:
(https://www.equilibar.com/electronic-pressure-regulators/)

> accuracy = .25%FS

> repeatability = 0.02%FS

However, the pressure regulator accuracy is too low for our application and one decides to calibrate it with a pressure transducer that has an accuracy of 0.04 % of FS.

> What will be the accuracy of the calibrated instrument?

Ucertainty before calibration?

common unit \% FS

```python
import numpy
u_acc = 0.25
u_rep = 0.02
u_e = numpy.sqrt(u_acc**2+u_rep**2)
print('u_e = ',u_e,' %FS')
```

    u_e =  0.25079872407968906  %FS

What if I calibrate?

```python
u_acc_cal = 0.04
u_rep_cal = u_rep
u_e_cal = numpy.sqrt(u_acc_cal**2+u_rep_cal**2)
print('u_e_cal = ',u_e_cal,' %FS')
```

    u_e_cal =  0.044721359549995794  %FS

### Pressure Measurement

A pressure transducer is connected to a digital data-acquisition board on a computer.  The characteristics of the pressure transducer and the data-acquisition system (DAS) are given below.  All uncertainties are given at 95 % confidence level.

The environment temperature is uncertain by $\pm 10^\circ$C.

> Pressure Transducer Characteristics:

Range: 0-1,000 kPa

 Output: 0-5 V

 Linearity: $\pm 0.25$ %FS

 Repeatability: $\pm 0.06$ %FS

 Thermal-stability error: $\pm 0.01$ %FS/$^\circ$C

> DAS Characteristics:

 Number of bits: 10 bits

 Input range: $0-10,\, 0-1,\, 0-0.1,\, 0-0.01$ V

 Gain error: $\pm 2$ LSD

 Linearity: $\pm 2$ LSD

Overall Uncertainty for measurement: Combination of Pressure Transducer and DAS

\begin{align}
u_{overall} & = \sqrt{\sum_{comp} u_{comp}^2} \\
& = \sqrt{u_{PT}^2 + u_{DAS}^2}
\end{align}

Uncertainty of presssure transducer
\begin{align}
u_{PT} = \sqrt{\sum_i u_i^2}
\end{align}
with $u_i$ elemental error:
> linearity: $\pm 0.25$ %FS

> repeatability: $\pm 0.06$ %FS

> Thermal stability: $\pm 0.01$ %FS/$^\circ$C

```python
# U_PT
FS_1 = 1000 #kPa
FS_2 = 5 # V
FS_3 = 100 # %
# here we will work in V, because error %FS, ie fixed V value
# if error % of reading, % would be ok as well.
tempFlu = 10 # degC

u_lin = 0.0025 * FS_2 # V
u_rep = 0.0006 * FS_2 # V
u_th = 0.0001 * FS_2 * tempFlu # V
u_PT = numpy.sqrt(u_lin**2 + u_rep**2 + u_th**2)
print(u_lin, u_rep, u_th)
print(u_PT, ' V')
```

    0.0125 0.0029999999999999996 0.005
    0.013793114224133724  V

LSD: Least Significant Digit

01101: binary word, LSD: bit associated with $2^0$ and MSD (most significant digit): bit associated with $2^{N-1}$ for $N$ bit word.

ie value of the first bin in your digitized word/measurement.

number of bins: $2^N$, $N$: number of bits

size of a bin: resolution, $\Delta V = \frac{V_{max} - V_{min}}{2^N}$

```python
#u _DAS: 3 error sources: DO NOT FORGET QUANTIZATION ERROR

N = 12 # bits
Range = 10 # V, guarantee no clipping
Q = Range/2**(N+1) # V
LSD = 2* Q # V
u_d_lin = 2 * LSD # V
u_d_gain = 2 * LSD # V
u_DAS = numpy.sqrt(u_d_lin**2 + u_d_gain**2 + Q**2)
print(u_d_lin, u_d_gain, Q)
print(u_DAS, ' V')
```

    0.0048828125 0.0048828125 0.001220703125
    0.007012405574387242  V

```python
u_overall = numpy.sqrt(u_PT**2 + u_DAS**2)
print(u_PT, u_DAS)
print(u_overall, ' V')
```

    0.013793114224133724 0.007012405574387242
    0.015473326466526108  V

## Uncertainty of a result

\begin{align}
R = f(x_1, x_2, \dots) \quad u_R = \sqrt{\sum_i \left( u_{x_i} \frac{\partial f}{\partial x_i} \right)^2}
\end{align}

If $f$/ is a polynomial:
\begin{align}
f(x_1, x_2, \dots) =  b \prod_i x_i^{a_i}
\end{align}

Then the uncertainty reduces to:
\begin{align}
\frac{u_R}{R} = \sqrt{\sum_i \left( a_i \frac{u_{x_i}}{x_i} \right)^2}\end{align}

### Design of experiment

One wishes to determine the heat transfer coefficient, $h$, of a heated rod of length, $L$, and diameter, $D$, in a flow. By applying the first law of thermodynamics one can relate the input power of the experiment, $W$, to
$h$ by:

\begin{align*}
h = \frac{W}{\pi D L \Delta T}, \textrm{where $\Delta$T is the temperature difference between the rod and the surrounding fluid}
\end{align*}

The relative uncertainties are: $\frac{u_L}{L}=0.1\%$, $\frac{u_D}{D}=0.2\%$, $\frac{u_{\Delta T}}{\Delta T} = 1.0\%$.  One wishes to have a maximum relative inaccuracy of 2% of $h$.

What should be the relative accuracy of the power measurements, $u_W/W$?

\begin{align}
h = W^1 \pi^{-1} D^{-1} L^{-1} \Delta T^{-1}
\end{align}

\begin{align}
\frac{u_h}{h} = \sqrt{(1\times \frac{u_W}{W})^2 + (-1 \times \frac{u_D}{D})^2 + (-1 \times \frac{u_L}{L})^2 + (-1 \times \frac{u_{\Delta T}}{\Delta T})^2}
\end{align}

\begin{align}
(\frac{u_W}{W})^2 = (\frac{u_h}{h})^2 - \left( (-1 \times \frac{u_D}{D})^2 + (-1 \times \frac{u_L}{L})^2 + (-1 \times \frac{u_{\Delta T}}{\Delta T})^2 \right)
\end{align}

```python
u_hh = 0.02
u_DD = 0.002
u_LL = 0.001
u_tt = 0.01
u_ww = numpy.sqrt(u_hh**2 - (u_DD**2 + u_LL**2 + u_tt**2))

print(u_ww)
```

    0.017175564037317667

uncertainty on W should be less than 1.7% at 95% confidence level (2 $\sigma$).

```python

```