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

# Elementary arithmetic operations
In this tutorial, we are going to provide a construction of quantum networks effecting basic arithmetic operations, covering from addition to modular exponentiation, providing some executable examples using the simulator and five qubit device.

\begin{align}
Plain\; addition \rightarrow Modular\; addition \rightarrow Modular\; multiplication \rightarrow Modular\; exponentiation
\end{align}

### Contributors

Carlos Bravo Prieto

## Introduction
Nowadays, there is no known efficient classical algorithm for factoring. In the past years, the mathematician and MIT professor Peter Shor discovered an efficient algorithm for factoring large numbers using quantum effects, showing the potential power of quantum computation.
There is a lot of interest in an efficient factoring algorithm, because most of the modern encryption is based on the impossibility to factorize large numbers. Therefore, Shor's algorithm could represent the fall of the actual cryptography.

Shor's algorithm uses the quantum fourier transform to find the order $r$ of a randomly chosen number $x$ in the multiplicative group. This is done by exponentiating $x$ modulo $N$. Here, we focus in the quantum modular exponentiation which seems to be the most difficult part. Although the quantum modular exponentiation does not represent any speed up respect their classical analog, it has to be done in order to implement the quantum Shor's algorithm.

## Plain addition
Our first step is to build the most basic operation, the plain addition, which can be written as

\begin{align}
|a, b \rangle \, \rightarrow \, |a, a+b \rangle \,.
\end{align}

To prevent overflows, if $a$ and $b$ are encoded in $n$ qubits, the second register must be of size $n+1$. We will provisionally write the carries of the addition in a temporary register of size $n-1$ initially in state $|0\rangle$. The operation of the plain addition can be understood as:
1. First we compute the most significant bit of the result $a + b$. We have to compute all the carries $c_i$ through
the relation $c_i$ $\leftarrow$ $a_i$ AND $b_i$ AND $c_{i−1}$, where $a_i$, $b_i$ and $c_i$ represent the $i$th qubit of the first, second and temporary register respectively.
2. Finally we reverse all these operations, except for the last one which computed the leading bit of the
result. This way we can reuse the same temporary register if we require repeated additions. Meanwhile we are doing the resetting process, the other qubits of the result are computed through the relation $b_i$ $\leftarrow$ $a_i$ XOR $b_i$ XOR $c_{i−1}$.

The total number of qubits needed is $3n$.

With this figure we seek a generalization of the algorithm, but the first carry qubit $c_0$ is not really needed, because the value will always be $0$.

### Implementation in IBM Q quantum device. Example 1.
We are going to start with the easiest example, the implementation of the plain addition of two one-qubit numbers $a$ and $b$. This program can be executed a 5-qubit device.

```python
# importing Qiskit
from qiskit import Aer, IBMQ, execute
from qiskit.backends.ibmq import least_busy
from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit

# import basic plotting tools
from qiskit.tools.visualization import plot_histogram
from qiskit.tools.qcvv.tomography import marginal_counts
```

```python
IBMQ.load_accounts()
```

```python
# Use local qasm simulator
# backend = Aer.get_backend('qasm_simulator')

# Use the IBM Quantum Experience
backend = least_busy(IBMQ.backends(simulator=False))

# Use the IBM qasm simulator
# backend = IBMQ.get_backend('ibmq_qasm_simulator')
```

First let's define the plain addition algorithm for two one-qubit numbers $a$ and $b$. We implement the quantum plain addition algorithm based on the previous figure. We also define a function that creates those initial states (assuming that our input in Qiskit code is integer). We have to build the Toffoli gate from the elementary gates of the quantum computer.

```python
# quantum plain addition algorithm for 1-qubit numbers
def addition_1bit(circuit, q):
    circuit.h(q[2])
    circuit.cx(q[1], q[2])
    circuit.tdg(q[2])
    circuit.cx(q[0], q[2])
    circuit.t(q[2])
    circuit.cx(q[1], q[2])
    circuit.tdg(q[2])
    circuit.cx(q[0], q[2])
    circuit.t(q[2])
    circuit.h(q[2])
    circuit.t(q[1])
    circuit.cx(q[0], q[1])
    circuit.t(q[0])
    circuit.tdg(q[1])

# n-qubit number input state
def number_state(circuit, q, a, b):
    if a == 1:
        circuit.x(q[0]) # q[0] contains the value of a
    if b == 1:
        circuit.x(q[1]) # q[1] contain the value of b
```

Let's now implement the plain addition of two one-qubit numbers $a+b$, for instance $1+1$, that should return $2 \,(10)$.

In the Quantum Experience composer this circuit would look like:

```python
# we define the values (0 or 1)
a = 1
b = 1

# one single quantum register which contains 'a' (1 qubit) and 'b' (2 qubits)
q = QuantumRegister(3, name="q") # 3 qubits
# clasical register
c = ClassicalRegister(2, name="cr") # 2 bits

# quantum circuit involving the quantum register and the classical register
add1bit_circuit = QuantumCircuit(q ,c, name="add")

# create the state containing a and b
number_state(add1bit_circuit, q, a, b)

# addition
addition_1bit(add1bit_circuit, q)

# measurements to see the result, which has been written in b (q[1]q[2])
add1bit_circuit.measure(q[1], c[0])
add1bit_circuit.measure(q[2], c[1])

# compile and execute the quantum program in the backend
result = execute(add1bit_circuit, backend=backend, shots=1024).result()

# show the results
print(result)
print(result.get_data(add1bit_circuit))
counts = marginal_counts(result.get_counts(add1bit_circuit), [0, 1])
plot_histogram(counts)
print("Backend:", backend.name())
print("Highest probability outcome: {}".format(int(max(counts, key = lambda x: counts[x]).replace(" ", ""), 2)))
```

We see that the highest probability outcome is $2 \;(10)$ when we execute the code on IBM Q quantum device.

### Implementation in the quantum simulator. Example 2.
The second example is the implementation of the plain addition of two $n$-qubit numbers $a$ and $b$. This program can be executed in the quantum simulator.

First let's define different modules that we are going to use repeatedly, as well as the plain addition algorithm for two $n$-qubit numbers.

```python
# Use local qasm simulator
backend = Aer.get_backend('qasm_simulator')

# Use the IBM qasm simulator
# backend = IBMQ.get_backend('ibmq_qasm_simulator')
```

```python
def carry(circuit, q0, q1, q2, q3):
    "carry module"
    circuit.ccx(q1, q2, q3)
    circuit.cx(q1, q2)
    circuit.ccx(q0, q2, q3)

def carry_inv(circuit, q0, q1, q2, q3):
    "carry module but running backwards"
    circuit.ccx(q0, q2, q3)
    circuit.cx(q1, q2)
    circuit.ccx(q1, q2, q3)

def summation(circuit, q0, q1, q2):
    "summation module"
    circuit.cx(q1, q2)
    circuit.cx(q0, q2)

# quantum plain addition algorithm for n-qubit numbers
def addition_nbit(circuit, qa, qb, qcar, n):
    if n == 1:
        circuit.ccx(qa[0], qb[0], qb[1])
        circuit.cx(qa[0], qb[0])
    else:
        circuit.ccx(qa[0], qb[0], qcar[0])
        circuit.cx(qa[0], qb[0])
        for i in range(n-2):
            carry(circuit, qcar[i], qa[i+1], qb[i+1], qcar[i+1])
        carry(circuit, qcar[n-2], qa[n-1], qb[n-1], qb[n])
        circuit.cx(qa[n-1], qb[n-1])
        for i in range(n-1, 1, -1):
            summation(circuit, qcar[i-1], qa[i], qb[i])
            carry_inv(circuit, qcar[i-2], qa[i-1], qb[i-1], qcar[i-1])
        summation(circuit, qcar[0], qa[1], qb[1])
        circuit.cx(qa[0], qb[0])
        circuit.ccx(qa[0], qb[0], qcar[0])
        circuit.cx(qa[0], qb[0])
```

Now we are going to define a function that creates the states containing $a$ and $b$.

```python
# n-qubit number input state
def number_state(circuit, q, x, n):
    # integer to binary
    x = "{0:b}".format(x)
    x = x.zfill(n)
    # creating the state
    for i in range(n):
        if int(x[n-1-i]) == 1:
            circuit.x(q[i])
```

Let's now implement the plain addition of two numbers $a+b$, for instance $9+14$, that should return $23$ $(10111)$:

```python
# we define the values
a = 9
b = 14

# computing the number of qubits n needed
n = len("{0:b}".format(a))
n2 = len("{0:b}".format(b))
if n2 > n:
    n = n2

# classical register with n+1 bits.
c = ClassicalRegister(n+1, name="cr")
# quantum registers
qa = QuantumRegister(n, name="qa") # a qubits
qb = QuantumRegister(n+1, name="qb") # b qubits
# if n = 1, no need of carry register
if n == 1:
    qcar = 0
    # quantum circuit involving the quantum registers and the classical register
    addnbit_circuit = QuantumCircuit(qa, qb,c, name="add")
else:
    qcar = QuantumRegister(n-1, name="qcar") # carry qubits
    # quantum circuit involving the quantum registers and the classical register
    addnbit_circuit = QuantumCircuit(qa, qb, qcar,c, name="add")

# create the state containing a
number_state(addnbit_circuit, qa, a, n)
# create the state containing b
number_state(addnbit_circuit, qb, b, n)

# addition
addition_nbit(addnbit_circuit, qa, qb, qcar, n)

# measurements to see the result
for i in range(n+1):
    addnbit_circuit.measure(qb[i], c[i])

# compile and execute the quantum program in the backend
result = execute(addnbit_circuit, backend=backend, shots=1024).result()

# show the results.
print(result)
print(result.get_data(addnbit_circuit))
counts = result.get_counts(addnbit_circuit)
plot_histogram(counts)
print("Backend:", backend.name())
print("Highest probability outcome: {}".format(int(max(counts, key = lambda x: counts[x]).replace(" ", ""), 2)))
```

We indeed see that the only appearing outcome is $23\,(10111)$ when we execute the code on local_qasm_simulator.

## Modular addition
As you may already expect, as we increase the complexity of the arithmetic operations we also increase the complexity of the circuit network. Our next goal is to build a network that effects

\begin{align}
| a, b \rangle\, \rightarrow \, | a, a+b\, mod \,N \rangle
\end{align}

where $a+b < 2N$ (this condition is enough in order to build the following arithmetic operations). The approach consists essentially on taking the output of the plain adder network and then substracting $N$, depending on whether the value $a+b$ is bigger or smaller than $N$. In order to do so, we use a temporary qubit which indicates whether there has been an overflow in the substraction or not, and adding back $N$ depending on this. The last part of the network takes care of reseting every register (except the second register which contains the result) to its initial state.

The total number of qubits needed is $5n + 2$.

### Implementation in the quantum simulator. Example 3.
The third example is the implementation of the modular addition of two $n$-qubit numbers $a$ and $b$ (modulo $N$). This program can be executed in the quantum simulator.

Now we define again the different modules that we are going to use repeatedly, as well as the modular addition algorithm for two $n$-qubit numbers. This time we also have to substract or use conditional adders (in order to substract $N$ or not), therefore we have to implement new modules such as the conditional plain addition or the plain substraction.

```python
# Use local qasm simulator
backend = Aer.get_backend('qasm_simulator')

# Use the IBM qasm simulator
# backend = IBMQ.get_backend('ibmq_qasm_simulator')
```

```python
def carry(circuit, q0, q1, q2, q3):
    "carry module"
    circuit.ccx(q1, q2, q3)
    circuit.cx(q1, q2)
    circuit.ccx(q0, q2, q3)

def carry_inv(circuit, q0, q1, q2, q3):
    "carry module running backwards"
    circuit.ccx(q0, q2, q3)
    circuit.cx(q1, q2)
    circuit.ccx(q1, q2, q3)

def summation(circuit, q0, q1, q2):
    "summation module"
    circuit.cx(q1, q2)
    circuit.cx(q0, q2)

def summation_inv(circuit, q0, q1, q2):
    "summation module running backwards"
    circuit.cx(q0, q2)
    circuit.cx(q1, q2)

# quantum plain addition algorithm for n-qubit numbers
def addition_nbit(circuit, qa, qb, qcar, n):
    if n == 1:
        circuit.ccx(qa[0], qb[0], qb[1])
        circuit.cx(qa[0], qb[0])
    else:
        circuit.ccx(qa[0], qb[0], qcar[0])
        circuit.cx(qa[0], qb[0])
        for i in range(n-2):
            carry(circuit, qcar[i], qa[i+1], qb[i+1], qcar[i+1])
        carry(circuit, qcar[n-2], qa[n-1], qb[n-1], qb[n])
        circuit.cx(qa[n-1], qb[n-1])
        for i in range(n-1, 1, -1):
            summation(circuit, qcar[i-1], qa[i], qb[i])
            carry_inv(circuit, qcar[i-2], qa[i-1], qb[i-1], qcar[i-1])
        summation(circuit, qcar[0], qa[1], qb[1])
        circuit.cx(qa[0], qb[0])
        circuit.ccx(qa[0], qb[0], qcar[0])
        circuit.cx(qa[0], qb[0])

# quantum plain substraction algorithm for n-qubit numbers
def subs_nbit(circuit, qa, qb, qcar, n):
    "same circuit as the plain addition but going backwards"
    if n == 1:
        circuit.cx(qa[0], qb[0])
        circuit.ccx(qa[0], qb[0], qb[1])
    else:
        circuit.cx(qa[0], qb[0])
        circuit.ccx(qa[0], qb[0], qcar[0])
        circuit.cx(qa[0], qb[0])
        summation_inv(circuit, qcar[0], qa[1], qb[1])
        for i in range(n-2):
            carry(circuit, qcar[i], qa[i+1], qb[i+1], qcar[i+1])
            summation_inv(circuit, qcar[i+1], qa[i+2], qb[i+2])
        circuit.cx(qa[n-1], qb[n-1])
        carry_inv(circuit, qcar[n-2], qa[n-1], qb[n-1], qb[n])
        for i in range(n-2, 0, -1):
            carry_inv(circuit, qcar[i-1], qa[i], qb[i], qcar[i])
        circuit.cx(qa[0], qb[0])
        circuit.ccx(qa[0], qb[0], qcar[0])

def cond_toffoli(circuit, qcond, q1, q2, q3):
    "toffoli gate conditioned by an external qubit"
    circuit.h(q3)
    circuit.ccx(qcond, q2, q3)
    circuit.tdg(q3)
    circuit.ccx(qcond, q1, q3)
    circuit.t(q3)
    circuit.ccx(qcond, q2, q3)
    circuit.tdg(q3)
    circuit.ccx(qcond, q1, q3)
    circuit.t(q3)
    circuit.h(q3)
    circuit.t(q2)
    circuit.ccx(qcond, q1, q2)
    circuit.t(q1)
    circuit.tdg(q2)
    circuit.ccx(qcond, q1, q2)

def cond_carry(circuit, q0, q1, q2, q3, qcond):
    "conditional carry module"
    cond_toffoli(circuit, qcond, q1, q2, q3)
    circuit.ccx(qcond, q1, q2)
    cond_toffoli(circuit, qcond, q0, q2, q3)

def cond_carry_inv(circuit, q0, q1, q2, q3, qcond):
    "conditional carry module running backwards"
    cond_toffoli(circuit, qcond, q0, q2, q3)
    circuit.ccx(qcond, q1, q2)
    cond_toffoli(circuit, qcond, q1, q2, q3)

def cond_summation(circuit, q0, q1, q2, qcond):
    "conditional summation module"
    circuit.ccx(qcond, q1, q2)
    circuit.ccx(qcond, q0, q2)

def cond_summation_inv(circuit, q0, q1, q2, qcond):
    "conditional summation module running backwards"
    circuit.ccx(qcond, q0, q2)
    circuit.ccx(qcond, q1, q2)

# quantum conditional plain addition algorithm for n-qubit numbers
def cond_addition_nbit(circuit, qa, qb, qcar, qcond, n):
    "plain addition algorithm conditioned by an external qubit"
    if n == 1:
        cond_toffoli(circuit, qcond[0], qa[0], qb[0], qb[1])
        circuit.ccx(qcond[0], qa[0], qb[0])
    else:
        cond_toffoli(circuit, qcond[0], qa[0], qb[0], qcar[0])
        circuit.ccx(qcond[0], qa[0], qb[0])
        for i in range(n-2):
            cond_carry(circuit, qcar[i], qa[i+1], qb[i+1], qcar[i+1], qcond[0])
        cond_carry(circuit, qcar[n-2], qa[n-1], qb[n-1], qb[n], qcond[0])
        circuit.ccx(qcond[0], qa[n-1], qb[n-1])
        for i in range(n-1, 1, -1):
            cond_summation(circuit, qcar[i-1], qa[i], qb[i], qcond[0])
            cond_carry_inv(circuit, qcar[i-2], qa[i-1], qb[i-1], qcar[i-1], qcond[0])
        cond_summation(circuit, qcar[0], qa[1], qb[1], qcond[0])
        circuit.ccx(qcond[0], qa[0], qb[0])
        cond_toffoli(circuit, qcond[0], qa[0], qb[0], qcar[0])
        circuit.ccx(qcond[0], qa[0], qb[0])

# quantum conditional plain substraction algorithm for n-qubit numbers
def cond_subs_nbit(circuit, qa, qb, qcar, qcond, n):
    "same circuit as the conditional plain addition but going backwards"
    if n == 1:
        circuit.ccx(qcond[0], qa[0], qb[0])
        cond_toffoli(circuit, qcond[0], qa[0], qb[0], qb[1])
    else:
        circuit.ccx(qcond[0], qa[0], qb[0])
        cond_toffoli(circuit, qcond[0], qa[0], qb[0], qcar[0])
        circuit.ccx(qcond[0], qa[0], qb[0])
        cond_summation_inv(circuit, qcar[0], qa[1], qb[1], qcond[0])
        for i in range(n-2):
            cond_carry(circuit, qcar[i], qa[i+1], qb[i+1], qcar[i+1], qcond[0])
            cond_summation_inv(circuit, qcar[i+1], qa[i+2], qb[i+2], qcond[0])
        circuit.ccx(qcond[0], qa[n-1], qb[n-1])
        cond_carry_inv(circuit, qcar[n-2], qa[n-1], qb[n-1], qb[n], qcond[0])
        for i in range(n-2, 0, -1):
            cond_carry_inv(circuit, qcar[i-1], qa[i], qb[i], qcar[i], qcond[0])
        circuit.ccx(qcond[0], qa[0], qb[0])
        cond_toffoli(circuit, qcond[0], qa[0], qb[0], qcar[0])

# quantum modular addition algorithm for n-qubit numbers
def mod_addition_nbit(circuit, qa, qb, qN, qNtemp, qcar, qtemp, n):
    addition_nbit(circuit, qa, qb, qcar, n)
    subs_nbit(circuit, qN, qb, qcar, n)
    circuit.x(qb[n])
    circuit.cx(qb[n], qtemp[0])
    circuit.x(qb[n])
    cond_subs_nbit(circuit, qNtemp, qN, qcar, qtemp, n)
    addition_nbit(circuit, qN, qb, qcar, n)
    cond_addition_nbit(circuit, qNtemp, qN, qcar, qtemp, n)
    subs_nbit(circuit, qa, qb, qcar, n)
    circuit.cx(qb[n], qtemp[0])
    addition_nbit(circuit, qa, qb, qcar, n)
```

Now we define the function that creates the states containing $a$, $b$ and $N$.

```python
# n-qubit number input state
def number_state(circuit, q, x, n):
    # integer to binary
    x = "{0:b}".format(x)
    x = x.zfill(n)
    # creating the state
    for i in range(n):
        if int(x[n-1-i]) == 1:
            circuit.x(q[i])
```

Let's implement the modular addition of two numbers $a+b \, mod \, N$, for instance $2+3 \,mod\, 3$, that should return $2 \, (010)$:

```python
# we define the values
a = 2
b = 3
N = 3

# computing number of qubits n needed
n = len("{0:b}".format(a))
n2 = len("{0:b}".format(b))
n3 = len("{0:b}".format(N))
if n2 > n:
    n = n2
if n3 > n:
    n = n3

# classical register with n+1 bits.
c = ClassicalRegister(n+1, name="cr")
# quantum registers
qa = QuantumRegister(n, name="qa") # a qubits
qb = QuantumRegister(n+1, name="qb") # b qubits
qN = QuantumRegister(n+1, name="qN") # N qubits
qNtemp = QuantumRegister(n, name="qNtemp") # temporary N qubits
qtemp = QuantumRegister(1, name="qtemp") # temporary qubit
# if n = 1, no need of carry register
if n == 1:
    qcar = 0
    # quantum circuit involving the quantum registers and the classical register
    mod_add_circuit = QuantumCircuit(qa, qb, qN, qNtemp, qtemp,c, name="mod_add")
else:
    qcar = QuantumRegister(n-1, name="qcar") # carry qubits
    # quantum circuit involving the quantum registers and the classical register
    mod_add_circuit = QuantumCircuit(qa, qb, qN, qcar, qNtemp, qtemp, c, name="mod_add")

# create the state containing 'a'
number_state(mod_add_circuit, qa, a, n)
# create the state containing 'b'
number_state(mod_add_circuit, qb, b, n)
# create the state containing 'N'
number_state(mod_add_circuit, qN, N, n)
# create the temporary state containing 'N'
number_state(mod_add_circuit, qNtemp, N, n)

# modular addition
mod_addition_nbit(mod_add_circuit, qa, qb, qN, qNtemp, qcar, qtemp, n)

# measurements to see the result
for i in range(n+1):
    mod_add_circuit.measure(qb[i], c[i])

# compile and execute the quantum program in the backend
result = execute(mod_add_circuit, backend=backend, shots=1024).result()

# show the results.
print(result)
print(result.get_data("mod_add"))
counts = result.get_counts("mod_add")
plot_histogram(counts)
print("Backend:", backend.name())
print("Highest probability outcome: {}".format(int(max(counts, key = lambda x: counts[x]).replace(" ", ""), 2)))
```

Indeed the result is what we were expecting, $2\, (010)$.

## Controlled modular multiplication
The modular multiplication can be implemented by repeated conditional additions modulo $N$: $ax = 2^0 ax_0 + 2^1 ax_1 + ... + 2^{n-1} ax_{n-1}$. We start from a register initiated in the state $|0\rangle$. Then, the network consists in adding conditionally the value $2^i a$, depending on the state of the qubit $|x_i\rangle$. Anyway, in order to implement the modular exponentiation, we need a more slightly complicated circuit, by the fact that we want the multiplication be effected conditionally depending on the value of some external qubit $|c\rangle$. If $|c\rangle = |1\rangle$

\begin{align}
|c; x,0\rangle \, \rightarrow \,  |c;x, a \times x\; mod\, N\rangle
\end{align}

This is done by applying Toffoli gates to $|x_i\rangle$ and control qubits $|c\rangle$ and the corresponding target qubit. The resetting of the register to its initial state is done by applying the same Toffoli gates again. If the control qubit is $|0\rangle$, no operations are done, giving the state $|c; x,0\rangle$. Since we want the state to be $|c;x,x\rangle$ we copy the values of the input register to the result register. This is done in the final part of the network.

However, this circuit requires that the value to be multiplied $a$ be hardwired into the circuit.

\begin{align}
a \times x = (a_{n-1} 2^{n-1} + ... + a_0 2^0)\times (x_{n-1} 2^{n-1} + ... + x_0 2^0) = 2^{2n-2}(a_{n-1} x_{n-1}) + ...+ 2^{n-1}(a_0 x_{n-1} + a_1 x_{n-2} + .... + a_{n-2} x_1 + a_{n-1} x_0) + ... + 2^0 (a_0 x_0).
\end{align}

Notice that the indices of each pair of qubits $a_i x_j$ indicate the respective power of $2$ ($2^{i+j}$). It is important to notice that $2^{i+j}$ can exceed our modulus N. Thus, we classically compute this value at each step and create a temporary register to hold these values.

Summarizing, the controlled multiplication proceed as follows:
1. We begin with $n$-qubits inputs $a$ and $x$ and examine each qubit individually. We apply a three-Toffoli gate to the qubits $a_i$ and $x_j$. If both qubits along with the control qubit are $|1\rangle$ we XOR the incoming value $2^{i+j} \, mod \, N$ to the temporary register.
2. The result is then fed into an addition modulo N module which adds the result to the register holding a running total. After this, the temporary register is set to $|0\rangle$.

The total number of qubits needed is $7n + 3$.

### Implementation in the quantum simulator. Example 4.

The fourth example is the implementation of the controlled modular multiplication of two $n$-qubit numbers $a$ and $x$. This program can be executed in the quantum simulator.

We define the same modules as in the modular addition, as well as the controlled modular multiplication algorithm for $n$-qubit numbers.

```python
# Use local qasm simulator
backend = Aer.get_backend('qasm_simulator')

# Use the IBM qasm simulator
# backend = IBMQ.get_backend('ibmq_qasm_simulator')
```

```python
def carry(circuit, q0, q1, q2, q3):
    "carry module"
    circuit.ccx(q1, q2, q3)
    circuit.cx(q1, q2)
    circuit.ccx(q0, q2, q3)

def carry_inv(circuit, q0, q1, q2, q3):
    "carry module running backwards"
    circuit.ccx(q0, q2, q3)
    circuit.cx(q1, q2)
    circuit.ccx(q1, q2, q3)

def summation(circuit, q0, q1, q2):
    "summation module"
    circuit.cx(q1, q2)
    circuit.cx(q0, q2)

def summation_inv(circuit, q0, q1, q2):
    "summation module running backwards"
    circuit.cx(q0, q2)
    circuit.cx(q1, q2)

# quantum plain addition algorithm for n-qubit numbers
def addition_nbit(circuit, qa, qb, qcar, n):
    if n == 1:
        circuit.ccx(qa[0], qb[0], qb[1])
        circuit.cx(qa[0], qb[0])
    else:
        circuit.ccx(qa[0], qb[0], qcar[0])
        circuit.cx(qa[0], qb[0])
        for i in range(n-2):
            carry(circuit, qcar[i], qa[i+1], qb[i+1], qcar[i+1])
        carry(circuit, qcar[n-2], qa[n-1], qb[n-1], qb[n])
        circuit.cx(qa[n-1], qb[n-1])
        for i in range(n-1, 1, -1):
            summation(circuit, qcar[i-1], qa[i], qb[i])
            carry_inv(circuit, qcar[i-2], qa[i-1], qb[i-1], qcar[i-1])
        summation(circuit, qcar[0], qa[1], qb[1])
        circuit.cx(qa[0], qb[0])
        circuit.ccx(qa[0], qb[0], qcar[0])
        circuit.cx(qa[0], qb[0])

# quantum plain substraction algorithm for n-qubit numbers
def subs_nbit(circuit, qa, qb, qcar, n):
    "same circuit as the addition but going backwards"
    if n == 1:
        circuit.cx(qa[0], qb[0])
        circuit.ccx(qa[0], qb[0], qb[1])
    else:
        circuit.cx(qa[0], qb[0])
        circuit.ccx(qa[0], qb[0], qcar[0])
        circuit.cx(qa[0], qb[0])
        summation_inv(circuit, qcar[0], qa[1], qb[1])
        for i in range(n-2):
            carry(circuit, qcar[i], qa[i+1], qb[i+1], qcar[i+1])
            summation_inv(circuit, qcar[i+1], qa[i+2], qb[i+2])
        circuit.cx(qa[n-1], qb[n-1])
        carry_inv(circuit, qcar[n-2], qa[n-1], qb[n-1], qb[n])
        for i in range(n-2, 0, -1):
            carry_inv(circuit, qcar[i-1], qa[i], qb[i], qcar[i])
        circuit.cx(qa[0], qb[0])
        circuit.ccx(qa[0], qb[0], qcar[0])

def cond_toffoli(circuit, qcond, q1, q2, q3):
    "toffoli gate conditioned by an external qubit"
    circuit.h(q3)
    circuit.ccx(qcond, q2, q3)
    circuit.tdg(q3)
    circuit.ccx(qcond, q1, q3)
    circuit.t(q3)
    circuit.ccx(qcond, q2, q3)
    circuit.tdg(q3)
    circuit.ccx(qcond, q1, q3)
    circuit.t(q3)
    circuit.h(q3)
    circuit.t(q2)
    circuit.ccx(qcond, q1, q2)
    circuit.t(q1)
    circuit.tdg(q2)
    circuit.ccx(qcond, q1, q2)

def cond_carry(circuit, q0, q1, q2, q3, qcond):
    "conditional carry module"
    cond_toffoli(circuit, qcond, q1, q2, q3)
    circuit.ccx(qcond, q1, q2)
    cond_toffoli(circuit, qcond, q0, q2, q3)

def cond_carry_inv(circuit, q0, q1, q2, q3, qcond):
    "conditional carry module running backwards"
    cond_toffoli(circuit, qcond, q0, q2, q3)
    circuit.ccx(qcond, q1, q2)
    cond_toffoli(circuit, qcond, q1, q2, q3)

def cond_summation(circuit, q0, q1, q2, qcond):
    "conditional summation module"
    circuit.ccx(qcond, q1, q2)
    circuit.ccx(qcond, q0, q2)

def cond_summation_inv(circuit, q0, q1, q2, qcond):
    "conditional summation module running backwards"
    circuit.ccx(qcond, q0, q2)
    circuit.ccx(qcond, q1, q2)

# quantum conditional plain addition algorithm for n-qubit numbers
def cond_addition_nbit(circuit, qa, qb, qcar, qcond, n):
    if n == 1:
        cond_toffoli(circuit, qcond, qa[0], qb[0], qb[1])
        circuit.ccx(qcond, qa[0], qb[0])
    else:
        cond_toffoli(circuit, qcond, qa[0], qb[0], qcar[0])
        circuit.ccx(qcond, qa[0], qb[0])
        for i in range(n-2):
            cond_carry(circuit, qcar[i], qa[i+1], qb[i+1], qcar[i+1], qcond)
        cond_carry(circuit, qcar[n-2], qa[n-1], qb[n-1], qb[n], qcond)
        circuit.ccx(qcond, qa[n-1], qb[n-1])
        for i in range(n-1, 1, -1):
            cond_summation(circuit, qcar[i-1], qa[i], qb[i], qcond)
            cond_carry_inv(circuit, qcar[i-2], qa[i-1], qb[i-1], qcar[i-1], qcond)
        cond_summation(circuit, qcar[0], qa[1], qb[1], qcond)
        circuit.ccx(qcond, qa[0], qb[0])
        cond_toffoli(circuit, qcond, qa[0], qb[0], qcar[0])
        circuit.ccx(qcond, qa[0], qb[0])

# quantum conditional plain substraction algorithm for n-qubit numbers
def cond_subs_nbit(circuit, qa, qb, qcar, qcond, n):
    "same circuit as the conditional plain addition but going backwards"
    if n == 1:
        circuit.ccx(qcond, qa[0], qb[0])
        cond_toffoli(circuit, qcond, qa[0], qb[0], qb[1])
    else:
        circuit.ccx(qcond, qa[0], qb[0])
        cond_toffoli(circuit, qcond, qa[0], qb[0], qcar[0])
        circuit.ccx(qcond, qa[0], qb[0])
        cond_summation_inv(circuit, qcar[0], qa[1], qb[1], qcond)
        for i in range(n-2):
            cond_carry(circuit, qcar[i], qa[i+1], qb[i+1], qcar[i+1], qcond)
            cond_summation_inv(circuit, qcar[i+1], qa[i+2], qb[i+2], qcond)
        circuit.ccx(qcond, qa[n-1], qb[n-1])
        cond_carry_inv(circuit, qcar[n-2], qa[n-1], qb[n-1], qb[n], qcond)
        for i in range(n-2, 0, -1):
            cond_carry_inv(circuit, qcar[i-1], qa[i], qb[i], qcar[i], qcond)
        circuit.ccx(qcond, qa[0], qb[0])
        cond_toffoli(circuit, qcond, qa[0], qb[0], qcar[0])

# quantum modular addition algorithm for n-qubit numbers
def mod_addition_nbit(circuit, qa, qb, qN, qNtemp, qcar, qtemp, n):
    addition_nbit(circuit, qa, qb, qcar, n)
    subs_nbit(circuit, qN, qb, qcar, n)
    circuit.x(qb[n])
    circuit.cx(qb[n], qtemp)
    circuit.x(qb[n])
    cond_subs_nbit(circuit, qNtemp, qN, qcar, qtemp, n)
    addition_nbit(circuit, qN, qb, qcar, n)
    cond_addition_nbit(circuit, qNtemp, qN, qcar, qtemp, n)
    subs_nbit(circuit, qa, qb, qcar, n)
    circuit.cx(qb[n], qtemp)
    addition_nbit(circuit, qa, qb, qcar, n)

# quantum controlled modular multiplication algorithm for n-qubit numbers
def cont_mod_mult_nbit(circuit, qa, qb, qN, qNtemp, qcar, qtemp, qtempst, qX, qext, N, n):
    for i in range(n):
        for j in range(n):
            classical_mod = (2**(i+j))%N
            cond_number_state(circuit, qtempst, classical_mod, qext, qa[i], qX[j], n)
            mod_addition_nbit(circuit, qtempst, qb, qN, qNtemp, qcar, qtemp, n)
            cond_number_state(circuit, qtempst, classical_mod, qext, qa[i], qX[j], n)
    circuit.x(qext)
    cond_addition_nbit(circuit, qX, qb, qcar, qext, n)
    circuit.x(qext)
```

We define the function that creates the different states. This time we also define another function, what does essentially the same, but controlled by 3 control qubits.

```python
# n-qubit number input state
def number_state(circuit, q, x, n):
    # integer to binary
    x = "{0:b}".format(x)
    x = x.zfill(n)
    # creating the state
    for i in range(n):
        if int(x[n-1-i]) == 1:
            circuit.x(q[i])

# n-qubit number input state, controlled by 2 control qubits
def cond_number_state(circuit, q, x, ext, control1, control2, n):
    # integer to binary
    x = "{0:b}".format(x)
    x = x.zfill(n)
    # creating the state
    for i in range(n):
        if int(x[n-1-i]) == 1:
            cond_toffoli(circuit, ext, control1, control2, q[i])
```

Let's implement now the controlled modular multiplication of two numbers $a \times x \, mod \, N$, for instance $1 \times 1 \,mod\, 1$ (in order to avoid long computing times, but you can try with two-qubit numbers), that should return $0 \, (00$), if the control qubit is $|1\rangle$ (otherwise the result would be $1 \, (01)$):

```python
# we define the values
a = 1
x = 1
N = 1

# computing number of qubits n needed
n = len("{0:b}".format(a))
n2 = len("{0:b}".format(x))
n3 = len("{0:b}".format(N))
if n2 > n:
    n = n2
if n3 > n:
    n = n3

# classical register with n+1 bits.
c = ClassicalRegister(n+1, name="cr")
# quantum registers
qa = QuantumRegister(n, name="qa") # a qubits
qb = QuantumRegister(n+1, name="qb") # result register
qN = QuantumRegister(n+1, name="qN") # N qubits
qNtemp = QuantumRegister(n, name="qNtemp") # temporary N qubits
qtemp = QuantumRegister(1, name="qtemp") # temporary qubit
qtempst = QuantumRegister(n, name="qtempst") # temporary register
qX = QuantumRegister(n, name="qX") # x register
qext = QuantumRegister(1, name="qext")
# if n = 1, no need of carry register
if n == 1:
    qcar = 0
    # quantum circuit involving the quantum registers and the classical register
    mod_mult_circuit = QuantumCircuit(qa, qb, qN, qNtemp, qtemp, qtempst, qX, qext, c, name="mod_mult")
else:
    qcar = QuantumRegister(n-1, name="qcar") # carry qubits
    # quantum circuit involving the quantum register and the classical register
    mod_mult_circuit = QuantumCircuit(qa, qb, qN, qcar, qNtemp, qtemp, qtempst, qX, qext, c, name="mod_mult")

# create the state containing 'a'
number_state(mod_mult_circuit, qa, a, n)
# create the state containing 'b'
number_state(mod_mult_circuit, qX, x, n)
# create the state containing 'N'
number_state(mod_mult_circuit, qN, N, n+1)
# create a temporary state containing 'N'
number_state(mod_mult_circuit, qNtemp, N, n)

mod_mult_circuit.x(qext[0]) # we set the control qubit to |1>

# controlled modular multiplication
cont_mod_mult_nbit(mod_mult_circuit, qa, qb, qN, qNtemp, qcar, qtemp[0], qtempst, qX, qext[0], N, n)

# measurements to see the result
for i in range(n+1):
    mod_mult_circuit.measure(qb[i], c[i])

# compile and execute the quantum program in the backend
result = execute(mod_mult_circuit, backend=backend, shots=1024).result()

# show the results.
print(result)
print(result.get_data(mod_mult_circuit))
counts = result.get_counts(mod_mult_circuit)
plot_histogram(counts)
print("Backend:", backend.name())
print("Highest probability outcome: {}".format(int(max(counts, key = lambda x: counts[x]).replace(" ", ""), 2)))
```

We indeed obtain the expected result $0 \, (00)$.

## Modular exponentiation
Using the previous constructions, we can finally implement the exponentiation modulo $N$: $a^x \,mod \,N$. The value $a^x$ can be written as $a^x = a^{2^{0}x_0} · a^{2^{1}x_1} · ... · a^{2^{m-1}x_{m-1}}$. The modular exponentiation can be implemented by initially setting a register to $|1\rangle$ and then effecting $n$ modular multiplications by $a^{2^i}$ depending on the value of the qubit $|x_i\rangle$. Let's look into the operations carefully. If $x_i = 1$:

\begin{align}
|a^{2^{0}x_{0} + ... + 2^{i-1}x_{i-1}}, 0 \rangle \;\rightarrow\; |a^{2^{0}x_{0} + ... + 2^{i-1}x_{i-1}}, a^{2^{0}x_{0} + ... + 2^{i-1}x_{i-1}} · a^{2^i}\rangle
\end{align}

otherwise, if $x_i = 0$:

\begin{align}
|a^{2^{0}x_{0} + ... + 2^{i-1}x_{i-1}}, 0 \rangle \;\rightarrow\; |a^{2^{0}x_{0} + ... + 2^{i-1}x_{i-1}}, a^{2^{0}x_{0} + ... + 2^{i-1}x_{i-1}}\rangle \,.
\end{align}

In both cases the result can be written as $|a^{2^{0}x_{0} + ... + 2^{i-1}x_{i-1}}, a^{2^{0}x_{0} + ... + 2^{i}x_{i}}\rangle$. We must take care about the accumulation of intermediate data. To avoid this partial information generated we run backwards a controlled multiplication network with the value $a^{-2^i} \,mod\, N$. This quantity is precomputed classically, but $a$ and $N$ must be coprimes.

The total number of qubits needed is $7n + 3 + n_x$, where $n_x$ are the number of qubits of the value $x$.

### Implementation in the quantum simulator. Example 5.

As you may expect, we do not want to finish this Qiskit tutorial of basic arithmetic operations without the keystone of the Shor's factorization algorithm. The final example is the implementation of the modular exponentiation $a^x \; mod\, N$. This program can be executed in the quantum simulator.

This time we have to run backwards the controlled modular multiplication block, therefore we will need to define new modules such as running backwards the modular addition.

```python
# Use local qasm simulator
backend = Aer.get_backend('qasm_simulator')

# Use the IBM qasm simulator
# backend = IBMQ.get_backend('ibmq_qasm_simulator')
```

```python
def carry(circuit, q0, q1, q2, q3):
    "carry module"
    circuit.ccx(q1, q2, q3)
    circuit.cx(q1, q2)
    circuit.ccx(q0, q2, q3)

def carry_inv(circuit, q0, q1, q2, q3):
    "carry module running backwards"
    circuit.ccx(q0, q2, q3)
    circuit.cx(q1, q2)
    circuit.ccx(q1, q2, q3)

def summation(circuit, q0, q1, q2):
    "summation module"
    circuit.cx(q1, q2)
    circuit.cx(q0, q2)

def summation_inv(circuit, q0, q1, q2):
    "summation module running backwards"
    circuit.cx(q0, q2)
    circuit.cx(q1, q2)

# quantum plain addition algorithm for n-qubit numbers
def addition_nbit(circuit, qa, qb, qcar, n):
    if n == 1:
        circuit.ccx(qa[0], qb[0], qb[1])
        circuit.cx(qa[0], qb[0])
    else:
        circuit.ccx(qa[0], qb[0], qcar[0])
        circuit.cx(qa[0], qb[0])
        for i in range(n-2):
            carry(circuit, qcar[i], qa[i+1], qb[i+1], qcar[i+1])
        carry(circuit, qcar[n-2], qa[n-1], qb[n-1], qb[n])
        circuit.cx(qa[n-1], qb[n-1])
        for i in range(n-1, 1, -1):
            summation(circuit, qcar[i-1], qa[i], qb[i])
            carry_inv(circuit, qcar[i-2], qa[i-1], qb[i-1], qcar[i-1])
        summation(circuit, qcar[0], qa[1], qb[1])
        circuit.cx(qa[0], qb[0])
        circuit.ccx(qa[0], qb[0], qcar[0])
        circuit.cx(qa[0], qb[0])

# quantum plain substraction algorithm for n-qubit numbers
def subs_nbit(circuit, qa, qb, qcar, n):
    "same as the plain addition but running backwards"
    if n == 1:
        circuit.cx(qa[0], qb[0])
        circuit.ccx(qa[0], qb[0], qb[1])
    else:
        circuit.cx(qa[0], qb[0])
        circuit.ccx(qa[0], qb[0], qcar[0])
        circuit.cx(qa[0], qb[0])
        summation_inv(circuit, qcar[0], qa[1], qb[1])
        for i in range(n-2):
            carry(circuit, qcar[i], qa[i+1], qb[i+1], qcar[i+1])
            summation_inv(circuit, qcar[i+1], qa[i+2], qb[i+2])
        circuit.cx(qa[n-1], qb[n-1])
        carry_inv(circuit, qcar[n-2], qa[n-1], qb[n-1], qb[n])
        for i in range(n-2, 0, -1):
            carry_inv(circuit, qcar[i-1], qa[i], qb[i], qcar[i])
        circuit.cx(qa[0], qb[0])
        circuit.ccx(qa[0], qb[0], qcar[0])

def cond_toffoli(circuit, qcond, q1, q2, q3):
    "conditional toffoli gate"
    circuit.h(q3)
    circuit.ccx(qcond, q2, q3)
    circuit.tdg(q3)
    circuit.ccx(qcond, q1, q3)
    circuit.t(q3)
    circuit.ccx(qcond, q2, q3)
    circuit.tdg(q3)
    circuit.ccx(qcond, q1, q3)
    circuit.t(q3)
    circuit.h(q3)
    circuit.t(q2)
    circuit.ccx(qcond, q1, q2)
    circuit.t(q1)
    circuit.tdg(q2)
    circuit.ccx(qcond, q1, q2)

def cond_carry(circuit, q0, q1, q2, q3, qcond):
    "conditional carry module"
    cond_toffoli(circuit, qcond, q1, q2, q3)
    circuit.ccx(qcond, q1, q2)
    cond_toffoli(circuit, qcond, q0, q2, q3)

def cond_carry_inv(circuit, q0, q1, q2, q3, qcond):
    "conditional carry module running backwards"
    cond_toffoli(circuit, qcond, q0, q2, q3)
    circuit.ccx(qcond, q1, q2)
    cond_toffoli(circuit, qcond, q1, q2, q3)

def cond_summation(circuit, q0, q1, q2, qcond):
    "conditional summation"
    circuit.ccx(qcond, q1, q2)
    circuit.ccx(qcond, q0, q2)

def cond_summation_inv(circuit, q0, q1, q2, qcond):
    "conditional summation running backwards"
    circuit.ccx(qcond, q0, q2)
    circuit.ccx(qcond, q1, q2)

# quantum conditional plain addition algorithm for n-qubit numbers
def cond_addition_nbit(circuit, qa, qb, qcar, qcond, n):
    if n == 1:
        cond_toffoli(circuit, qcond, qa[0], qb[0], qb[1])
        circuit.ccx(qcond, qa[0], qb[0])
    else:
        cond_toffoli(circuit, qcond, qa[0], qb[0], qcar[0])
        circuit.ccx(qcond, qa[0], qb[0])
        for i in range(n-2):
            cond_carry(circuit, qcar[i], qa[i+1], qb[i+1], qcar[i+1], qcond)
        cond_carry(circuit, qcar[n-2], qa[n-1], qb[n-1], qb[n], qcond)
        circuit.ccx(qcond, qa[n-1], qb[n-1])
        for i in range(n-1, 1, -1):
            cond_summation(circuit, qcar[i-1], qa[i], qb[i], qcond)
            cond_carry_inv(circuit, qcar[i-2], qa[i-1], qb[i-1], qcar[i-1], qcond)
        cond_summation(circuit, qcar[0], qa[1], qb[1], qcond)
        circuit.ccx(qcond, qa[0], qb[0])
        cond_toffoli(circuit, qcond, qa[0], qb[0], qcar[0])
        circuit.ccx(qcond, qa[0], qb[0])

# quantum conditional plain substraction algorithm for n-qubit numbers
def cond_subs_nbit(circuit, qa, qb, qcar, qcond, n):
    "same as conditional plain addition but running backwards"
    if n == 1:
        circuit.ccx(qcond, qa[0], qb[0])
        cond_toffoli(circuit, qcond, qa[0], qb[0], qb[1])
    else:
        circuit.ccx(qcond, qa[0], qb[0])
        cond_toffoli(circuit, qcond, qa[0], qb[0], qcar[0])
        circuit.ccx(qcond, qa[0], qb[0])
        cond_summation_inv(circuit, qcar[0], qa[1], qb[1], qcond)
        for i in range(n-2):
            cond_carry(circuit, qcar[i], qa[i+1], qb[i+1], qcar[i+1], qcond)
            cond_summation_inv(circuit, qcar[i+1], qa[i+2], qb[i+2], qcond)
        circuit.ccx(qcond, qa[n-1], qb[n-1])
        cond_carry_inv(circuit, qcar[n-2], qa[n-1], qb[n-1], qb[n], qcond)
        for i in range(n-2, 0, -1):
            cond_carry_inv(circuit, qcar[i-1], qa[i], qb[i], qcar[i], qcond)
        circuit.ccx(qcond, qa[0], qb[0])
        cond_toffoli(circuit, qcond, qa[0], qb[0], qcar[0])

# quantum modular addition algorithm for n-qubit numbers
def mod_addition_nbit(circuit, qa, qb, qN, qNtemp, qcar, qtemp, n):
    addition_nbit(circuit, qa, qb, qcar, n)
    subs_nbit(circuit, qN, qb, qcar, n)
    circuit.x(qb[n])
    circuit.cx(qb[n], qtemp)
    circuit.x(qb[n])
    cond_subs_nbit(circuit, qNtemp, qN, qcar, qtemp, n)
    addition_nbit(circuit, qN, qb, qcar, n)
    cond_addition_nbit(circuit, qNtemp, qN, qcar, qtemp, n)
    subs_nbit(circuit, qa, qb, qcar, n)
    circuit.cx(qb[n], qtemp)
    addition_nbit(circuit, qa, qb, qcar, n)

# quantum modular substraction algorithm for n-qubit numbers
def mod_subs_nbit(circuit, qa, qb, qN, qNtemp, qcar, qtemp, n):
    "same as modular addition but running backwards"
    subs_nbit(circuit, qa, qb, qcar, n)
    circuit.cx(qb[n], qtemp)
    addition_nbit(circuit, qa, qb, qcar, n)
    cond_subs_nbit(circuit, qNtemp, qN, qcar, qtemp, n)
    subs_nbit(circuit, qN, qb, qcar, n)
    cond_addition_nbit(circuit, qNtemp, qN, qcar, qtemp, n)
    circuit.x(qb[n])
    circuit.cx(qb[n], qtemp)
    circuit.x(qb[n])
    addition_nbit(circuit, qN, qb, qcar, n)
    subs_nbit(circuit, qa, qb, qcar, n)

# quantum controlled modular multiplication algorithm for n-qubit numbers
def cont_mod_mult_nbit(circuit, qa, qb, qN, qNtemp, qcar, qtemp, qtempst, qX, qext, N, n):
    for i in range(n):
        for j in range(n):
            classical_mod = (2**(i+j))%N
            cond_number_state(circuit, qtempst, classical_mod, qext, qa[i], qX[j], n)
            mod_addition_nbit(circuit, qtempst, qb, qN, qNtemp, qcar, qtemp, n)
            cond_number_state(circuit, qtempst, classical_mod, qext, qa[i], qX[j], n)
    circuit.x(qext)
    cond_addition_nbit(circuit, qX, qb, qcar, qext, n)
    circuit.x(qext)

def cont_inv_mod_mult_nbit(circuit, qa, qb, qN, qNtemp, qcar, qtemp, qtempst, qX, qext, N, n):
    "same as the controlled modular multiplication but running backwards"
    circuit.x(qext)
    cond_subs_nbit(circuit, qX, qb, qcar, qext, n)
    circuit.x(qext)
    for i in range(n):
        for j in range(n):
            classical_mod = (2**(i+j))%N
            cond_number_state(circuit, qtempst, classical_mod, qext, qa[i], qX[j], n)
            mod_subs_nbit(circuit, qtempst, qb, qN, qNtemp, qcar, qtemp, n)
            cond_number_state(circuit, qtempst, classical_mod, qext, qa[i], qX[j], n)

# quantum modular exponentiation algorithm for n-qubit numbers
def mod_exp_nbit(circuit, qa, qb, qN, qNtemp, qcar, qtemp, qtempst, q1, qX, N, a, n):
    for k in range(len(qX)):
        clas_value = (a**(2**(k)))%N
        if k % 2 == 0:
            number_state(circuit, qa, clas_value, n)
            cont_mod_mult_nbit(circuit, qa, qb, qN, qNtemp, qcar, qtemp, qtempst, q1, qX[k], N, n)
            number_state(circuit, qa, clas_value, n)
            clas_value = modinv(a**(2**(k)), N)
            number_state(circuit, qa, clas_value, n)
            cont_inv_mod_mult_nbit(circuit, qa, q1, qN, qNtemp, qcar, qtemp, qtempst, qb, qX[k], N, n)
            number_state(circuit, qa, clas_value, n)
        else:
            number_state(circuit, qa, clas_value, n)
            cont_mod_mult_nbit(circuit, qa, q1, qN, qNtemp, qcar, qtemp, qtempst, qb, qX[k], N, n)
            number_state(circuit, qa, clas_value, n)
            clas_value = modinv(a**(2**(k)), N)
            number_state(circuit, qa, clas_value, n)
            cont_inv_mod_mult_nbit(circuit, qa, qb, qN, qNtemp, qcar, qtemp, qtempst, q1, qX[k], N, n)
            number_state(circuit, qa, clas_value, n)
```

We define the function that creates the different states. This time we also define another two functions, which compute the value of the modular multiplicative inverse $a^{-1} \, mod \, m$.

```python
# n-qubit number input state
def number_state(circuit, q, x, n):
    # integer to binary
    x = "{0:b}".format(x)
    x = x.zfill(n)
    # creating the state
    for i in range(n):
        if int(x[n-1-i]) == 1:
            circuit.x(q[i])

# n-qubit number input state, controlled by 2 control qubits
def cond_number_state(circuit, q, x, ext, control1, control2, n):
    # integer to binary
    x = "{0:b}".format(x)
    x = x.zfill(n)
    # creating the state
    for i in range(n):
        if int(x[n-1-i]) == 1:
            cond_toffoli(circuit, ext, control1, control2, q[i])

# efficient algorithm for computing the modular multiplicative inverse a^-1 mod m
def egcd(a, b):
    if a == 0:
        return (b, 0, 1)
    else:
        g, y, x = egcd(b % a, a)
        return (g, x - (b // a) * y, y)

def modinv(a, m):
    g, x, y = egcd(a, m)
    if g != 1:
        raise Exception('modular inverse does not exist')
    else:
        return x % m
```

Let's implement now the modular exponentiation of two $n$-qubit numbers $a$ and $N$ ($a^x \, mod \, N$), for instance $1^1 \,mod\, 1$ (in order to avoid long computing times, but you can try with two-qubit numbers), that should return $0 \, (00)$:

```python
# we define the values, a and N must be coprimes
a = 1
x = 1
N = 1

# computing number of qubits n needed
n = len("{0:b}".format(a))
n2 = len("{0:b}".format(x))
n3 = len("{0:b}".format(N))

if n3 > n:
    n = n3

# classical register with n+1 bits.
c = ClassicalRegister(n+1, name="cr")
# quantum registers
qa = QuantumRegister(n, name="qa") # a qubits
qb = QuantumRegister(n+1, name="qb") # initial state |0>
qN = QuantumRegister(n+1, name="qN") # N qubits
qNtemp = QuantumRegister(n, name="qNtemp") # temporary N qubits
qtemp = QuantumRegister(1, name="qtemp") # temporary qubit
qtempst = QuantumRegister(n, name="qtempst") # temporary register
q1 = QuantumRegister(n+1, name="q1") # initial state |1>
qX = QuantumRegister(n2, name="qX") # x register
# if n = 1, no need of carry register
if n == 1:
    qcar = 0
    # quantum circuit involving the quantum registers and the classical register
    mod_exp_circuit = QuantumCircuit(qa, qb, qN, qNtemp, qtemp, qtempst, q1, qX, c, name="mod_exp")
else:
    qcar =QuantumCircuit(n-1, name="qcar") # carry qubits
    # quantum circuit involving the quantum registers and the classical register
    mod_exp_circuit = QuantumCircuit(qa, qb, qN, qcar, qNtemp, qtemp, qtempst, q1, qX, c, name="mod_exp")

# create the initial state |1>. If N = 1, initial state is |0>
if N != 1:
    number_state(mod_exp_circuit, q1, 1, 1)
# create the state containing 'x'
number_state(mod_exp_circuit, qX, x, n2)
# create the state containing 'N'
number_state(mod_exp_circuit, qN, N, n+1)
# create a temporary state containing 'N'
number_state(mod_exp_circuit, qNtemp, N, n)

# modular exponentiation
mod_exp_nbit(mod_exp_circuit, qa, qb, qN, qNtemp, qcar, qtemp[0], qtempst, q1, qX, N, a, n)

# measurements to see the result, the result would be in one of those registers, q1 or qb
if n2 % 2 == 0:
    for i in range(n+1):
        mod_exp_circuit.measure(q1[i], c[i])
else:
    for i in range(n+1):
        mod_exp_circuit.measure(qb[i], c[i])

# compile and execute the quantum program in the backend
result = execute(mod_exp_circuit, backend=backend, shots=1024).result()

# show the results.
print(result)
print(result.get_data(mod_exp_circuit))
counts = result.get_counts(mod_exp_circuit)
plot_histogram(counts)
print("Backend:", backend.name())
print("Highest probability outcome: {}".format(int(max(counts, key = lambda x: counts[x]).replace(" ", ""), 2)))
```

We indeed have obtained the expected result $0 \, (00)$.


# Tuning the parameters of a PID controller
In this notebook you can test your intuition for how to adjust the parameters of a PID controller.

Start by watching [this excellent video](https://www.youtube.com/watch?v=XfAt6hNV8XM).

## Blockdiagram
We consider the model used in the video: Velocity control of a car. The plant model describes how the velocity of the car responds to the position of the accelerator (the gas pedal). In addition to the signal from the accelerator, there are also unknown forces such as wind resistance and gravity when the car is going uphill or downhill. These forces are represented by a disturbance signal entering at the input to the system.

<!---  -->

## The PID controller
The PID-controller is on so-called *parallel form*
\begin{equation}
F(s) = K_p + \frac{K_i}{s} + K_d s.
\end{equation}

## The closed-loop system from the reference signal to the output
The model is linear and hence the principle of superposition holds. This mean that we can look at the response to the reference signal and the response to the disturbance signal separately. Setting $$d=0,$$ we get a closed-loop response given by
\begin{equation}
Y(s) = \frac{\frac{1}{s(s+1)}F(s)}{1 + \frac{1}{s(s+1)}F(s)}R(s).
\end{equation}

## The closed-loop system from disturbance to the output
Setting $$r=0,$$ the reponse to the disturbance is given by
\begin{equation}
Y(s) = \frac{\frac{1}{s(s + 1)}}{1 + \frac{1}{s(s+1)}F(s)}D(s)
\end{equation}

## The full closed-loop system
We can find the response to a combination of input signals $r$ and $d$ by summation:
\begin{equation}
Y(s) = \frac{\frac{1}{s(s+1)}F(s)}{1 + \frac{1}{s(s+1)}F(s)}R(s) + \frac{\frac{1}{s(s + 1)}}{1 + \frac{1}{s(s+1)}F(s)}D(s)
\end{equation}

```python
# Uncomment and run the commands in this cell if a packages is missing
!pip install slycot
!pip install control
```

```python
%matplotlib widget
import ipywidgets as widgets
import matplotlib.pyplot as plt
import numpy as np
import control.matlab as cm
```

## Step response
Below you can manipulate the $K_p$, $K_i$ and $K_d$ parameters of the PID-controller, and see a time-response of the system. At time $t_1=1$ there is a unit step in the reference signal, and at time $t_2=10$ yhere is a negative step in the disturbance signal. Note that since we scaled time using the time constant of the system, the time is not measured in seconds but in the length of time constant. So to get $t_2$ in seconds you will have to multiply with the time constant
\begin{equation}
t_2 = 5 T = 5 \frac{1}{\omega}
\end{equation}
where $\omega$ has unit $1/s$ or $Hz$.

```python
G1 = cm.tf([1.], [1, 1.])
Gint = cm.tf([1], [1, 0])
G = Gint*G1
print(G)
N = 600
t_end = 30
t = np.linspace(0, t_end, N)

# The reference signal
r = np.zeros(N)
r[int(N/t_end):] = 1.0

# The disturbance signal
d = np.zeros(N)
d[int(N/t_end)*10:] = -1.0

# set up plot
fig, ax = plt.subplots(figsize=(8, 3))
#ax.set_ylim([-.1, 4])
ax.grid(True)

def sim_PID(G, Kp, Ki, Kd, r,d,t):
    """
    Returns the simulated response of the closed-loop system with a
    PID controller.
    """
    F = cm.tf([Kd, Kp, Ki], [1.0, 0])
    Gr = cm.feedback(G*F,1)
    Gd = cm.feedback(G,F)
    yr = cm.lsim(Gr, r, t)
    yd = cm.lsim(Gd, d, t)
    return (yr, yd)

@widgets.interact(Kp=(0, 10, .2), Ki=(0, 8, .2), Kd=(0, 8, .2))
def update(Kp = 1.0, Ki=0, Kd=0):
    """Remove old lines from plot and plot new one"""
    [l.remove() for l in ax.lines]
    yr, yd = sim_PID(G, Kp, Ki, Kd, r,d,t)
    ax.plot(yr[1], yr[0]+yd[0], color='C0')
    #ax.plot(yd[1], yd[0], color='C1')

```

       1
    -------
    s^2 + s

    FigureCanvasNbAgg()

    interactive(children=(FloatSlider(value=1.0, description='Kp', max=10.0, step=0.2), FloatSlider(value=0.0, des…

### Exercise
Try to find PID parameters that give
1. about 10% overshoot,
2. settling time of about 4,
3. negligable stationary error at $t=14$ (4 after onset of constant disturbance)

## Ramp response
A negative unit ramp disturbance starts at time $t_1=0$.

```python

# The reference signal
dramp = np.linspace(0, t_end, N)
rr = np.zeros(N)

# set up plot
fig, ax = plt.subplots(figsize=(8, 3))
#ax.set_ylim([-.1, 4])
ax.grid(True)

@widgets.interact(Kp=(0, 10, .2), Ki=(0, 8, .2), Kd=(0, 8, .2))
def update(Kp = 1.0, Ki=1, Kd=0):
    """Remove old lines from plot and plot new one"""
    [l.remove() for l in ax.lines]
    yr, yd = sim_PID(G, Kp, Ki, Kd, rr,dramp,t)
    ax.plot(yr[1], yr[0]+yd[0], color='C0')
    #ax.plot(yd[1], yd[0], color='C1')

```

    FigureCanvasNbAgg()

    interactive(children=(FloatSlider(value=1.0, description='Kp', max=10.0, step=0.2), FloatSlider(value=1.0, des…

### Exercise
Why does the error in the ramp response keep growing although the controller contains an integrating term?

## Further reading
- [Documentation for the Python Control Systems library](https://python-control.readthedocs.io/en/0.8.3/)
- [PID control on Wikipedia](https://en.wikipedia.org/wiki/PID_controller)

```python

```


Implicit Multistep Methods
==========================

```
%matplotlib inline
```

```
import numpy as np
import matplotlib
import matplotlib.pyplot as plt

matplotlib.rcParams.update({'font.size': 14})
```

Looking at IVPs of the form

$${\boldsymbol{y}}'(x) = {\boldsymbol{f}}(x, {\boldsymbol{y}}(x)).$$

Looked at multistep methods using the $k$-step method formula

$$a_k y_{n+1} + a_{k-1} y_n + \dots + a_0 y_{n+1-k} = h \left[ b_k f_{n+1} + b_{k-1} f_n + \dots + b_0 f_{n+1-k} \right].$$

Explicit when $b_k = 0$, as in Adams-Bashforth methods. Even an explicit
multistep method still needs a different method to start; multistage
methods such as Runge-Kutta are typically used.

May want to use an *implicit* formula where $b_k \neq 0$. This may be
either for accuracy or stability, as we shall consider later.

Adams-Moulton Methods
---------------------

Compare the explicit Adams-Bashforth methods

$$y_{n+1} - y_n = h\left[ b_{k-1} f_n + b_{k-2} f_{n-1} + \dots \right]$$

with the *implicit* Adams-Moulton methods

$$y_{n+1} - y_n = h \left[ b_k f_{n+1} + b_{k-1} f_{n} + \dots \right]$$

 As before assume the $x_j$ are evenly spaced. Efficient
as function evaluations can be stored, but difficult to use with
adaptive step sizes. Formula cannot be used directly:
$f_{n+1}$ cannot be computed directly.

Standard practice to use Adams-Moulton methods as part of a
predictor-correct method, where estimate $y^{(p)}_{n+1}$ is computed,
and then the unknown $f_{n+1}$ estimated as

$$f_{n+1} = f(x_{n+1}, y^{(p)}_{n+1}).$$

Typically use Adams-Bashforth as the predictor
(minimizes function evaluations), but any method (e.g. RK45) valid.

### Computing the coefficients

Computing the fixed coefficients $b_m$ is identical to Adams-Bashforth:
integrate the formula

$$y_{n+1} - y_n = h \left[ b_k f_{n+1} + b_{k-1} f_{n} + \dots b_1 f_{n+2-k} \right]$$

over the range, assuming $f(x) = p_s(x)$ for polynomials of degree
$s = 0, \dots k-1$.

As before we choose as a basis for the polynomials

$$p_s(x) = x (x + h) \dots (x + h (s-1))$$

as $p_s$ vanishes at $x = 0, -h, \dots, -h (s-1)$. Then choose
$x_{n+1} = 0, x_{n} = -h$ and so on: the resulting system of equations
is in upper-triangular form – solve by back substitution.

### Adams-Moulton example

The Adams-Moulton method of order 5 has formula

$$y_{n+1} - y_n = h \left[ b_5 f_{n+1} + b_4 f_{n} + b_3 f_{n-1} + b_2 f_{n-2} + b_1 f_{n-3} \right],$$

and the coefficients follow from

\begin{align}
p_0 & = 1: & h & = h \left[ b_5 + b_4 + b_3 + b_2 + b_1 \right] \\
p_1 & = x: & -\frac{h^2}{2} & = h \left[ -h \left( b_4 + 2 b_3 + 3
    b_2 + 4 b_1 \right) \right] \\
p_2 & = x (x + h): & -\frac{h^3}{6} & = h \left[ h^2 \left( 2 b_3
    + 6 b_2 + 12 b_1 \right) \right] \\
p_3 & = x (x + h) (x + 2 h): & -\frac{h^4}{4} & = h \left[ -h^3
  \left( 6 b_2 + 24 b_1 \right) \right] \\
p_4 & = x (x + h) (x + 2 h) (x + 3 h): & -19 \frac{h^5}{30} & = h
\left[ 24 h^4 b_1 \right].
\end{align}

### Standard example

Apply the 5 step Adams-Moulton method to

$$y'(x) = - \sin(x), \quad y(0) = 1$$

and integrate to $x = 0.5$. Use RK4 to start algorithm and
Adams-Bashforth 5 to predict. Using $h = 0.05$ gives

|$n$|$x_n$|$y_n$|$f(x_n, y_n)$|$\cos(x_n)$|
|---|-----|-----|-------------|-----------|
|0  | 0.0 |1.000| 0.000       | 1.000     |
|1  | 0.1 |0.995| -0.100      | 0.995     |
|2  | 0.2 |0.980| -0.199      | 0.980     |
|3  | 0.3 |0.955| -0.296      | 0.955     |
|4  | 0.4 |0.921| -0.389      | 0.921     |
|5  | 0.5 |0.878|             | 0.878     |

The error is is $2 \times 10^{-9}\%$, not visible at this precision.
With $h = 0.01$ the error is $0.9 \times 10^{-12}\%$. Convergence is
slightly worse than fifth order; result is slightly biased by the
starting method.

```
def am5_method(f, x_end, y0, N):
    """Solve IVP y'=f(x, y(x)) on x \in [0, x_end] with y(0) = y0 using N+1 points, using 5th order Adams-Moulton method."""

    h = x_end / N
    x = np.linspace(0.0, x_end, N+1)

    y = np.zeros((N+1, len(y0)))
    fn = np.zeros_like(y)
    y[0, :] = y0

    # Start using RK4 steps
    for n in range(4):
        fn[n, :] = f(x[n], y[n, :])
        k1 = h * fn[n, :]
        k2 = h * f(x[n] + h / 2.0, y[n, :] + k1 / 2.0)
        k3 = h * f(x[n] + h / 2.0, y[n, :] + k2 / 2.0)
        k4 = h * f(x[n] + h, y[n, :] + k3)
        y[n+1, :] = y[n, :] +  (k1 + k4 + 2.0 * (k2 + k3)) / 6.0

    # Solve for the coefficients

    coeff_A = np.array([[1.0, 1.0, 1.0, 1.0, 1.0],[0.0, -1.0, -2.0, -3.0, -4.0],[0.0, 0.0, 2.0, 6.0, 12.0],
                        [0.0, 0.0, 0.0, -6.0, -24.0],[0.0, 0.0, 0.0, 0.0, 24.0]])
    coeff_b = np.array([1.0, 1.0 / 2.0, 5.0/6.0, 9.0/4.0, 251.0/30.0])
    b_ab4 = np.linalg.solve(coeff_A, coeff_b)
    b_am5 = np.array([251.0, 646.0, -264.0, 106.0, -19.0])/720.0

    for n in range(4, N):
        fn[n, :] = f(x[n], y[n, :])
        yp = y[n, :] + h * (b_ab4[0]*fn[n, :] + b_ab4[1]*fn[n-1, :] + b_ab4[2]*fn[n-2, :] +
                            b_ab4[3]*fn[n-3, :] + b_ab4[4]*fn[n-4, :])
        y[n+1, :] = y[n, :] + h * (b_am5[0]*f(x[n+1], yp) + b_am5[1]*fn[n, :] + b_am5[2]*fn[n-1, :] +
                                   b_am5[3]*fn[n-2, :] + b_am5[4]*fn[n-3, :])

    return x, y

def fn_simple(x, y):
    """Simple function for example"""

    return -np.sin(x)

# Example given above
x_5, y_5 = am5_method(fn_simple, 0.5, [1.0], 5)
x_50, y_50 = am5_method(fn_simple, 0.5, [1.0], 50)

print("Solution at x = 0.5 using h = 0.1 is y = {}.".format(y_5[-1, 0]))
print("Error at x = 0.5 using h = 0.1 is y = {}.".format(abs(np.cos(0.5) - y_5[-1, 0])))
print("Solution at x = 0.5 using h = 0.01 is y = {}.".format(y_50[-1, 0]))
print("Error at x = 0.5 using h = 0.01 is y = {}.".format(abs(np.cos(0.5) - y_50[-1, 0])))
```

    Solution at x = 0.5 using h = 0.1 is y = 0.877582541412.
    Error at x = 0.5 using h = 0.1 is y = 2.04779130319e-08.
    Solution at x = 0.5 using h = 0.01 is y = 0.87758256189.
    Error at x = 0.5 using h = 0.01 is y = 8.2911455479e-13.

### Standard Example: 2

Consider the system

$$\left\{
\begin{aligned}
  \dot{x} & = -y \\ \dot{y} & = x
\end{aligned} \right., \quad x(0) = 1, \, \, y(0) = 0.$$

In polar coordinates this is $\dot{r} = 0$, $\dot{\phi} = 1$.

Use Adams-Moulton 5 step method with $h=0.1$. At
$t=500$ the result matches the correct answer to the eye.

The growth of the radius makes the errors visible.

Use Adams-Moulton 5 step method with $h=0.01$. At
$t=500$ the result matches the correct answer to the eye.

Looking at the growth of the radius makes the errors visible even now.

```
def fn_circular_motion(x, y):
    """Simple function for circular motion example"""

    dfdt = np.zeros_like(y)
    dfdt[0] = -y[1]
    dfdt[1] =  y[0]

    return dfdt

# Example given above
y0 = np.array([1.0, 0.0])
t_0_1, y_0_1 = am5_method(fn_circular_motion, 50.0, y0, 500)
t_0_01, y_0_01 = am5_method(fn_circular_motion, 50.0, y0, 5000)

fig = plt.figure(figsize = (10,6))
ax1 = fig.add_subplot(121)
ax1.plot(y_0_1[:, 0], y_0_1[:, 1], 'b-', label = '$h = 0.1$')
ax1.set_xlabel('$x$')
ax1.set_ylabel('$y$')
ax2 = fig.add_subplot(122)
ax2.plot(y_0_01[:, 0], y_0_01[:, 1], 'b-', label = '$h = 0.01$')
ax2.set_xlabel('$x$')
ax2.set_ylabel('$y$')
fig.tight_layout()
plt.show()
```

```
fig = plt.figure(figsize = (10,6))
ax1 = fig.add_subplot(121)
ax1.plot(t_0_1, np.sqrt(y_0_1[:, 0]**2 + y_0_1[:, 1]**2)-1.0, 'b-', label = '$h = 0.1$')
ax1.set_xlabel('$t$')
ax1.set_ylabel('$r - 1$')
ax2 = fig.add_subplot(122)
ax2.plot(t_0_01, np.sqrt(y_0_01[:, 0]**2 + y_0_01[:, 1]**2)-1.0, 'b-', label = '$h = 0.01$')
ax2.set_xlabel('$t$')
ax2.set_ylabel('$r - 1$')
fig.tight_layout()
plt.show()
```

```
Npoints = np.array([50*2**i for i in range(2, 10)])
h = np.zeros_like(Npoints, float)
h = 50.0 / Npoints
err = np.zeros_like(h)

for i in range(len(Npoints)):
    N = Npoints[i]
    x, y = am5_method(fn_circular_motion, 50.0, y0, N)
    err[i] = np.abs(y[-1, 0]**2 + y[-1, 1]**2 - 1.0)

# Find the best fit to the data: if err ~ A h^s then the best fit straight line of log(err) vs log(h) has slope s.
p = np.polyfit(np.log(h[1:]), np.log(err[1:]), 1)

fig = plt.figure(figsize = (10,6))
ax = fig.add_subplot(111)
ax.loglog(h, err, 'kx', label = 'Numerical data')
ax.loglog(h, np.exp(p[1])*h**(p[0]), 'b-', label = "Line slope {:.2f}".format(p[0]))
ax.set_xlabel('h')
ax.set_ylabel('$\|$ Error $\|$')
ax.set_title("Convergence of $5^{th}$ order Adams-Bashforth method for circular motion example")
ax.legend(loc = 2)
fig.tight_layout()
plt.show()
```

Order of multistep methods
--------------------------

A $k$-step multistep method should be $k^{\text{th}}$ order accurate as
$k^{\text{th}}$ order polynomials are exactly represented. To check
compute the local truncation error.

For the general $k$-step method

$$a_k y_{n+1} + a_{k-1} y_n + \dots + a_0 y_{n+1-k} = h \left[ b_k f_{n+1} + b_{k-1} f_n + \dots + b_0 f_{n+1-k} \right]$$

use the linear functional

$$L(y) = \sum_{p=0}^k \left[ a_p y(x_{n-k} + p h) - h b_p y'(x_{n-k}+ ph) \right].$$

Substitute $y' = f(x, y)$ to see that the functional represents the
$k$-step formula. Leading order term therefore gives the local
truncation error.

Another way of thinking about the functional is the following. The local
truncation error is given by $| y_{n+1} - y(x_{n+1}) |$, assuming that
$y_n, y_{n-1}, \dots$ are all exact. We can then write

\begin{align}
  \text{Error} & \propto | a_k \left( y_{n+1} - y(x_{n+1}) \right) |
  \\
  & = | -a_k y(x_{n+1}) + h \sum_{p=0}^k b_p f_{n+1-k+p} -
  \sum_{\ell=0}^{k-1} a_p y_{n+1-k+p} |
\end{align}
using the $k$-step formula, then
\begin{align}
  & = | -a_k y(x_{n+1}) + h \sum_{p=0}^k b_p y'(x_{n+1-k+p}) -
  \sum_{\ell=0}^{k-1} a_p y(x_{n+1-k+p}) | \\
\end{align}
from the IVP $y' = f$, and using that $y_n$ etc. are exact,
\begin{align}
  & = | \sum_{p=0}^k \left[ a_p y(x_{n-k} + p h) - h b_p y'(x_{n-k} +
ph) \right] |
\end{align}

by re-ordering the sum and shifting the location of the $x_n$ points
(which we are free to do).

### Local truncation error of multistep methods

Take the Taylor series expansion of both $y$ and $y'$,

$$y(x_0 + p h)  = \sum_j \frac{(p h)^j}{j!} y^{(j)}(x_0), \quad y'(x_0 + p h) = \sum_j \frac{(p h)^j}{j!} y^{(j+1)}(x_0),$$

and substitute in the linear functional

$$L(y) = \sum_{p=0}^k \left[ a_p y(x_{n-k} + p h) - h b_p y'(x_{n-k} + ph) \right]$$

to give

$$L(y) = d_0 y(x_0) + d_1 h y'(x_0) + d_2 h^2 y''(x_0) + \dots$$

Expect that the first $k$ coefficients should vanish in a $k$-step
method.

### Relating the coefficients

The $k$-step method depends on the $a_m$ and $b_m$ coefficients; the
accuracy of the method depends on the $d_m$ coefficients. By matching
terms in the Taylor expansion get

$$\begin{aligned}
d_0 & = \sum_{p=0}^k a_p, \\
d_1 & = \sum_{p=0}^k p a_p - b_p, \\
d_2 & = \sum_{p=0}^k \frac{p^2}{2} a_p - p b_p,
\end{aligned}$$

and in general

$$d_j = \sum_{p=0}^k \left( \frac{p^j}{j!} a_p - \frac{p^{j-1}}{(j-1)!} b_p \right), \quad j \geq 2.$$

### Example

For the two step Adams-Bashforth method have

$$y_{n+1} - y_n = \frac{h}{2} \left[ 3 f_n - f_{n-1} \right].$$

Equivalent to the coefficients

$$a_2 = 1, \,\, a_1 = -1, \quad b_1 = 3/2, \,\, b_0 = -1/2.$$

From this compute the $d_m$ coefficients:

$$\begin{aligned}
d_0 & = \textstyle\sum_{p=0}^k a_p = (1 - 1) = 0, \\
d_1 & = \textstyle\sum_{p=0}^k p a_p - b_p = \left(2 - 1
\right) - \left( 3/2 - 1/2 \right) = 0, \\
d_2 & = \textstyle\sum_{p=0}^k \tfrac{p^2}{2} a_p - p b_p =
\left( 2 - 1/2 \right) - \left( 3/2 + 0 \right) = 0, \\
d_3 & = \textstyle\sum_{p=0}^k \tfrac{p^3}{6} a_p -
\tfrac{p^2}{2} b_p = \left( 4/3 - 1/6 \right) - \left( 1/2 \cdot
  3/2 \right) \neq 0.
\end{aligned}$$

Hence the local truncation error is ${\cal O}(h^3)$ and the global
truncation error ${\cal O}(h^2)$.

### Example: Milne’s method

Milne’s method represents $y'$ using central differencing and the
integral with Simpson’s rule:

$$y_{n+1} - y_{n-1} = \frac{h}{3} \left( f_{n+1} + 4 f_{n} + f_{n-1} \right).$$

This is a $k$-step method with coefficients

$$\begin{aligned}
a_0 & = -1, & a_1 & = 0, & a_2 & = 1, \\
b_0 & = 1/3, & b_1 & = 4/3, & b_2 & = 1/3.
\end{aligned}$$

From this compute the accuracy coefficients

\begin{align}
  d_0 &= a_0 + a_1 + a_2 = 0,\\
  d_1 &= (0 a_0 - b_0) + (a_1 - b_1) + (2 a_2 - b_2) = 0,\\
  d_2 &= (0 a_0 - 0 b_0) + (\frac{1}{2} a_1 - b_1) +(\frac{2^2}{2} a_2 - 2 b_2) = 0,\\
  d_3 &= (0 a_0 - 0 b_0) + (\frac{1}{6} a_1 - \frac{1}{2} b_1) +(\frac{2^3}{6} a_2 - \frac{2^2}{2} b_2) = 0,\\
  d_4 &= (0 a_0 - 0 b_0) + (\frac{1}{24} a_1 - \frac{1}{6} b_1) +(\frac{2^4}{24} a_2 - \frac{2^3}{6} b_2) = 0,\\
  d_5 &= (0 a_0 - 0 b_0) + (\frac{1}{120} a_1 - \frac{1}{24} b_1) + (\frac{2^5}{120} a_2 - \frac{2^4}{24} b_2) \neq 0.
\end{align}

Hence the local truncation error is $\propto h^5$ giving a fourth
order method.

Summary
=======

-   Implicit multistep methods can be used in a predictor-corrector
fashion.

-   Typically the implicit *Adams-Moulton* methods use the explicit
Adams-Bashforth methods as a predictor, but any explicit method
(such as the Euler method) can be used.

-   To check the accuracy of a general $k$-step method we Taylor expand
the appropriate linear functional.

-   The functional gives the error made in taking a single step; its
local truncation error.

-   The coefficients of the linear functional $d_m$ are given by a
simple relation to the coefficients $a_m,b_m$ of the $k$-step
method.


Table of Contents

1  Objectives

2  What Has Calculus Done For You

3  Differentiation

3.1  Common Derivatives

3.2  sympy

4  Integration

4.1  Common Integrals

4.2  sympy

5  The Chain Rule

5.1  Exercise:

6  Partial Differentiation

```python
# conda install -c anaconda sympy
```

> NOTE: `sympy` is not a package that comes included in the `learn-env`. You can install it by running the command below though here in the notebook or in your terminal (with `learn-env` activated). You might have to restart the kernel after installation.

```python
import numpy as np
from matplotlib import pyplot as plt
from sympy import *
from sympy.abc import x, y
from scipy import stats
```

# Objectives

- Perform simple derivatives and indefinite integrals
- Use the Chain Rule to construct derivatives of complex functions
- Construct partial derivatives for functions of multiple variables

# What Has Calculus Done For You

We have already had occasion to use calculus in a few places. Calculus shows us:

- that the mean of a group of numbers is the number $n$ that minimizes the sum of squared differences $\Sigma(p-n)^2$ for each number $p$ in the group;
- that the median of a group of numbers is the number $n$ that minimizes the sum of absolute differences $\Sigma|p-n|$ for each number $p$ in the group;
- how to find the coefficients for a linear regression optimization problem.

The two main tools of calculus are **differentiation** and **integration**. For functions of one dimension:

- Differentiation gives us the *slope* of the function at any point.
- Integration gives us the *area under the curve* of the function between any two points.

Surprisingly, these two operations turn out to be inverses of one another in the sense that the derivative of the integral of a given function takes us back to the initial function:

$\frac{d}{dx}[\int^x_a f(t) dt] = f(x)$. This is known as the First Fundamental Theorem of Calculus.

# Differentiation

To find the slope of a function *at a point*, we imagine calculating the slope of the function between two points, and then gradually bringing those two points together. Consider the slope of the function $y=x^2$ at the point $x=100$.

We'll calculate the slope of the parabola between $x_1=100$ and $x_2=1000$, and then slowly move $x_2$ close to $x_1$:

```python
10**(7/3)
```

```python
X = np.logspace(3, 2, 11)

fig, ax = plt.subplots()
ax.plot(X, X**2, lw=5, c='k', alpha=0.4)
for x_ in X[:-1]:
    ax.plot([100, x_], [10000, x_**2], 'r-.')
```

```python
# This will show how the slope approaches the value
# of the derivative

slopes = []
for x_ in X[:-1]:
    slopes.append((x_**2-10000) / (x_-100))

fig, ax = plt.subplots()
ax.plot(X[:-1], slopes, label='approximation')
ax.scatter(X[-1], 200, label='value of derivative at $x=100$', c='r')
ax.set_xlabel('X')
ax.set_ylabel('slope')
ax.set_title('slope between x=100 and X')
plt.legend();
```

## Common Derivatives

Here is a list of rules for some common derivative patterns.

$\large\frac{d}{dx}[cf(x)] = cf'(x)$

$\rightarrow$ Example: $\frac{d}{dx}[2x] = 2\frac{d}{dx}[x] = (2)(1) = 2$

$\large\frac{d}{dx}[x^n] = nx^{n - 1}$

$\rightarrow$ Example: $\frac{d}{dx}[x^4] = 4x^3$

$\large\frac{d}{dx}[sin(x)] = cos(x)$

$\large\frac{d}{dx}[cos(x)] = -sin(x)$

$\large\frac{d}{dx}[a^x] = a^xln(a)$

$\large\frac{d}{dx}[log_bx)] = \frac{1}{xln(b)}$

$\rightarrow$ Example: $\frac{d}{dx}[ln(x)] = \frac{1}{xln(e)} = \frac{1}{x}$

$\large\frac{d}{dx}[f(x) + g(x)] = f'(x) + g'(x)$

$\rightarrow$ Example: $\frac{d}{dx}[2x + 4x^2] = 2 + 8x$

$\large\frac{d}{dx}[f(x)g(x)] = f(x)g'(x) + g(x)f'(x)$

$\rightarrow$ Example: $\frac{d}{dx}[(x+1)(x-1)] = (x+1)(1) + (x-1)(1) = 2x$

$\large\frac{d}{dx}\left[\frac{f(x)}{g(x)}\right] = \frac{g(x)f'(x) - f(x)g'(x))}{(g(x))^2}$

$\rightarrow$ Example: $\frac{d}{dx}\left[\frac{x+1}{x-1}\right] = \frac{(x-1)(1) - (x+1)(1)}{(x-1)^2} = -\frac{2}{(x-1)^2}$

## `sympy`

The `sympy` package can be helpful:

```python
diff(sin(x), x)
```

```python
diff(exp(2*x))
```

# Integration

Integration is how we calculate the area under a curve. If the curve is a probability density function, then the area under this curve will be equal to 1:

```python
X = np.linspace(0, 5, 51)
for pt in X:
    print(stats.norm.cdf(pt))
```

How do you calculate the area of a shape with a curvy side? Imagine approximating the shape with rectangles, and then imagine making those rectangles narrower and narrower.

Again, let's work with the parabola $y=x^2$ between $x=100$ and $x=1000$:

```python
# This will show how we imagine ever narrower rectangles
# under the curve to approximate the area underneath it.

spacing = np.arange(3, 13)

X = [np.linspace(100, 1000, step) for step in spacing]
X_curve = np.linspace(100, 1000, 10000)

fig, ax = plt.subplots(10, figsize=(10, 30))

for num in spacing:
    ax[num-3].plot(X_curve, X_curve**2)
    for j in range(1, len(X[num-3])-1):
        ax[num-3].hlines(X[num-3][j]**2, X[num-3][j], X[num-3][j+1])
        ax[num-3].vlines(X[num-3][j], 0, X[num-3][j]**2)
    ax[num-3].set_xlabel(f'Area = {900/(num-1) * sum(X[num-3][1:-1]**2)}\n\
    For a=100, b=1000, $\int^b_ax^2=333000000$')
plt.tight_layout()
```

```python
# This will show the area of the rectangles as the number
# of rectangles increases.

spacing_longer = np.arange(3, 100)
X_longer = [np.linspace(100, 1000, step) for step in spacing_longer]

areas = [900 / (num-1) * sum(X_longer[num-3][1:-1]**2) for num in spacing_longer]

fig, ax = plt.subplots()

ax.hlines(333000000, 3, 99, label='333000000', color='r')
ax.plot(spacing_longer, areas, label='approximation')
ax.set_title('Area as a function of number of rectangles')
ax.set_xlabel('Number of rectangles')
ax.set_ylabel('Area')
plt.legend();
```

## Common Integrals

$\large\int cf(x)dx = c\int f(x)dx$

$\large\int x^ndx = \frac{x^{n+1}}{n+1}$

$\large\int sin(x)dx = -cos(x)$

$\large\int cos(x)dx = sin(x)$

$\large\int a^xdx = \frac{a^x}{ln(a)}$

$\large\int (f(x)dx+g(x))dx = \int f(x)dx + \int g(x)dx$

## `sympy`

```python
integrate(cos(x), x)
```

```python
integrate(exp(2*x), x)
```

# The Chain Rule

$\large\frac{d}{dx}[f(g(x))] = f'(g(x))g'(x)$

That is: The derivative of a *composition* of functions is: the derivative of the first applied to the second, multiplied by the derivative of the second.

So if we know e.g. that $\frac{d}{dx}[e^x] = e^x$ and $\frac{d}{dx}[x^2] = 2x$, then we can use the Chain Rule to calculate $\frac{d}{dx}[e^{x^2}]$. We set $f(x) = e^x$ and $g(x) = x^2$, so the derivative must be:

$\large\frac{d}{dx}[e^{x^2}] = (e^{x^2})(2x) = 2xe^{x^2}$.

## Exercise:

Calculate the derivatives for the following compositions:

1. $\frac{d}{dx}[sin(4x)]$

     Answer

    $f(x) = sin(x)$

    $g(x) = 4x$

    So the derivative will be: $cos(4x)*4 = 4cos(4x)$

2. $\frac{d}{dx}[e^{sin(x)}]$

     Answer

    $f(x) = e^x$

    $g(x) = sin(x)$

    So the derivative will be: $e^{sin(x)}*cos(x) = cos(x)e^{sin(x)}$

# Partial Differentiation

Partial differentiation is required for functions of multiple variables. If e.g. I have some function $h = h(a, b)$, then I can consider how $h$ changes with respect to $a$ (while keeping $b$ constant)––that's $\frac{\partial h}{\partial a}$, and I can consider how $h$ changes with respect to $b$ (while keeping $a$ constant)––that's $\frac{\partial h}{\partial b}$. And so the rule is simple enough: If I'm differentiating my function with respect to some variable, I'll **treat all other variables as constants**.

Consider the following function:

$\large\xi(x, y, z) = x^2y^5z^3 - ze^{cos(xy)} + (yz)^3$;

for some parameters $x$, $y$, and $z$.

What are the partial derivatives of this function?

$\large\frac{\partial\xi}{\partial x} = ?$

        Check

    $2xy^5z^3 + yze^{cos(xy)}sin(xy)$

$\large\frac{\partial\xi}{\partial y} = ?$

        Check

    $5x^2y^4z^3 + xze^{cos(xy)}sin(xy) + 3y^2z^3$

$\large\frac{\partial\xi}{\partial z} = ?$

        Check

    $3x^2y^5z^2 - e^{cos(xy)} + 3y^3z^2$


```python
# HIDDEN
from datascience import *
from prob140 import *
import numpy as np
import matplotlib.pyplot as plt
plt.style.use('fivethirtyeight')
%matplotlib inline
import math
from scipy import stats
from sympy import *
init_printing()
```

## Independence ##

Jointly distributed random variables $X$ and $Y$ are *independent* if
$$
P(X \in A, Y \in B) = P(X \in A)P(Y \in B)
$$
for all intervals $A$ and $B$.

Let $X$ have density $f_X$, let $Y$ have density $f_Y$, and suppose $X$ and $Y$ are independent. Then if $f$ is the joint density of $X$ and $Y$,

\begin{align*}
f(x, y)dxdy &\sim P(X \in dx, Y \in dy) \\
&= P(X \in dx)P(Y \in dy) ~~~~~ \text{(independence)} \\
&= f_X(x)dx f_Y(y)dy \\
&= f_X(x)f_Y(y)dxdy
\end{align*}

Thus if $X$ and $Y$ are independent then their joint density is given by

$$
f(x, y) = f_X(x)f_Y(y)
$$

This is the *product rule for densities*: the joint density of two independent random variables is the product of their densities.

### Independent Standard Normal Random Variables ###
Suppose $X$ and $Y$ are i.i.d. standard normal random variables. Then their joint density is given by

$$
f(x, y) = \frac{1}{\sqrt{2\pi}} e^{-\frac{1}{2}x^2} \cdot \frac{1}{\sqrt{2\pi}} e^{-\frac{1}{2}y^2}, ~~~~ -\infty < x, y < \infty
$$

Equivalently,
$$
f(x, y) = \frac{1}{2\pi} e^{-\frac{1}{2}(x^2 + y^2)}, ~~~~ -\infty < x, y < \infty
$$

Here is a graph of the joint density surface.

```python
def indep_standard_normals(x,y):
    return 1/(2*math.pi) * np.exp(-0.5*(x**2 + y**2))

Plot_3d((-4, 4), (-4, 4), indep_standard_normals, rstride=4, cstride=4)
```

Notice the circular symmetry of the surface. This is because the formula for the joint density involves the pair $(x, y)$ through the expression $x^2 + y^2$ which is symmetric in $x$ and $y$.

Notice also that $P(X = Y) = 0$, as the probability is the volume over a line. This is true of all pairs of independent random variables with a joint density: $P(X = Y) = 0$. So for example $P(X > Y) = P(X \ge Y)$. You don't have to worry about whether or not to the inequality should be strict.

### The Larger of Two Independent Exponential Random Variables ###
Let $X$ and $Y$ be independent random variables. Suppose $X$ has the exponential $(\lambda)$ distribution and $Y$ has the exponential $(\mu)$ distribution. The goal of this example is to find $P(Y > X)$.

By the product rule, the joint density of $X$ and $Y$ is given by

$$
f(x, y) ~ = ~ \lambda e^{-\lambda x} \mu e^{-\mu y}, ~~~~ x > 0, ~ y > 0
$$

The graph below shows the joint density surface in the case $\lambda = 0.5$ and $\mu = 0.25$, so that $E(X) = 2$ and $E(Y) = 4$.

```python
def independent_exp(x, y):
    return 0.5 * 0.25 * np.e**(-0.5*x - 0.25*y)

Plot_3d((0, 10), (0, 10), independent_exp)
```

To find $P(Y > X)$ we must integrate the joint density over the upper triangle of the first quadrant, a portion of which is shown below.

```python
# HIDDEN
xx = np.arange(0, 10.1, 0.1)
yy = 10*np.ones(len(xx))
plt.fill_between(xx, xx, yy, alpha=0.3)
plt.axes().set_aspect('equal')
plt.xlabel('$x$')
plt.ylabel('$y$', rotation=0)
plt.title('$Y > X$ (portion of infinite region)');
```

The probability is therefore
$$
P(Y > X) ~ = ~ \int_0^\infty \int_x^\infty \lambda e^{-\lambda x} \mu e^{-\mu y} dy dx
$$

We can do this double integral without much calculus, just by using probability facts.

\begin{align*}
P(Y > X) &= \int_0^\infty \int_x^\infty \lambda e^{-\lambda x} \mu e^{-\mu y} dy dx \\ \\
&= \int_0^\infty \lambda e^{-\lambda x} \big{(} \int_x^\infty \mu e^{-\mu y} dy\big{)} dx \\ \\
&= \int_0^\infty \lambda e^{-\lambda x} e^{-\mu x} dx ~~~~~~ \text{(survival function of } Y\text{, evaluated at } x \text{)} \\ \\
&= \frac{\lambda}{\lambda + \mu} \int_0^\infty (\lambda + \mu) e^{-(\lambda + \mu)x} dx \\ \\
&= \frac{\lambda}{\lambda + \mu} ~~~~~~~ \text{(total integral of exponential }
(\lambda + \mu) \text{ density is 1)}
\end{align*}

Thus
$$
P(Y > X) ~ = ~ \frac{\lambda}{\lambda + \mu}
$$
Analogously,
$$
P(X > Y) ~ = ~ \frac{\mu}{\lambda + \mu}
$$

Notice that the two chances are proportional to the parameters. This is consistent with intuition if you think of $X$ and $Y$ as two lifetimes. If $\lambda$ is large, the corresponding lifetime $X$ is likely to be short, and therefore $Y$ is likely to be larger than $X$ as the formula implies.

If $\lambda = \mu$ then $P(Y > X) = 1/2$ which you can see by symmetry since $P(X = Y) = 0$.

If we had attempted the double integral in the other order – first $x$, then $y$ – we would have had to do more work. The integral is

$$
\int_0^\infty \int_0^y \lambda e^{-\lambda x} \mu e^{-\mu y} dx dy
$$

Let's do it in `SymPy` to check that the answer comes out the same.

Keep in mind that `SymPy` doesn't like to display negative exponents, so some functions appear in a different form compared to the way we usually write them.

```python
declare('x', 'y', 'lamda', 'mu', positive=True)

f_X = lamda * exp(-lamda * x)

f_Y = mu * exp(-mu * y)

jt_density = f_X * f_Y
jt_density
```

```python
p_Y_greater_than_X = Integral(jt_density, (x, 0, y), (y, 0, oo))
p_Y_greater_than_X
```

```python
p_Y_greater_than_X.doit()
```

That looks strange but it is equal to

$$
1 - \frac{\mu}{\lambda + \mu} ~ = ~ \frac{\lambda}{\lambda + \mu}
$$

which is the same as the answer we got earlier.

```python

```