## QCamp Training Session
# Advanced Simulation

* Christopher J. Wood
* IBM TJ Watson Research Center, Yorktown Heights, NY, USA
* February 26, 2019

## How do we simulate NISQ devices using classical computers?

### Outline

1. Ideal quantum circuits
1. Ideal simulations with Qiskit Aer
1. Noisy quantum circuits
1. Noisy simulations with Qiskit Aer
1. Noise model examples

```python
# Import the basics
import numpy as np

import matplotlib.pyplot as plt
import qiskit
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit.tools import visualization as qplt
```

# Ideal Quantum Circuits

The ideal circuit based model of quantum computation consists of three pieces:
1. **States:** A *complex statevector* $|\psi\rangle$ satisfying $|\langle\psi|\psi\rangle|^2=1$
1. **Gates:** A *unitary* matrix $U$ satisfying $U^\dagger U = U U^\dagger = \mathbb{1}$.
1. **Measurements:** A *projective-valued measure (PVM)* $\{\Pi_j\}$ s.t. $p_j = |\langle \psi |\Pi_j|\psi\rangle|^2$

## Ideal Quantum States

Quantum states my be expressed as a complex vector with respect to the computation basis:

Eg for a single qubit: $|\psi\rangle = \alpha |0\rangle + \beta |1\rangle = [\alpha, \beta]$

```python
psi = np.array([1, 1j]) / np.sqrt(2)

qplt.plot_bloch_multivector(psi)
```

### How do we store a quantum state (classically)?

* Storing a quantum state requies exponential classical resources

* A general quantum statevector is $2^n$ complex floating point numbers

* $n$-qubits = $16 * 2^n$ bytes of memory

To put that in perfective:

```python
nqubits = [16,20,24,28,32,36]
plt.semilogy(nqubits, [1, 16, 256, 4096, 65536, 1048576])
plt.title('n-qubit statevector')
plt.xlabel('Number of qubits')
plt.ylabel('Required Memory')
plt.xticks(nqubits)
plt.yticks((1, 2 ** 10, 2 ** 20), ('1MB', '1GB', '1TB'))
plt.show()
```

## Ideal Quantum Gates

Ideal quantum gates are operations that map statevectors to statevectors:

$ |\psi_{i+1}\rangle = U_i |\psi_i\rangle $

These operations may be represented by *unitary matrices*.

* A matrix $U$ is unitary if and only if $U^\dagger U = U^\dagger U = \mathbb{1}$

### How do we apply unitaries in simulation?

* Full specifying an n-qubit unitary matrix requires $16 * 4^n$ complex floating point numbers

* Scaling is twice that of a statevector ($n$-qubit unitary same memory as $2n$-qubit vector)

Generally for simulation we never store the full unitary: *A quantum circuit is a decomposition of a unitary into smaller (typically 1 and 2-qubit) operations.*

```python
nqubits = [8, 10, 12, 14, 16, 18]
plt.semilogy(nqubits, [1, 16, 256, 4096, 65536, 1048576])
plt.title('n-qubit unitary matrix')
plt.xlabel('Number of qubits')
plt.ylabel('Required Memory')
plt.xticks(nqubits)
plt.yticks((1, 2 ** 10, 2 ** 20), ('1MB', '1GB', '1TB'))
plt.show()
```

## Ideal Measurement

* In simulation we can direct access to the wavefunction, however in experiment we can only perform measurements to extract information.

* An ideal measurement is a non-unitary operation which causes the wavefunction to *collapse*

* We will consider measurement in the computational ($Z$) basis:

If we measure a state $|\psi\rangle = \alpha_0 |0\rangle + \alpha_1 |1\rangle$

For a single measurement:
* We will observe outcome "0" with probability $p_0 = |\alpha_0|^2$,
* After observing "0" the system will have collapsed to the state $|0\rangle$.
* Similarly outcome "1" will be observed with probability $p_1 = |\alpha_1|^2$, leaving the system in state $|1\rangle$.

# Ideal Simulations with Qiskit Aer

* Qiskit Aer is a local simulation provider for Qiskit.
* We will briefly show how to do ideal simulations with Qiskit-Aer.
* These are taken from the Aer Provider tutorial in `qiskit-tutorials`.

## Qiskit Aer simulator backends

 Qiskit Aer currently includes three simulator backends

* `QasmSimulator`: Simulates experiments (returns counts)
* `StatevectorSimulator`: Simulates an ideal shot of an experiment (returns statevector)
* `UnitarySimulator`: Simulates an ideal circuit (return circuit unitary)

These backends are found in the `Aer` provider with the names `qasm_simulstor`, `statevector_simulator` and `unitary_simulator` respectively

```python
from qiskit import Aer
import qiskit.providers.aer as aer
from qiskit.providers.aer import QasmSimulator, StatevectorSimulator, UnitarySimulator
```

## QasmSimulator

* Designed to mimic an actual device.
* Executes `QuantumCircuit` for $N$ shots
* Returns count dictionary containing $N$ final vlaues of `ClassicalRegisters` in the circuit.
* The circuit may contain *gates*, *measure*, *reset*, *conditionals*

### Example: prepare and measure a Bell-state

* Circuit prepares a 2-qubit Bell-state $|\psi\rangle = \frac{1}{2}(|0,0\rangle + |1,1 \rangle)$
* Measures both qubits

```python
# Construct quantum circuit
qr = QuantumRegister(2, 'qr')
cr = ClassicalRegister(2, 'cr')
circ = QuantumCircuit(qr, cr)
circ.h(qr[0])
circ.cx(qr[0], qr[1])
circ.measure(qr, cr)

# Plot the circuit
print(circ.draw())
```

             ┌───┐     ┌─┐
    qr_0: |0>┤ H ├──■──┤M├───
             └───┘┌─┴─┐└╥┘┌─┐
    qr_1: |0>─────┤ X ├─╫─┤M├
                  └───┘ ║ └╥┘
     cr_0: 0 ═══════════╩══╬═
                           ║
     cr_1: 0 ══════════════╩═

```python
# Select the QasmSimulator from the Aer provider
simulator = Aer.get_backend('qasm_simulator')

# Execute and get counts
result = qiskit.execute(circ, simulator).result()
counts = result.get_counts(circ)
qplt.plot_histogram(counts, title='Bell-State counts')
```

### Returning measurements outcomes for each shot

* `QasmSimulator` also supports returning a list of outcomes for each shot.
* Enabled by setting the keyword argument `memory=True` in `compile` or `execute`

```python
# Execute and get memory
result = qiskit.execute(circ, simulator, shots=10, memory=True).result()
memory = result.get_memory(circ)
print(memory)
```

    ['00', '00', '11', '00', '11', '11', '00', '00', '00', '00']

### Starting simulation with a custom initial state

* The default initial state for `QasmSimulator` is $|\psi_0\rangle = |0^{\otimes n}\rangle$.
* `QasmSimulator` allows setting a custom initial statevector for the simulation.
* Done using `backend_options={"initial_statevector": psi_0}` in `QasmSimulator.run` or `qiskit.execute`.

```python
# Set the initial state
opts = {"initial_statevector": np.array([1, 0, 0, 1] / np.sqrt(2))}

# Select the QasmSimulator from the Aer provider
simulator = Aer.get_backend('qasm_simulator')

# Execute and get counts
result = qiskit.execute(circ, simulator, backend_options=opts).result()
counts = result.get_counts(circ)
qplt.plot_histogram(counts, title="Bell initial statevector")
```

### Requirements for initial statevector

* $|\psi_0\rangle$ must be valid quantum state: $|\langle\psi_0|\psi_0\rangle|=1$.
* If not an error will be raised.
* The same $\psi_0\rangle$ will be used for *all* circuits in Qobj.
* If it is wrong size for number of qubits will raise an error

## Statevector Simulator

* Simulates a single-shot of a `QuantumCircuit` and returns the final quantum statevector.
* The circuit may contain *gates*, and also *measure*, *reset*, and *conditional* operations.
* If measure/reset operations are in the circuit:
  * The final state will be a *conditional state* for the outcome of those operations.
  * The measurement outcome (counts) will also be returned (with $N=1$ shots).

* A custom initial statevector $|\psi_0\rangle$ can be used (same as for `QasmSimulator`)

### Example: Bell state preparation

```python
# Construct quantum circuit without measure
qr = QuantumRegister(2, 'qr')
circ = QuantumCircuit(qr)
circ.h(qr[0])
circ.cx(qr[0], qr[1])

# Plot the circuit
print(circ.draw())
```

             ┌───┐
    qr_0: |0>┤ H ├──■──
             └───┘┌─┴─┐
    qr_1: |0>─────┤ X ├
                  └───┘

```python
# Select the StatevectorSimulator from the Aer provider
simulator = Aer.get_backend('statevector_simulator')

# Execute and get counts
result = qiskit.execute(circ, simulator).result()
statevector = result.get_statevector(circ)
qplt.plot_state_city(statevector, title='Bell state')
```

### Example: Bell state preparation *with measurement*

```python
# Construct quantum circuit with measure
qr = QuantumRegister(2, 'qr')
cr = ClassicalRegister(2, 'cr')
circ = QuantumCircuit(qr, cr)
circ.h(qr[0])
circ.cx(qr[0], qr[1])
circ.measure(qr, cr)

# Select the StatevectorSimulator from the Aer provider
simulator = Aer.get_backend('statevector_simulator')

# Execute and get counts
result = qiskit.execute(circ, simulator).result()
statevector = result.get_statevector(circ)
qplt.plot_state_city(statevector, title='Bell state post-measurement')
```

## Unitary Simulator

* Constructs the full $n$-qubit unitary matrix for a `QuantumCircuit`.
* Done by applying each gate matrix to an initial identity matrix.
* Circuit may only contain *gates*

### Example: Bell-state preparation circuit

```python
# Construct an empty quantum circuit
qr = QuantumRegister(2)
circ = QuantumCircuit(qr)
circ.h(qr[0])
circ.cx(qr[0], qr[1])

# Select the UnitarySimulator from the Aer provider
simulator = Aer.get_backend('unitary_simulator')

# Execute and get counts
result = qiskit.execute(circ, simulator).result()
unitary = result.get_unitary(circ)
print("Final Unitary:")
print(unitary)
```

    Final Unitary:
    [[ 0.70710678+0.j  0.70710678+0.j  0.        +0.j  0.        +0.j]
     [ 0.        +0.j  0.        +0.j  0.70710678+0.j -0.70710678+0.j]
     [ 0.        +0.j  0.        +0.j  0.70710678+0.j  0.70710678+0.j]
     [ 0.70710678+0.j -0.70710678+0.j  0.        +0.j  0.        +0.j]]

### Custom initial unitary

* Instead of the identity matrix, a custom initial unitary can be used
* `backend_options={"initial_unitary": U0}`

### Requirements for initial unitary

* $U_0$ must be unitary ($U_0^\dagger U_0 = \mathbb{1}$).
* The same $U_0$ will be used for *all* circuits in Qobj.
* If these are violated an error will be raised

# Noisy Quantum Circuits

A noisy quantum circuit is described using formalism of *Open Quantum Systems*. For the circuit model noisy quantum computation consists of three pieces:

1. **States:** A density matrix $\rho$, satisfying $\rho \ge 0$, $Tr[\rho]=1$.
1. **Gates:** A *completely-positive trace-preserving* (CPTP) map $\cal{E}$.
1. **Measurement:** A *positive operator valued measure* (POVM) $\{E_j\}$ s.t. $p_j = Tr[E_j\rho]$

## Noisy Quantum States

* The density matrix $\rho$ can be thought of our *average knowledge* about a state averaged over many experiments.

### Example: Noisy initialization

* We have a device that is supposed to generate a single-qubit state $|0\rangle$.
* 10% of the time it generates the state $|1\rangle$ instead.
* We describe the *average output* of this noisy process as a state:

$$\rho = \frac{9}{10} * |0\rangle\langle0| + \frac{1}{10} * |1\rangle\langle1|$$

```python
rho = 0.9 * np.array([[1, 0], [0, 0]]) + 0.1 * np.array([[0, 0,], [0, 1]])
qplt.plot_bloch_multivector(rho)
```

```python
qplt.plot_state_city(rho)
```

## Noisy Quantum Gates

Noisy quantum gates (CPTP-maps) a *roughly speaking* operations which map density matrices to density matrices. Thus, like density matrices themselves they represent our *average knowledge* about evolution of a system (under certain assumptions).

They may always be written in the **Kraus representation** as

$$\mathcal{E}(\rho) = \sum_j K_j \rho K_j^\dagger, \quad\mbox{where}\quad \sum_j K_j^\dagger K_j = \mathbb{1}$$

The set of matrices $\{K_j\}$ *non-uniquely* defines a noisy quantum gate.

A *unique* representation of a CPTP map is the linear *superoperator* $S$, which is the matrix that acts on *vectorized* density matrices:

$$|\mathcal{E}\rangle\!\rangle = S|\rho\rangle\!\rangle$$

In terms of the *Kraus* representation the superoprator is given by $S = \sum_j K_j^*\otimes K_j$ and is a $4^n * 4^n$ matrix.

### Example: Noisy X gate

* Suppose we have a device that is supposed to apply an $X$ gate to a qubit.
* 10% of the time it does nothing (an identity gate)
* We describe the *average output* of this noisy process on an *arbitrary* state as the CPTP map:

$$\mathcal{E}(\rho) = \frac{9}{10}\, X\rho X + \frac{1}{10}\, \rho$$

* In terms of a Kraus representation this is $\{\sqrt{9/10}\, X, \sqrt{1/10}\, I\}$
* **NOTE:** This is a special case of a CPTP map called a *mixed unitary channel*, where all the Kraus operators are a unitary matrix applied with some probability

### How do we apply CPTP maps in simulation?

* If we want to do a density matrix simulation we require $16 * 4^n$ complex floating point numbers to store the state itself
* We require an additional $16 * 16^m$ complex floats for each $m$-qubit superoperator to apply.
* This greatly limits the size of systems we can simulate

### Alternative approach: quantum trajectories

* We can mimic CPTP evolution by *unravelling* noisy gates into (possibly non-unitary) operations that can be applied to a pure quantum state
* We must rescale the statevector after such a gate to preserve normalization
* We recover the average dynamics of the map by *averaging* over the output of many shots
* **NOTE:** This trades of memory requirement for simulation time (less RAM more shots)

### Quantum Trajectory state update

* Let the system be in the pure state $|psi\rangle$
* Consider a Kraus gate error $\{K_j\}$
* Compute probability of applying Kraus operator $j$ to the state from:
$$ p_j = |\langle\psi|K_j^\dagger K_j\psi\rangle|^2 $$
* Sample a single Kraus operator to apply from the discrete distribution give by $\{p_j\}$
* Update the state to:
$$|\psi^\prime\rangle = \frac{1}{\sqrt{p_j}} K_j|\psi\rangle$$

### Important: Mixed unitary errors

* This can be done more efficiently for mixed unitary error channels
* *The probabilities are known ahead of time and are independent of the current state*
* In this case for each noisy operation we can sample the noisy unitary to apply in advance and insert it into the circuit. Then apply the noisy circuits to the initial state and average over the output

## Noisy Measurement

* Noisy measurement is represented by a POVM $\{E_j\}$, where $E_j \ge 0$, $\sum_j E_j = \mathbb{1}$.

* Measurement outcome probability is given by $p_j = Tr[E_j\rho]$.

* For simulation purposes we consider *two types* of noisy measurement.

  1. **Quantum measurement error:** The error can be expressed as a CPTP map *before* an ideal measurement.
  2. **Classical readout error:** The error is expressed as a stochastic map that applies to the *classical register* recording the measurement outcome *after measurment*.

### Classical Readout Error

* Readout errors are specified by a list of assignment probabilities vectors $P(A|B)$.
  * $A$ is the *recorded* classical bit value
  * $B$ is the *true* bit value returned from the measurement

Eg for 1 qubits: $ P(A|B) = [P(A|0), P(A|1)]$

# Noisy Simulations with Qiskit Aer

* We will demonstrate how noisy simulations may be done in Qiskit-Aer
* The `QasmSimulator` supports a trajector based noise simulation

## Qiskit Aer Noise Module

* The `aer.noise` module contains Python classes to build customized noise models for simulation.
* Three key classes:

  1. The `NoiseModel` class which stores a noise model used for noisy simulation
  2. The `QuantumError` class which describes CPTP gate errors. These can be applied
    * After *gate* or *reset* instructions
    * Before *measure* instructions.
  3. The `ReadoutError` class which describes classical readout errors.
* Functions to generate common `QuantumError` objects in `aer.noise.errors`.
* Functions to automatically generate a noise model in `aer.noise.device`

### Importing necessary functions

```python
# Import the noise model object
from qiskit.providers.aer.noise import NoiseModel

# Import all standard errors
from qiskit.providers.aer.noise.errors import *

# Import noise base clases
from qiskit.providers.aer.noise.errors.quantum_error import QuantumError
from qiskit.providers.aer.noise.errors.readout_error import ReadoutError
```

## Standard Quantum Errors

Rather than deal with the `QuantumError` object directly, many helper functions exist to automatically generate a specific type of parameterized quantum error. These are contained in the `noise.errors` submodule and include functions for many common errors types used in quantum computing research. The function names and the type of error they return are:

| Standard error function | Details |
| --- | --- |
| `kraus_error` | a general n-qubit CPTP error channel given as a list of Kraus matrices $[K_0, ...]$. |
| `mixed_unitary_error` | an n-qubit mixed unitary error given as a list of unitary matrices and probabilities $[(U_0, p_0),...]$. |
| `coherent_unitary_error` | an n-qubit coherent unitary error given as a single unitary matrix $U$. |
| `pauli_error` | an n-qubit Pauli error channel (mixed unitary) given as a list of Pauli's and probabilities $[(P_0, p_0),...]$ |
| `depolarizing_error` | an n-qubit depolarizing error channel parameterized by a depolarizion probability $p$. |
| `reset_error` | a single-qubit reset error parameterized by a probabilities $p_0, p_1$ of reseting to the $|0\rangle$, $|1\rangle$ state.|
| `thermal_relaxation_error` | a single qubit thermal relaxation channel parameterized by relaxation time constants $T_1$, $T_2$, gate time $t$, and excited state thermal population $p_1$. |
| `phase_amplitude_damping_error` | A single-qubit generalized combined phase and amplitude damping error channel given by an amplitude damping parameter $\lambda$, a phase damping parameter $\gamma$, and an excited state thermal population $p_1$. |
| `amplitude_damping_error` |  A single-qubit generalized amplitude damping error channel given by an amplitude damping parameter $\lambda$, and an excited state thermal population $p_1$. |
| `phase_damping_error` |  A single-qubit phase damping error channel given by a phase damping parameter $\gamma$ |

### Combinging Errors

`QuantumError` instances can be combined to produce new Quantum Errors using:
 * Composition: $\cal{E}=\cal{E_1}\circ\cal{E_2}$ as `error1.compose(error2)`
 * Tensor product: $\cal{E}=\cal{E_1}\otimes\cal{E_2}$ as `error1.kron(error2)`

### Example: Depolarizing errors

For example to construct a 5% single-qubit depolarizing error:

```python
# Construct a 1-qubit depolarizing error
depol1 = depolarizing_error(0.05, 1)
print(depol1)
```

    QuantumError on 1 qubits. Noise circuits:
      P(0) = 0.0125, QobjInstructions = [[{'name': 'x', 'qubits': [0]}]
      P(1) = 0.0125, QobjInstructions = [[{'name': 'y', 'qubits': [0]}]
      P(2) = 0.0125, QobjInstructions = [[{'name': 'z', 'qubits': [0]}]
      P(3) = 0.9625, QobjInstructions = [[{'name': 'id', 'qubits': [0]}]

### Composition

```python
depol1sq = depol1.compose(depol1)
print(depol1sq)
```

    QuantumError on 1 qubits. Noise circuits:
      P(0) = 0.00015625000000000003, QobjInstructions = [[{'name': 'x', 'qubits': [0]}, {'name': 'x', 'qubits': [0]}]
      P(1) = 0.00015625000000000003, QobjInstructions = [[{'name': 'x', 'qubits': [0]}, {'name': 'y', 'qubits': [0]}]
      P(2) = 0.00015625000000000003, QobjInstructions = [[{'name': 'x', 'qubits': [0]}, {'name': 'z', 'qubits': [0]}]
      P(3) = 0.01203125, QobjInstructions = [[{'name': 'x', 'qubits': [0]}, {'name': 'id', 'qubits': [0]}]
      P(4) = 0.00015625000000000003, QobjInstructions = [[{'name': 'y', 'qubits': [0]}, {'name': 'x', 'qubits': [0]}]
      P(5) = 0.00015625000000000003, QobjInstructions = [[{'name': 'y', 'qubits': [0]}, {'name': 'y', 'qubits': [0]}]
      P(6) = 0.00015625000000000003, QobjInstructions = [[{'name': 'y', 'qubits': [0]}, {'name': 'z', 'qubits': [0]}]
      P(7) = 0.01203125, QobjInstructions = [[{'name': 'y', 'qubits': [0]}, {'name': 'id', 'qubits': [0]}]
      P(8) = 0.00015625000000000003, QobjInstructions = [[{'name': 'z', 'qubits': [0]}, {'name': 'x', 'qubits': [0]}]
      P(9) = 0.00015625000000000003, QobjInstructions = [[{'name': 'z', 'qubits': [0]}, {'name': 'y', 'qubits': [0]}]
      P(10) = 0.00015625000000000003, QobjInstructions = [[{'name': 'z', 'qubits': [0]}, {'name': 'z', 'qubits': [0]}]
      P(11) = 0.01203125, QobjInstructions = [[{'name': 'z', 'qubits': [0]}, {'name': 'id', 'qubits': [0]}]
      P(12) = 0.01203125, QobjInstructions = [[{'name': 'id', 'qubits': [0]}, {'name': 'x', 'qubits': [0]}]
      P(13) = 0.01203125, QobjInstructions = [[{'name': 'id', 'qubits': [0]}, {'name': 'y', 'qubits': [0]}]
      P(14) = 0.01203125, QobjInstructions = [[{'name': 'id', 'qubits': [0]}, {'name': 'z', 'qubits': [0]}]
      P(15) = 0.9264062500000001, QobjInstructions = [[{'name': 'id', 'qubits': [0]}, {'name': 'id', 'qubits': [0]}]

### Tensor Product

```python
depol2 = depol1.kron(depol1)
print(depol2)
```

    QuantumError on 2 qubits. Noise circuits:
      P(0) = 0.00015625000000000003, QobjInstructions = [[{'name': 'x', 'qubits': [0]}, {'name': 'x', 'qubits': [1]}]
      P(1) = 0.00015625000000000003, QobjInstructions = [[{'name': 'y', 'qubits': [0]}, {'name': 'x', 'qubits': [1]}]
      P(2) = 0.00015625000000000003, QobjInstructions = [[{'name': 'z', 'qubits': [0]}, {'name': 'x', 'qubits': [1]}]
      P(3) = 0.01203125, QobjInstructions = [[{'name': 'id', 'qubits': [0]}, {'name': 'x', 'qubits': [1]}]
      P(4) = 0.00015625000000000003, QobjInstructions = [[{'name': 'x', 'qubits': [0]}, {'name': 'y', 'qubits': [1]}]
      P(5) = 0.00015625000000000003, QobjInstructions = [[{'name': 'y', 'qubits': [0]}, {'name': 'y', 'qubits': [1]}]
      P(6) = 0.00015625000000000003, QobjInstructions = [[{'name': 'z', 'qubits': [0]}, {'name': 'y', 'qubits': [1]}]
      P(7) = 0.01203125, QobjInstructions = [[{'name': 'id', 'qubits': [0]}, {'name': 'y', 'qubits': [1]}]
      P(8) = 0.00015625000000000003, QobjInstructions = [[{'name': 'x', 'qubits': [0]}, {'name': 'z', 'qubits': [1]}]
      P(9) = 0.00015625000000000003, QobjInstructions = [[{'name': 'y', 'qubits': [0]}, {'name': 'z', 'qubits': [1]}]
      P(10) = 0.00015625000000000003, QobjInstructions = [[{'name': 'z', 'qubits': [0]}, {'name': 'z', 'qubits': [1]}]
      P(11) = 0.01203125, QobjInstructions = [[{'name': 'id', 'qubits': [0]}, {'name': 'z', 'qubits': [1]}]
      P(12) = 0.01203125, QobjInstructions = [[{'name': 'x', 'qubits': [0]}, {'name': 'id', 'qubits': [1]}]
      P(13) = 0.01203125, QobjInstructions = [[{'name': 'y', 'qubits': [0]}, {'name': 'id', 'qubits': [1]}]
      P(14) = 0.01203125, QobjInstructions = [[{'name': 'z', 'qubits': [0]}, {'name': 'id', 'qubits': [1]}]
      P(15) = 0.9264062500000001, QobjInstructions = [[{'name': 'id', 'qubits': [0]}, {'name': 'id', 'qubits': [1]}]

## Adding errors to a Noise Model

When adding a quantum error to a noise model we must specify the type of *instruction* that it acts on, and what qubits to apply it to. There are three cases for Quantum Errors:

 1. All-qubit quantum error
 2. Specific qubit quantum error
 3. Non-local quantum error

### All-qubit quantum error

This applies the same error to any occurence of an instruciton, regardless of which qubits it acts on.

It is added as `noise_model.add_all_qubit_quantum_error(error, instructions)`:

```python
# Create an empty noise model
noise_model = NoiseModel()

# Add depolarizing error to all single qubit u1, u2, u3 gates
noise_model.add_all_qubit_quantum_error(depol1, ['u1', 'u2', 'u3'])

# Print noise model info
print(noise_model)
```

    NoiseModel:
      Instructions with noise: ['u2', 'u3', 'u1']
      All-qubits errors: ['u1', 'u2', 'u3']

### Specific qubit quantum error

This applies the error to any occurence of an instruction acting on a specified list of qubits. Note that the order of the qubit matters: For a 2-qubit gate an error applied to qubits [0, 1] is different to one applied to qubits [1, 0] for example.

It is added as `noise_model.add_quantum_error(error, instructions, qubits)`:

```python
# Create an empty noise model
noise_model = NoiseModel()

# Add depolarizing error to all single qubit u1, u2, u3 gates on qubit 0 only
noise_model.add_quantum_error(depol1, ['u1', 'u2', 'u3'], [0])

# Print noise model info
print(noise_model)
```

    NoiseModel:
      Instructions with noise: ['u2', 'u3', 'u1']
      Specific qubit errors: [('u1', [0]), ('u2', [0]), ('u3', [0])]

### Non-local qubit quantum error

This applies an error to a specific set of noise qubits after any occurence of an instruction acting on a specific of gate qubits.

It is added as `noise_model.add_quantum_error(error, instructions, instr_qubits, error_qubits)`:

```python
# Create an empty noise model
noise_model = NoiseModel()

# Add depolarizing error on qubit 2 forall single qubit u1, u2, u3 gates on qubit 0
noise_model.add_nonlocal_quantum_error(depol1, ['u1', 'u2', 'u3'], [0], [2])

# Print noise model info
print(noise_model)
```

    NoiseModel:
      Instructions with noise: ['u2', 'u3', 'u1']
      Non-local specific qubit errors: [('u1', [0], [2]), ('u2', [0], [2]), ('u3', [0], [2])]

### Executing a noisy simulation with a noise model

* To execute a noisy simulation we pass the noise model object to `QasmSimulator.run` or `execute` using the `noise_model` kwarg.
* Eg: `qiskit.execute(circuits, QasmSimulator(), noise_model=noise)`

**Important:** *When running a noisy simulation make sure you compile your qobj to the same basis gates as the noise model!*

This can be done using `NoiseModel.basis_gates`

# Noise Model Examples

We will now give some examples of noise models

### Test circuit

* For our demonstrations we wil use a simple test circuit generating a n-qubit GHZ state:

```python
# Simulator
simulator = QasmSimulator()

# System Specification
n_qubits = 4
qr = QuantumRegister(n_qubits)
cr = ClassicalRegister(n_qubits)
circ = QuantumCircuit(qr, cr)

# Test Circuit
circ.h(qr[0])
for qubit in range(n_qubits - 1):
    circ.cx(qr[qubit], qr[qubit + 1])
circ.measure(qr, cr)
print(circ.draw())
```

             ┌───┐               ┌─┐
    q1_0: |0>┤ H ├──■────────────┤M├─────────
             └───┘┌─┴─┐          └╥┘┌─┐
    q1_1: |0>─────┤ X ├──■────────╫─┤M├──────
                  └───┘┌─┴─┐      ║ └╥┘┌─┐
    q1_2: |0>──────────┤ X ├──■───╫──╫─┤M├───
                       └───┘┌─┴─┐ ║  ║ └╥┘┌─┐
    q1_3: |0>───────────────┤ X ├─╫──╫──╫─┤M├
                            └───┘ ║  ║  ║ └╥┘
     c0_0: 0 ═════════════════════╩══╬══╬══╬═
                                     ║  ║  ║
     c0_1: 0 ════════════════════════╩══╬══╬═
                                        ║  ║
     c0_2: 0 ═══════════════════════════╩══╬═
                                           ║
     c0_3: 0 ══════════════════════════════╩═

```python
# Test execution
result = qiskit.execute(circ, simulator).result()
qplt.plot_histogram(result.get_counts(0))
```

# Basic Error Model Examples

## Example 1: Basic bit-flip error noise model

Lets consider a simple toy noise model example common in quantum information theory research:

* When applying a single qubit gate, flip the state of the qubit with probability `p_gate1.
* When applying a 2-qubit gate apply single-qubit errors to each qubit.
* When reseting a qubit reset to 1 instead of 0 with probability `p_reset`
* When measuring a qubit, flip the state of the qubit before with probability `p_meas`.

```python
# Example error probabilities
p_reset = 0.03
p_meas = 0.1
p_gate1 = 0.05

# QuantumError objects
error_reset = pauli_error([('X', p_reset), ('I', 1 - p_reset)])
error_meas = pauli_error([('X',p_meas), ('I', 1 - p_meas)])
error_gate1 = pauli_error([('X',p_gate1), ('I', 1 - p_gate1)])
error_gate2 = error_gate1.kron(error_gate1)

# Add errors to noise model
noise_bit_flip = NoiseModel()
noise_bit_flip.add_all_qubit_quantum_error(error_reset, "reset")
noise_bit_flip.add_all_qubit_quantum_error(error_meas, "measure")
noise_bit_flip.add_all_qubit_quantum_error(error_gate1, ["u1", "u2", "u3"])
noise_bit_flip.add_all_qubit_quantum_error(error_gate2, ["cx"])

print(noise_bit_flip)
```

    NoiseModel:
      Instructions with noise: ['measure', 'u1', 'reset', 'u2', 'cx', 'u3']
      All-qubits errors: ['reset', 'measure', 'u1', 'u2', 'u3', 'cx']

### Executing the noisy simulation

```python
# Compile previous GHZ circuit using noise model basis gates
qobj = qiskit.compile(circ, simulator,
               basis_gates=noise_bit_flip.basis_gates)

# Run the noisy simulation
result_bit_flip = simulator.run(qobj, noise_model=noise_bit_flip).result()
counts_bit_flip = result_bit_flip.get_counts(0)

# Plot noisy output
qplt.plot_histogram(counts_bit_flip)
```

## Example 2: T1/T2 thermal relaxation

* Now consider a more realistic error model based on thermal relaxation with the qubit environment
* Each qubit parameterized by a thermal relaxation time constant $T_1$ and a dephasing time constant $T_2$.
* Note that we must have $T_2 \le 2 T_1$
* Error rates on instructions are determined by gate time and qubit $T_1$, $T_2$ values

```python
# T1 and T2 values for qubits 0-3
T1s = np.random.normal(50e3, 10e3, 4) # Sampled from normal distribution mean 50 microsec
T2s = np.random.normal(70e3, 10e3, 4)  # Sampled from normal distribution mean 50 microsec

# Truncate random T2s <= T1s
T2s = np.array([min(T2s[j], 2 * T1s[j]) for j in range(4)])

# Instruction times (in nanoseconds)
time_u1 = 0   # virtual gate
time_u2 = 50  # (single X90 pulse)
time_u3 = 100 # (two X90 pulses)
time_cx = 300
time_reset = 1000  # 1 microsecond
time_measure = 1000 # 1 microsecond

# QuantumError objects
errors_reset = [thermal_relaxation_error(t1, t2, time_reset)
                for t1, t2 in zip(T1s, T2s)]
errors_measure = [thermal_relaxation_error(t1, t2, time_measure)
                  for t1, t2 in zip(T1s, T2s)]
errors_u1  = [thermal_relaxation_error(t1, t2, time_u1)
              for t1, t2 in zip(T1s, T2s)]
errors_u2  = [thermal_relaxation_error(t1, t2, time_u2)
              for t1, t2 in zip(T1s, T2s)]
errors_u3  = [thermal_relaxation_error(t1, t2, time_u3)
              for t1, t2 in zip(T1s, T2s)]
errors_cx = [[thermal_relaxation_error(t1a, t2a, time_cx).kron(
             thermal_relaxation_error(t1b, t2b, time_cx))
              for t1a, t2a in zip(T1s, T2s)]
               for t1b, t2b in zip(T1s, T2s)]

# Add errors to noise model
noise_thermal = NoiseModel()
for j in range(4):
    noise_thermal.add_quantum_error(errors_reset[j], "reset", [j])
    noise_thermal.add_quantum_error(errors_measure[j], "measure", [j])
    noise_thermal.add_quantum_error(errors_u1[j], "u1", [j])
    noise_thermal.add_quantum_error(errors_u2[j], "u2", [j])
    noise_thermal.add_quantum_error(errors_u3[j], "u3", [j])
    for k in range(4):
        noise_thermal.add_quantum_error(errors_cx[j][k], "cx", [j, k])

print(noise_thermal)
```

    NoiseModel:
      Instructions with noise: ['measure', 'reset', 'u2', 'cx', 'u3']
      Specific qubit errors: [('reset', [0]), ('reset', [1]), ('reset', [2]), ('reset', [3]), ('measure', [0]), ('measure', [1]), ('measure', [2]), ('measure', [3]), ('u2', [0]), ('u2', [1]), ('u2', [2]), ('u2', [3]), ('u3', [0]), ('u3', [1]), ('u3', [2]), ('u3', [3]), ('cx', [0, 0]), ('cx', [0, 1]), ('cx', [0, 2]), ('cx', [0, 3]), ('cx', [1, 0]), ('cx', [1, 1]), ('cx', [1, 2]), ('cx', [1, 3]), ('cx', [2, 0]), ('cx', [2, 1]), ('cx', [2, 2]), ('cx', [2, 3]), ('cx', [3, 0]), ('cx', [3, 1]), ('cx', [3, 2]), ('cx', [3, 3])]

### Executing the noisy simulation

```python
# Compile previous GHZ circuit using noise model basis gates
qobj = qiskit.compile(circ, simulator,
                      basis_gates=noise_thermal.basis_gates)

# Run the noisy simulation
result_thermal = simulator.run(qobj, noise_model=noise_thermal).result()
counts_thermal = result_thermal.get_counts(0)

# Plot noisy output
qplt.plot_histogram(counts_thermal)
```

# Intermediate Error Model Examples

We will now show how one can simulate more complicated error processes using the Qiskit-Aer noise model

## Example 3: Correlated spectator qubit noise

* For our second example we will consider correlated spectator qubit noise on the CX gates in our test circuit.

* Let us consider the case where our 4 qubits are arrange on the verticies of a square.
* Apply a CNOT gate with control $c$ and target $t$.
* With probability $p$ there is a of a correlated $ZZ$ error between $t$ and spectator qubit $s$ coupled to the control:

```
c -- t                     I -- I              I -- Z
|    |  --> (1-p) * CNOT * |    | + p * CNOT * |    |
s -- x                     I -- I              Z -- I
```

```python
# Define an example coupling map for the square
coupling_map = [[0, 1], [0, 3], [2, 1], [2, 3]]

# Example error probabilities
p_01 = 0.03
p_03 = 0.01
p_21 = 0.02
p_23 = 0.05

# QuantumError objects
def zz_error(p):
    return pauli_error([('ZZ', p), ('II', 1 - p)])

# Add errors to noise model
noise_corr_cx = NoiseModel()
noise_corr_cx.add_nonlocal_quantum_error(zz_error(p_01), "cx", [0, 1], [1, 3])
noise_corr_cx.add_nonlocal_quantum_error(zz_error(p_01), "cx", [0, 3], [1, 3])
noise_corr_cx.add_nonlocal_quantum_error(zz_error(p_01), "cx", [2, 1], [1, 3])
noise_corr_cx.add_nonlocal_quantum_error(zz_error(p_01), "cx", [2, 3], [1, 3])

print(noise_corr_cx)
```

    NoiseModel:
      Instructions with noise: ['cx']
      Non-local specific qubit errors: [('cx', [0, 1], [1, 3]), ('cx', [0, 3], [1, 3]), ('cx', [2, 1], [1, 3]), ('cx', [2, 3], [1, 3])]

### Executing the noisy simulation

Now when we execute we have to remember to compile using the coupling map so that the CX gates of the circuit will be mapped to those in the coupling map that are specified in the noise model

```python
# Compile previous GHZ circuit using noise model basis gates
qobj = qiskit.compile(circ, simulator,
                      coupling_map=coupling_map,
                      basis_gates=noise_corr_cx.basis_gates)

# Run the noisy simulation
result_corr_cx = simulator.run(qobj, noise_model=noise_corr_cx).result()
counts_corr_cx = result_corr_cx.get_counts(0)

# Plot noisy output
qplt.plot_histogram(counts_corr_cx)
```

## Example 4: Coherent leakage error

* *Leakage* is an error that occurs when a qubit is actually a subspace in a higher-dimensional system
* Coherent leakage errors are unitary errors between the qubit subspace and the other energy levels.
* Now show how one can model *leakage error* even though the simulation is only a qubit simulation.

### To model coherent leakage:

* We add a second quantum register of ancilla qubits to represent the leakage levels for each original qubit
* We add  second set of classical bits to flag when leakage has occured based on the state of the ancilla qubits
* For a single qubit we have
$$|\psi\rangle \mapsto  |\phi\rangle\otimes|\psi\rangle$$

* where our qubit is now a four level system

$$
\begin{align}
|0\rangle \equiv |0\rangle\otimes|0\rangle \\
|1\rangle \equiv |0\rangle\otimes|1\rangle \\
|2\rangle \equiv |1\rangle\otimes|0\rangle \\
|3\rangle \equiv |1\rangle\otimes|1\rangle
\end{align}
$$

* If we measure the ancilla qubit state $|\phi\rangle$ and get an outcome of 1 we know that the qubit state is in the leakage subspace.

```python
# Define additional registers
qa = QuantumRegister(n_qubits)
ca = ClassicalRegister(n_qubits)
circ_leakage = QuantumCircuit(qr, qa, cr, ca)

# Add original circuit
circ_leakage += circ

# Measure ancillas to flag leakage
circ_leakage.barrier(qr, qa)
circ_leakage.measure(qa, ca)
print(circ_leakage)
```

             ┌───┐               ┌─┐          ░
    q1_0: |0>┤ H ├──■────────────┤M├──────────░─────────────
             └───┘┌─┴─┐          └╥┘┌─┐       ░
    q1_1: |0>─────┤ X ├──■────────╫─┤M├───────░─────────────
                  └───┘┌─┴─┐      ║ └╥┘┌─┐    ░
    q1_2: |0>──────────┤ X ├──■───╫──╫─┤M├────░─────────────
                       └───┘┌─┴─┐ ║  ║ └╥┘┌─┐ ░
    q1_3: |0>───────────────┤ X ├─╫──╫──╫─┤M├─░─────────────
                            └───┘ ║  ║  ║ └╥┘ ░ ┌─┐
    q2_0: |0>─────────────────────╫──╫──╫──╫──░─┤M├─────────
                                  ║  ║  ║  ║  ░ └╥┘┌─┐
    q2_1: |0>─────────────────────╫──╫──╫──╫──░──╫─┤M├──────
                                  ║  ║  ║  ║  ░  ║ └╥┘┌─┐
    q2_2: |0>─────────────────────╫──╫──╫──╫──░──╫──╫─┤M├───
                                  ║  ║  ║  ║  ░  ║  ║ └╥┘┌─┐
    q2_3: |0>─────────────────────╫──╫──╫──╫──░──╫──╫──╫─┤M├
                                  ║  ║  ║  ║  ░  ║  ║  ║ └╥┘
     c0_0: 0 ═════════════════════╩══╬══╬══╬═════╬══╬══╬══╬═
                                     ║  ║  ║     ║  ║  ║  ║
     c0_1: 0 ════════════════════════╩══╬══╬═════╬══╬══╬══╬═
                                        ║  ║     ║  ║  ║  ║
     c0_2: 0 ═══════════════════════════╩══╬═════╬══╬══╬══╬═
                                           ║     ║  ║  ║  ║
     c0_3: 0 ══════════════════════════════╩═════╬══╬══╬══╬═
                                                 ║  ║  ║  ║
     c1_0: 0 ════════════════════════════════════╩══╬══╬══╬═
                                                    ║  ║  ║
     c1_1: 0 ═══════════════════════════════════════╩══╬══╬═
                                                       ║  ║
     c1_2: 0 ══════════════════════════════════════════╩══╬═
                                                          ║
     c1_3: 0 ═════════════════════════════════════════════╩═

### Representing coherent leakage error

* Represent a coherent leakage error as a 2-qubit unitary matrix.
* Add it to the noise model as a non-local error for gates on the gate qubit
* Non-local error  effects both the gate qubit and corresponding ancilla qubit

```python
from scipy.linalg import expm

# Ideal X-90 drive Hamiltonian
H_ideal =  0.25 * np.pi * np.kron(np.eye(2), np.array([[0, 1], [1, 0]]))
U_ideal = U_noisy = expm(-1j * H_ideal)

# Coherent unitary error due to leakage transitions in Hamiltonian
H_error = 0.1 * np.array([[0, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 0]])
U_error = coherent_unitary_error(np.dot(U_ideal.conj().T, expm(-1j * (H_ideal + H_error))))

# Build noise model and add to H and CX
# For CX we will just do a tensor-product of the single qubit errors
U_error2 = U_error.kron(U_error)

# Add errors to noise model, but be careful about qubit numbers!
noise_leakage = NoiseModel()
for i in range(n_qubits):
    noise_leakage.add_nonlocal_quantum_error(U_error, ['u1', 'u2', 'u3'],
                                             [i], [i, n_qubits + i])
    for j in range(n_qubits):
        if j != i:
            noise_leakage.add_nonlocal_quantum_error(U_error2, 'cx',
                                                     [i, j],
                                                     [i, n_qubits + i, j, n_qubits + j])
print(noise_leakage)
```

    NoiseModel:
      Instructions with noise: ['u2', 'cx', 'u3', 'u1']
      Non-local specific qubit errors: [('u1', [0], [0, 4]), ('u1', [1], [1, 5]), ('u1', [2], [2, 6]), ('u1', [3], [3, 7]), ('u2', [0], [0, 4]), ('u2', [1], [1, 5]), ('u2', [2], [2, 6]), ('u2', [3], [3, 7]), ('u3', [0], [0, 4]), ('u3', [1], [1, 5]), ('u3', [2], [2, 6]), ('u3', [3], [3, 7]), ('cx', [0, 1], [0, 4, 1, 5]), ('cx', [0, 2], [0, 4, 2, 6]), ('cx', [0, 3], [0, 4, 3, 7]), ('cx', [1, 0], [1, 5, 0, 4]), ('cx', [1, 2], [1, 5, 2, 6]), ('cx', [1, 3], [1, 5, 3, 7]), ('cx', [2, 0], [2, 6, 0, 4]), ('cx', [2, 1], [2, 6, 1, 5]), ('cx', [2, 3], [2, 6, 3, 7]), ('cx', [3, 0], [3, 7, 0, 4]), ('cx', [3, 1], [3, 7, 1, 5]), ('cx', [3, 2], [3, 7, 2, 6])]

### Executing the noisy simulation

```python
# Compile previous GHZ circuit using noise model basis gates
qobj = qiskit.compile(circ_leakage, simulator,
                      basis_gates=noise_leakage.basis_gates)

result_leakage = simulator.run(qobj, noise_model=noise_leakage).result()
counts_leakage = result_leakage.get_counts(0)

# Plot noisy output
qplt.plot_histogram(counts_leakage)
```

# Advanced Noise Model Examples

These advanced noise modules use features of *Qiskit Aer* that aren't fully integrated with Qiskit Terra. They involve modifying a compiled qobj to insert special instructions that will be added to the Terra QuantumCircuit API in a future release.

## Example 5: Adding correlated classical readout error

* Now let us consider adding a correlated readout error on a multi-qubit measurement.

* For 2-qubits our assignment probabilities are
$$P(A|B) = [P(A|00), P(A|01), P(A|10), P(A|11)]$$

* We will build the noise model for 2-qubit readout errors on pairs of qubits `[0, 1]` and `[2, 3]`
* *Note: We could also do a 4-qubit correlated readout error, but we'll stick to 2-qubit to keep the matrices small*

```python
# Assignment probabilities for measure qubits [0, 1]
p_00_qubits01 = [0.8, 0.1, 0.1, 0.0]
p_01_qubits01 = [0.2, 0.6, 0.1, 0.1]
p_10_qubits01 = [0.2, 0.1, 0.6, 0.1]
p_11_qubits01 = [0.1, 0.2, 0.2, 0.5]
p_qubits01 = [p_00_qubits01, p_01_qubits01, p_10_qubits01, p_11_qubits01]

# Assignment probabilities for measure qubits [2, 3]
p_00_qubits23 = [0.7, 0.1, 0.1, 0.1]
p_01_qubits23 = [0.2, 0.6, 0.1, 0.1]
p_10_qubits23 = [0.2, 0.1, 0.6, 0.1]
p_11_qubits23 = [0.2, 0.2, 0.2, 0.4]
p_qubits23 = [p_00_qubits23, p_01_qubits23, p_10_qubits23, p_11_qubits23]

# Build readout error objects
ro_error01 = ReadoutError(p_qubits01)
print(ro_error01)

# Build readout error objects
ro_error23 = ReadoutError(p_qubits23)
print(ro_error23)
```

    ReadoutError on 2 qubits. Assignment probabilities:
     P(j|0) =  [0.8, 0.1, 0.1, 0.0]
     P(j|1) =  [0.2, 0.6, 0.1, 0.1]
     P(j|2) =  [0.2, 0.1, 0.6, 0.1]
     P(j|3) =  [0.1, 0.2, 0.2, 0.5]
    ReadoutError on 2 qubits. Assignment probabilities:
     P(j|0) =  [0.7, 0.1, 0.1, 0.1]
     P(j|1) =  [0.2, 0.6, 0.1, 0.1]
     P(j|2) =  [0.2, 0.1, 0.6, 0.1]
     P(j|3) =  [0.2, 0.2, 0.2, 0.4]

Now we add these errors to the noise model, note that we dont actually have to construct the `ReadoutError` object, we could just add the readout errors using the lists of probabilities `p_qubitsij` directly

```python
noise_corr_ro = NoiseModel()
noise_corr_ro.add_readout_error(ro_error01, [0, 1])
noise_corr_ro.add_readout_error(ro_error01, [2, 3])
print(noise_corr_ro)
```

    NoiseModel:
      Instructions with noise: ['measure']
      Specific qubit errors: [('measure', [0, 1]), ('measure', [2, 3])]

### Executing the noisy simulation

* To actually use this model in a simulation we need to modify our qobj
* We need to make the `measure` instruction a multi-qubit measurement
* Editing is necessary as the Terra compiler decomposes all `measure` circuit instructions into successive single-qubit `measure` qobj instructions.
* We can do this using helper functions in `aer.utils.qobj_utils`

```python
# Compile previous GHZ circuit using noise model basis gates
qobj = qiskit.compile(circ, simulator,
                      basis_gates=noise_corr_ro.basis_gates)

# Find measure instruction positions in qobj and drop them
meas_pos = aer.utils.qobj_utils.get_instr_pos(qobj, 0, 'measure')

# Delete measure from qobj
instructions = qobj.experiments[0].instructions
[instructions.pop(j) for j in reversed(meas_pos)]

# Add new multi-qubit measure instructions
meas01 = aer.utils.qobj_utils.measure_instr([0, 1], [0, 1])
meas23 = aer.utils.qobj_utils.measure_instr([2, 3], [2, 3])
instructions += [meas01, meas23]

# Replace experiment instructions with new modified ones
qobj.experiments[0].instructions = instructions

# Run the noisy simulation
result_corr_ro = simulator.run(qobj, noise_model=noise_corr_ro).result()
counts_corr_ro = result_corr_ro.get_counts(0)

# Plot noisy output
qplt.plot_histogram(counts_corr_ro)
```

## Example 6: Always-on cross-talk noise

Finally we will add always on cross-talk to do this we will define a noise model on a *custom unitary gate* (which in our case will be an identity), that we can then assign an error to in the noise model. We will insert this gate in between all slices to simulate an always on multi-qubit cross-talk error.

Let us consider an error that consists of correlated two-body $XX$ errors between all the qubits on a square

```python
p01 = 0.01
p03 = 0.03
p21 = 0.02
p23 = 0.04
ct_error = pauli_error([('IIXX', p01),
                        ('XIIX', p03),
                        ('IXXI', p21),
                        ('XXII', p23),
                        ('IIII', 1 - p01 - p03 - p21 - p23)],
                        standard_gates=True)
print(ct_error)

# Add error to a noise model for custom instruction "ct"
noise_ct = NoiseModel()
noise_ct.add_nonlocal_quantum_error(ct_error, "ct", [0], [0, 1, 2, 3])
# the following aren't really necessary if we always apply ct to qubit-0
noise_ct.add_nonlocal_quantum_error(ct_error, "ct", [1], [0, 1, 2, 3])
noise_ct.add_nonlocal_quantum_error(ct_error, "ct", [2], [0, 1, 2, 3])
noise_ct.add_nonlocal_quantum_error(ct_error, "ct", [3], [0, 1, 2, 3])

print(noise_ct)
```

    QuantumError on 4 qubits. Noise circuits:
      P(0) = 0.01, QobjInstructions = [[{'name': 'x', 'qubits': [0]}, {'name': 'x', 'qubits': [1]}]
      P(1) = 0.03, QobjInstructions = [[{'name': 'x', 'qubits': [0]}, {'name': 'x', 'qubits': [3]}]
      P(2) = 0.02, QobjInstructions = [[{'name': 'x', 'qubits': [1]}, {'name': 'x', 'qubits': [2]}]
      P(3) = 0.04, QobjInstructions = [[{'name': 'x', 'qubits': [2]}, {'name': 'x', 'qubits': [3]}]
      P(4) = 0.8999999999999999, QobjInstructions = [[{'name': 'id', 'qubits': [0]}]
    NoiseModel:
      Instructions with noise: ['ct']
      Non-local specific qubit errors: [('ct', [0], [0, 1, 2, 3]), ('ct', [1], [0, 1, 2, 3]), ('ct', [2], [0, 1, 2, 3]), ('ct', [3], [0, 1, 2, 3])]

### Executing noisy simulation

* We need to modify the Qobj for custom gate errors
* First we compile to a qobj
* Next we insert our custom identity matrix with name `"ct"` after the Hadamard and CX gates

```python
# Compile previous GHZ circuit using noise model basis gates
qobj = qiskit.compile(circ, simulator,
                      basis_gates=noise_ct.basis_gates)

# Create custom gate
ct_gate = aer.utils.qobj_utils.unitary_instr(np.eye(2), [0], label="ct")

# Because of our noise basis gates the circuit will be compiled to U and CX gates:
U_pos = aer.utils.qobj_utils.get_instr_pos(qobj, 0, 'U')
CX_pos = aer.utils.qobj_utils.get_instr_pos(qobj, 0, 'CX')
for pos in reversed(sorted(U_pos + CX_pos)):
    aer.utils.qobj_utils.insert_instr(qobj, 0, ct_gate, pos + 1)

# Run the noisy simulation
result_ct = simulator.run(qobj, noise_model=noise_ct).result()
counts_ct = result_ct.get_counts(0)

# Plot noisy output
qplt.plot_histogram(counts_ct)
```


# FEniCS overview

https://fenicsproject.org/

## Overview

- FEniCS means Finite Elements + Computational Software and 'ni' just "sits nicely in the middle"
- Finite elements solver for PDE
- Easy-to-use notation to translate scientific models into efficient code
- C++ and Python interface
- Easy installation with
```
conda install -c conda-forge fenics
```
- Comprehensive [tutorial](https://link.springer.com/book/10.1007%2F978-3-319-52462-7) and [FEniCS book](https://fenicsproject.org/book/) are available online
- To install package for mesh generation
```
conda install -c conda-forge mshr
```
- Important! To use FEniCS inside separate environment Jupyter Notebook
```
conda create -n fenics_env python=3.6
source activate fenics_env
conda install -c conda-forge fenics
conda install notebook
conda install -c conda-forge matplotlib
conda install -c conda-forge mshr
jupyter notebook
```

## Main steps

1. Identify the computational domain $(\Omega)$, the PDE, its boundary conditions, and source terms $(f)$
2. Reformulate the PDE as a finite element variational problem
3. Write a Python program which defines the computational domain, the variational problem, the boundary conditions, and source terms, using the corresponding FEniCS abstractions
4. Call FEniCS to solve the boundary-value problem and visualize the results

### Variational form

Finite elements methods starts from *variational form* of PDE. Consider Poisson equation with Dirichlet boundary conditions
$$
\begin{align}
-\Delta u &= f(x), \quad x \in \Omega\\
u(x) &= u_D(x), \quad x \in \partial \Omega
\end{align}
$$

To get its variational form one needs:
1. Multiply both sides by test function $v$ such that $v \equiv 0$ on the boundary $\partial \Omega$
2. Intergate both sides over the domain $\Omega$ and apply integration-by-parts theorem

$$
\begin{align}
& -\int_{\Omega} \nabla^2 u \cdot v dx = \int_{\Omega} f \cdot v dx \\
& \int_{\Omega} \nabla u \cdot \nabla v dx - \underbrace{\int_{\partial \Omega} \frac{\partial u}{\partial n} v ds}_{=0} = \int_{\Omega} f \cdot v dx
\end{align}
$$
Therefore, variational form of Poisson equation with Dirichlet boundary conditions is
$$
\int_{\Omega} \nabla u \cdot \nabla v dx = \int_{\Omega} f \cdot v dx
$$
Now let's see how these form is converted to FEniCS code...

## FEniCS example

- Domain $\Omega = [0, 1]^2$
- $f = -6$
- $u_D = 1 + x^2 + y^2$ for $(x, y) \in \partial \Omega$

```python
%matplotlib notebook
from __future__ import print_function
import fenics
import matplotlib.pyplot as plt
```

### Set domain and mesh

```python
mesh = fenics.UnitSquareMesh(8, 8)
_ = fenics.plot(mesh)
```

    <IPython.core.display.Javascript object>

- List of other default meshes are available [here](https://fenicsproject.org/olddocs/dolfin/1.5.0/python/demo/documented/built-in_meshes/python/documentation.html)
- Also you can create your own mesh in the following manner

```python
import dolfin
import mshr
import math

domain_vertices = [dolfin.Point(0.0, 0.0),
                   dolfin.Point(10.0, 0.0),
                   dolfin.Point(10.0, 2.0),
                   dolfin.Point(8.0, 2.0),
                   dolfin.Point(7.5, 1.0),
                   dolfin.Point(2.5, 1.0),
                   dolfin.Point(2.0, 4.0),
                   dolfin.Point(0.0, 4.0),
                   dolfin.Point(0.0, 0.0)]

p = mshr.Polygon(domain_vertices);
fenics.plot(mshr.generate_mesh(p, 20))
```

    <IPython.core.display.Javascript object>

    [<matplotlib.lines.Line2D at 0x7f46851298d0>,
     <matplotlib.lines.Line2D at 0x7f4685129a58>]

```python
R = 1.1
r = 0.9
x, y, z = 1, -1, 1

# Create geometry
s1 = mshr.Sphere(dolfin.Point(0, 0, 0), 1)
s2 = mshr.Sphere(dolfin.Point(x, y, z), r)
b1 = mshr.Box(dolfin.Point(-2, -2, -0.03), dolfin.Point(2, 2, 0.03))
geometry = s1 - b1 - s2
death_star_mesh = mshr.generate_mesh(geometry, 10)

# Plot mesh
fenics.plot(death_star_mesh, color="grey")
plt.xlabel("x")
plt.ylabel("y")
```

    <IPython.core.display.Javascript object>

    <matplotlib.text.Text at 0x7f4684057860>

### Set function space

```python
V = fenics.FunctionSpace(mesh, 'P', 1)
```

- The first argument is a mesh
- The second argument is a family of functions that we use: "P" is a Lagrange polynomial. "DP" means discontinuous polynomial. The list of supported function spaces is available [here](https://www.femtable.org/)
- The third argument is a degree of the polynomial

### Set Dirichlet boundary conditions

```python
u_D = fenics.Expression('1 + x[0]*x[0] + 2*x[1]*x[1]', degree=2)

def boundary(x, on_boundary):
    return on_boundary

bc = fenics.DirichletBC(V, u_D, boundary)
```

- Function $u_D$ is written as ```Expression``` object that is directly translated into efficient C++ code
- The first argument is a string that represent mathematical equation in a valid C++ syntax
- If inside ```Expression``` some constant appeares, they should be clearly specified
```python
f = Expression("exp(-kappa*pow(pi, 2)*t)*sin(pi*k*x[0])", degree=2, kappa=1.0, t=0, k=4)
```
- The second parameter indicates the degree of function space that will be used to evaluare expression in every local element. So, the optimal value of this parameter is the same as in function space or greater by one or two
- Function ```boundary``` checks whether requested point ```x``` lies on the *Dirichlet boundary* or not. Argument ```on_boundary``` is ```True``` if requested point lies on the *physical boundary* of the mesh and ```False``` otherwise

### Set variational form

```python
u = fenics.TrialFunction(V)
v = fenics.TestFunction(V)
f = fenics.Constant(-6.0)   # Or f = Expression(’-6’, degree=0)
# Left-hand side
a = fenics.dot(fenics.grad(u), fenics.grad(v))*fenics.dx
# Right-hand side
L = f*v*fenics.dx
```

- Function $u$ in variational form is called *trial function*
- Auxilliary function $v$ is called *test function*
- Variational forms of right- and left-hand sides are very similar to the mathematical notation
- ```dot``` is used for vectors and ```inner``` is used for matrices

### Compute solution

```python
u = fenics.Function(V)
fenics.solve(a == L, u, bc)
```

- Redefine ```u``` from ```TrialFunction``` to solution ```Function```
- In solver we pass, variational form with sign ```==```, unknown solution and bondary conditions

```python
# Plot solution and mesh
fenics.plot(u)
```

    <IPython.core.display.Javascript object>

    <matplotlib.tri.tricontour.TriContourSet at 0x7f467e42c2b0>

### Compute error of the solution

$$
E = \sqrt{\int_{\Omega} (u - u_D)^2 dx}
$$

```python
error_L2 = fenics.errornorm(u_D, u, 'L2')
print("Error in L2 norm = {}".format(error_L2))
error_H1 = fenics.errornorm(u_D, u, 'H1')
print("Error in H1 norm = {}".format(error_H1))
```

    Error in L2 norm = 0.008235098073354806
    Error in H1 norm = 0.1615842922655032

Maximum of the difference between boundary function and solution on mesh vertices:
$$
\max_{x_i \in M} |u(x_i) - u_D(x_i)|
$$

```python
vertex_values_u_D = u_D.compute_vertex_values(mesh)
vertex_values_u = u.compute_vertex_values(mesh)
import numpy as np
error_max = np.max(np.abs(vertex_values_u_D - vertex_values_u))
print('Error max =', error_max)
```

    Error max = 1.33226762955e-15

```python
plt.contourf(vertex_values_u.reshape(9, 9).T)
plt.colorbar()
```

    <IPython.core.display.Javascript object>

    <matplotlib.colorbar.Colorbar at 0x7f467e49ada0>

## Recap

- The power of variational formulation and FEniCS
- The workflow for solving PDE with FEniCS
- Dirichlet and Neumann boundary conditions are supported
- Different boundary conditions for different parts of mesh are supported
- For more examples and use cases, see tutorial and FEniCS book


```python
import matplotlib.pyplot as plt
import sympy as sp
from chempy import Substance, Equilibrium, ReactionSystem
from chempy.kinetics.ode import get_odesys
%matplotlib inline
```

```python
K1, K3, R1f, R3f = sp.symbols('K_1 K_3 R_1f R_3f')

def mono_rev_single():
    '''Reversible mechanism with one central complex (pg 36)
    '''
    E, A, P, X = map(Substance, 'EAPX')
    equilibria = e1, e3 = [
        Equilibrium({'E', 'A'}, {'X'}, param=K1),
        Equilibrium({'X'}, {'E', 'P'}, param=K3)
    ]
    reactions = e1.as_reactions(kf=R1f) + e3.as_reactions(kf=R3f)
    return ReactionSystem(reactions, (E, A, P, X))
```

```python
rsys = mono_rev_single()
```

```python
rsys
```

```python
type(rsys.rxns[1].param)
```

```python
odesys = get_odesys(rsys, include_params=False, params=True)[0]
```

```python
assert len(odesys.params) == 4
odesys.params
```

```python
odesys.get_jac()
```

```python
c0 = {'E': 5, 'A': 2, 'P': 0, 'X': 0}
params = {K1: 17, K3: 23, R1f: 63, R3f: 43}
```

```python
odesys.f_cb(42, [c0[k] for k in rsys.substances], [params[k] for k in odesys.params])
```

```python
odesys.names, odesys.param_names  # get_odesys or SymbolicSys could determine names from Symbol.name
```

```python
result = odesys.integrate(42, c0, params, integrator='cvode')
```

```python
result.plot(xscale='log')
plt.gca().set_xlim([1e-6, plt.gca().get_xlim()[1]])
_ = plt.legend(loc='best')
```


# Random Forests

- 'Random' - each tree gets some unique training data randomly - so helps against overfitting !

- 'Forest' because it is a collection - an ensemble - of decision trees!

- It's like cross-validation meets decision tree.

**Aside: Awesome data visualisation website** http://www.r2d3.us/visual-intro-to-machine-learning-part-1/

| concept | description |
|:-------:|:-----------:|
| decision tree | a classification model constructing questions|
| root node | the first question a tree asks |
| leaf node | the result of the last question |
| depth | number of consecutive questions |
| CART (Classification & Regression Tree)| name of the tree construction algorithm |
| GINI | a metric for the purity of nodes |
| ensemble | a model consisting of many sub-models |
| Random Forest | an ensemble of randomized decision trees |
| Gradient Boosting | a deluxe tree-based ensemble model |

## Let's try out a Random Forest Classifier in Scikit-Learn

Train and fine-tune a Random Forest on the Titanic dataset.
- experiment with the hyperparameters `max_depth` and `n_estimators`
- compare your model to the Decision Tree model

### **1. Read Data in Pandas**

```python
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
```

```python
df = pd.read_csv('train.csv', index_col=0)
df.head()
```

      Survived
      Pclass
      Name
      Sex
      Age
      SibSp
      Parch
      Ticket
      Fare
      Cabin
      Embarked

      PassengerId

      1
      0
      3
      Braund, Mr. Owen Harris
      male
      22.0
      1
      0
      A/5 21171
      7.2500
      NaN
      S

      2
      1
      1
      Cumings, Mrs. John Bradley (Florence Briggs Th...
      female
      38.0
      1
      0
      PC 17599
      71.2833
      C85
      C

      3
      1
      3
      Heikkinen, Miss. Laina
      female
      26.0
      0
      0
      STON/O2. 3101282
      7.9250
      NaN
      S

      4
      1
      1
      Futrelle, Mrs. Jacques Heath (Lily May Peel)
      female
      35.0
      1
      0
      113803
      53.1000
      C123
      S

      5
      0
      3
      Allen, Mr. William Henry
      male
      35.0
      0
      0
      373450
      8.0500
      NaN
      S

```python
df.info()
```

    <class 'pandas.core.frame.DataFrame'>
    Int64Index: 891 entries, 1 to 891
    Data columns (total 11 columns):
    Survived    891 non-null int64
    Pclass      891 non-null int64
    Name        891 non-null object
    Sex         891 non-null object
    Age         714 non-null float64
    SibSp       891 non-null int64
    Parch       891 non-null int64
    Ticket      891 non-null object
    Fare        891 non-null float64
    Cabin       204 non-null object
    Embarked    889 non-null object
    dtypes: float64(2), int64(4), object(5)
    memory usage: 83.5+ KB

### **2. Split data - Select columns - into X and y**

**Note:** Now we're gonna purposefuly not choose 'Age' and 'cabin' so that we don't have missing values

```python
X = df[['Pclass', 'Sex', 'Fare']]
y = df['Survived']
```

```python
len(X) == len(y)
```

    True

### **3. Split X data into training and testing sets**

```python
X_train, X_test, y_train, y_test = train_test_split(X, y,
                                                    random_state=42)
```

```python
X_train.shape, X_test.shape, y_train.shape, y_test.shape
```

    ((668, 3), (223, 3), (668,), (223,))

### **4. Feature Engineering**

```python
# pd.get_dummies(X_train['Sex']).head()
```

```python
# Four options for encoding strings to numbers:
# 1. LabelEncoder
# 2. pd.factorize()
# 3. pd.get_dummies()
# 4. .map() - Probably the best option here!

# d = {'female': 0, 'male': 1}

# X_train = X_train.copy() # gets rid of warning!!
# X_train['Sex'] = X_train['Sex'].map(d)
# X_train.head()
```

### We now have X_train data that has different columns compared to the original dataframe, therefore we need to be able to apply these changes to the X_test dataframe too - We need to write a function!

```python
def feature_engineer(dataframe):

    d = {'female': 0, 'male': 1}

    dataframe = dataframe.copy() # just to supress pandas warning!!
    dataframe['Sex'] = dataframe['Sex'].map(d)

    return dataframe
```

```python
X_train = feature_engineer(X_train)
X_train.head()
```

      Pclass
      Sex
      Fare

      PassengerId

      299
      1
      1
      30.5000

      885
      3
      1
      7.0500

      248
      2
      0
      14.5000

      479
      3
      1
      7.5208

      306
      1
      1
      151.5500

### **5. "Build" a Random Forest model**

`m = RandomForestClassifier()`

```python
m = RandomForestClassifier(n_estimators=3, max_depth=2, criterion='gini' )
```

In the brackets, we can now introduce the hyperparameters:
- number of trees i.e. n_estimators
- number of levels/depth - how many questions deep you want it to go
- criterion - RandomForest is using the gini index to split the data as well as possible

### The GINI index

The GINI index is used to evaluate which partition results in the best separation of the classes K:

\begin{align}
G = \sum_{k=1}^{K}{(p_k (1 - p_k))}
\end{align}
Where p indicates the proportion of points belonging to class K.

**Which is the best possible GINI score, which the worst?**

### **6. Train the model**

```python
m.fit(X_train, y_train)
```

    RandomForestClassifier(bootstrap=True, class_weight=None, criterion='gini',
                max_depth=2, max_features='auto', max_leaf_nodes=None,
                min_impurity_decrease=0.0, min_impurity_split=None,
                min_samples_leaf=1, min_samples_split=2,
                min_weight_fraction_leaf=0.0, n_estimators=3, n_jobs=None,
                oob_score=False, random_state=None, verbose=0,
                warm_start=False)

```python
m.score(X_train, y_train)
```

    0.7874251497005988

#### Now we check the test data (remember we wrote a function earlier!)

```python
X_test = feature_engineer(X_test)
```

```python
m.score(X_test, y_test)
```

    0.7713004484304933

### Now we want to optimise both our train and test scores: change the hyperparameter values (will see how to optimise these tomorrow)

```python
m = RandomForestClassifier(n_estimators=50000, max_depth=60000,
                           criterion='gini')
```

```python
m.fit(X_train, y_train)
```

    RandomForestClassifier(bootstrap=True, class_weight=None, criterion='gini',
                max_depth=60000, max_features='auto', max_leaf_nodes=None,
                min_impurity_decrease=0.0, min_impurity_split=None,
                min_samples_leaf=1, min_samples_split=2,
                min_weight_fraction_leaf=0.0, n_estimators=50000, n_jobs=None,
                oob_score=False, random_state=None, verbose=0,
                warm_start=False)

```python
m.score(X_train, y_train)
```

    0.9086826347305389

```python
m.score(X_test, y_test)
```

    0.8251121076233184

**Parameters**: selecting the right questions - in order to maximise the gini algorithm!

**Hyperparameters**: See next lesson

**Notable features**:
- fast
- great starter
- but difficult to interpret

### Pros and Cons of Random Forests
| Pros | Cons |
|:----:|:----:|
|handle a mix of categorical and scalar data | prone to overfitting |
|automatically select important features| don’t calculate probabilities |
|produce high-quality models | sensitive to transformations of the data |
|can be used for regression, too | e.g. rotations |
|great as a “first shot” | regression trees cannot extrapolate |
|fast |  |

```python

```


# K-means  Clustering and Image Sharpening

In this exercise, you are asked to approximate the depth of field (DOF) effect on an
image. The depth of field is the distance between the nearest and farthest objects in a
scene which appear acceptably sharp in an image. When the DOF is small, the object
of interest (i.e., focused object) appears sharp and the rest appears blurred. This effect
is usually employed for artistic reasons and is achieved by having a large aperture while
capturing an image.

In our case, the image has already been captured, and we are trying to simulate a similar
effect. The task is to first segment the image into 3 areas (front/middle/background
areas), and then to sharpen the object of interest while blurring the rest.

In our case in addition to the RGB image, a depth image has also been captured.
The intensity of each pixel in the depth image correspond to the inverse distance
between the camera and the objects of the scene (up to a scale). Instead of finding the
correct segmentation of the image, which is a difficult task, we are going to perform the
segmentation on the depth image. The depth image, however, is usually a bit noisy, so a
simple thresholding does not produce good results in most cases, thus you will need to
implement a k-means segmentation algorithm.

In this exercise, you are asked to:
* implement the k-means algorithm to segment the depth image into three disjoint
regions. This will produce a label image where the three objects (doll, hand,
background) have a different label.
* generate three binary images each focusing on a different object.
* sharpen the focused object and smooth the rest, using these binary images and
appropriate filters.

<figure>

    <figcaption>Figure 1: Pair of images, captured by Microsoft Kinect. The image on the left depicts the RGB image
and the one on the right shows the corresponding depth image. </figcaption>
</figure>

```python
%matplotlib inline

import numpy as np
import matplotlib.pyplot as plt
import cv2
import math
import scipy.signal as conv
```

```python
# Read the image
img = cv2.imread('images/rgb.png',0)
depth_img = cv2.imread('images/depth_noisy.png',0)

# Display the images
plt.figure(1)
plt.imshow(img,cmap='gray')
plt.figure(2)
plt.imshow(depth_img,cmap='gray')

depth_img = np.float64(depth_img)*4; # rescaled for better results
```

## Exercise 1: Depth-based Segmentation

To start with, You are asked to implement the fucntion ```labelImage = clusterImage(depthImage,max_iters)``` by the following the steps below:
* $\forall pixels[i, j]: 0 ≤ i < H, 0 ≤ j < W$ of the depth image, compute the Euclidean
distance between its depth $D[i,j]$ and each cluster mean depth $k^l_d$: $dist[i,j,l] = \sqrt{(D[i,j] - {k^{l}}_{d})^2}$ and $l = 0,1,2$. How would you initialize cluster means?
* For each pixel find the smallest distance and assign this pixel to the
corresponding cluster.
* Compute the new mean of each cluster.
* Iterate over the above two steps till the cluster centroids (mean of the clusters) do not move any more or a fixed number of iterations is reached.

What issues do you observe in the output?

#### Inputs:

- ```depthImage```: an image where each value indicates the depth of the corresponding pixel.
- ```max_iters```: the maximum number of iterations

#### Outputs:

- ```labelImage```:  output label image where each value indicates to which cluster the corresponding pixels belongs. There are three clusters: value 0 for the background, value 1 for the hand and value 2 for the doll.

```python
def clusterImage(depthImage, max_iters):

    k = 3 # Number of clusters
    #initiate centroids
    #we create 3 centroids, in the format [Z X Y], where Z in the inverse depth
    mu = np.zeros((k,3))
    mu[0,:] = [0, 0, 0] #background centroid
    mu[1,:] = [500, 300, 150] #hand centroid
    mu[2,:] = [1000, 100, 200] #doll centroid
    # Note: For Exercise 1, only Z (inverse) depth is used. X,Y is used in Exercise 2.

    # Code here

    return cluster_assignments

cluster_assignments = clusterImage(depth_img,50)
plt.imshow(cluster_assignments)
```

## Exercise 2: Using Location Information
As you saw in the previous exercise, sometimes the depth information is not enough to correctly segment the image. This can be fixed by taking into account also the spatial proximity of the pixels - pixels from the same cluster should be close to each other in the image. This can be
accomplished by adding 2 dimensions to the original depth image representing the i and
j coordinates of each pixel.

You are asked to implement a new function ```labelImage = clusterImage(depthImage,max_iters)``` following the steps:
* $\forall pixels[i, j]: 0 ≤ i < H, 0 ≤ j < W$ of the depth image, compute the Euclidean
distance between its combined depth $D[i,j]$ and position $[i,j]$ and each cluster mean $[k^l_d,k^l_i,k^l_j]$: $dist[i,j,l] = \sqrt{(D[i,j] - {k^{l}}_{d})^2 + (i-k^{l}_{i})^2) + (j-k^{l}_{j})^2)}$ for $l = 0,1,2$.

* For each pixel find the smallest distance and assign this pixel to the
corresponding cluster.
* Compute the new mean of each cluster.
* Iterate over the above two steps till the cluster centroids (mean of the clusters)
do not move any more or a fixed number of iterations is reached.

How does addition of spatial information improve the segmentation results?

#### Inputs:

- ```depthImage```: an image where each value indicates the depth of the corresponding pixel.
- ```max_iters```: maximum number of iterations

#### Outputs:

- ```labelImage```:  output label image where each value indicates to which cluster the corresponding pixels belongs. There are three clusters: value 0 for the background, value 1 for the hand and value 2 for the doll.

```python
def clusterImage(depthImage, max_iters):

    k = 3 # Number of clusters
    #initiate centroids
    #we create 3 centroids, in the format [Z X Y], where Z in the inverse depth
    mu = np.zeros((k,1))
    mu[0,:] = [0, 0, 0] #background centroid
    mu[1,:] = [500, 300, 150] #hand centroid
    mu[2,:] = [1000, 100, 200] #doll centroid

    # Code here

    return cluster_assignments

cluster_assignments = clusterImage(depth_img,50)
plt.imshow(cluster_assignments)
```

## Exercise 3: Image Sharpening

We further need to implement a region-based filtering function to sharpen the object of interest and blur the rest of
the image. In the previous parts you produced a label image (with each object assigned a different label) and produced three binary images corresponding to different objects. Now the task is to sharpen the focused regions by applying a Laplacian filter and smooth the other regions using a Gaussian filter. It should produce results as shown below.

To this end we introduce a Laplacian filter parametrized by $\alpha$ defined as:

\begin{align}
L = \frac{\partial ^2}{\partial x^2} + \frac{\partial^2}{\partial y^2} = \frac{4}{\alpha+1} \begin{bmatrix} \frac{\alpha}{4} & \frac{1-\alpha}{4} & \frac{\alpha}{4} \\
\frac{1-\alpha}{4} & -1 & \frac{1-\alpha}{4} \\
\frac{\alpha}{4} & \frac{1-\alpha}{4} & \frac{\alpha}{4} \\
\end{bmatrix}
\end{align}

Then, the sharpening filter is defined as $\delta - L$, where $\delta$ is a $3\times3$ Kronecker delta kernel with a values of 1 at the center position and zero elsewhere. The Gaussian kernel is parametrized by standard deviation $\sigma$.

* Implement functions
``` s = SharpeningKernel(alpha) ``` and ``` g = GaussianKernel(sigma, sz)``` that compute sharpening filter of size $3\times3$ and gaussian kernel of size $sz\times sz$ respectively (you computed gaussian kernel already in Exercise Session 2).

```python
def SharpeningKernel(alpha):

    return s

def GaussianKernel(sigma,sz):

    return g
```

* Implement a function ``` filteredImage = FilterRegions(origImage,binaryForeground, alpha, sigma)``` that smooths the background and sharpens the objects in the foreground.

#### Outputs:
* ```filteredImage``` - original image with sharpened foreground and smoothed background

#### Inputs:
* ```origImage``` - original image
* ```binaryForeground``` - binary mask with foreground pixels value equal to 1
* ```alpha``` - sharpening parameter
* ```sigma``` - smoothing parameter

```python
def FilterRegions(origImage,binaryForeground, alpha, sigma,sz):

    return filteredImage
```

* Visualize 3 images by focusing on different objects detected in the image. Use sharpnening filter of size $3\times3$ and $\alpha = 0.5$ and Gaussian kernel of size $7\times7$ and $\sigma = 2.0$.

```python

```

```python

```

Image Segmentation & Sharpening/.ipynb_checkpoints/kmeans_sharpening-checkpoint.ipynb


---
## Example 02. Pandas and Pyplot

Eduard Larrañaga (ealarranaga@unal.edu.co)

---

### About this notebook

In this worksheet, we use a real dataset in the csv format to illustrate the use of `pandas` and `matplotlib` in visualization.

---

### Reading the dataset
Since the dataset is a .csv file, we use pandas to read the file and take a look to the first elements.

```python
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np

# We will present the package seaborn
import seaborn as sns
sns.set()
```

```python
df = pd.read_csv('data.csv')

df.head()
```

      Name
      z
      sigma*
      e_sigma*
      n_sigma*
      FWHM
      e_FWHM
      logL
      e_logL
      logM
      E_logM
      e_logM

      0
      SDSS J000805.62+145023.4
      0.0454
      140.0
      27.0
      NaN
      7610.0
      380.0
      41.13
      0.04
      7.7
      NaN
      0.1

      1
      SDSS J004236.86-104921.8
      0.0419
      78.4
      10.0
      NaN
      1960.0
      97.0
      41.58
      0.14
      6.7
      NaN
      0.1

      2
      SDSS J011703.58+000027.3
      0.0456
      98.8
      16.0
      NaN
      2270.0
      110.0
      41.45
      0.08
      6.8
      NaN
      0.1

      3
      SDSS J020459.25-080816.0
      0.0772
      121.0
      9.4
      a
      3720.0
      180.0
      41.13
      0.05
      7.0
      NaN
      0.1

      4
      SDSS J020615.99-001729.1
      0.0426
      216.0
      30.0
      NaN
      3860.0
      190.0
      41.91
      0.07
      7.5
      NaN
      0.1

The `.describe()` attribute summarize the content of the dataset.

```python
df.describe()
```

      z
      sigma*
      e_sigma*
      FWHM
      e_FWHM
      logL
      e_logL
      logM
      E_logM
      e_logM

      count
      88.000000
      88.000000
      88.000000
      71.000000
      71.000000
      71.000000
      71.000000
      88.00000
      15.000000
      88.000000

      mean
      0.048665
      117.142045
      11.805682
      3206.056338
      210.760563
      41.504225
      0.078028
      6.86625
      0.140667
      0.189886

      std
      0.032562
      48.285108
      5.308383
      1759.679743
      191.219953
      0.663268
      0.041700
      0.72825
      0.074303
      0.172470

      min
      0.000947
      30.000000
      2.900000
      810.000000
      41.000000
      40.100000
      0.030000
      4.90000
      0.020000
      0.020000

      25%
      0.027750
      87.025000
      7.750000
      1905.000000
      110.000000
      41.155000
      0.050000
      6.30000
      0.100000
      0.100000

      50%
      0.042250
      113.500000
      12.000000
      2970.000000
      160.000000
      41.510000
      0.070000
      7.00000
      0.120000
      0.100000

      75%
      0.062200
      139.250000
      15.000000
      3870.000000
      210.000000
      41.860000
      0.090000
      7.40750
      0.170000
      0.200000

      max
      0.184000
      268.000000
      30.000000
      8240.000000
      1190.000000
      43.610000
      0.200000
      8.52000
      0.310000
      1.060000

The dataframe includes data from 88 supermassive black holes. There can be seen the mean of the values, the standard deviation and the minimum and maximum values.

The columns correspond to

**z** : Redshift \
**sigma**\* : Stellar velocity dispersion \
**e_sigma**\* : Formal uncertainty in sigma* \
**FWHM** : H$\alpha$ Full-Width at Half Maximum \
**e_FWHM** : Formal uncertainty in FWHM \
**logL** : $\log_{10}$ of H$\alpha$ luminosity in erg/s \
**e_logL** : Formal uncertainty in logL \
**logM** : $\log_{10}$ of the Black Hole mass \
**E_logM** : Formal (upper limit) uncertainty in logM \
**e_logM** : Formal (lower limit) uncertainty in logM

---

### Ploting the Data

It is possible to use `pyplot` to plot directly from the dataframe.

```python
plt.scatter(df['z'], df['logM'],  color='blue')
plt.xlabel(r'$z$')
plt.ylabel(r'$\log M$')
plt.show()
```

Now we show different combinations of the characteristics searching for some kind of patterns.

```python
plt.figure(figsize=(10, 10))

plt.subplot(221)
plt.plot(df['z'], df['logM'], 'b.')
plt.xlabel(r'$z$')
plt.ylabel(r'$\log M$')

plt.subplot(222)
plt.plot(df['logL'], df['logM'], 'r.')
plt.xlabel(r'$\log L$')
plt.ylabel(r'$\log M$')

plt.subplot(223)
plt.plot(df['z'], df['logL'], 'g.')
plt.xlabel(r'$z$')
plt.ylabel(r'$\log L$')

plt.subplot(224)
plt.plot(df['sigma*'], df['logM'], 'k.')
plt.xlabel(r'$\sigma_{*}$')
plt.ylabel(r'$\log M$')

plt.show()
```

Note that using a `pandas` dataframe has a clear advantage because the absence of data for some points doesn't induce zero values as seen when using numpy arrays. In fact, looking at the $\log L$ data, we see the absence of information represented by a `NaN` tag.

```python
df['logL']
```

    0     41.13
    1     41.58
    2     41.45
    3     41.13
    4     41.91
          ...
    83      NaN
    84      NaN
    85      NaN
    86      NaN
    87      NaN
    Name: logL, Length: 88, dtype: float64

From the plots above we recognize the following patterns

- Two separate points (outcasts or anomalies) in the plot of $\log M$ vs $z$
- Two separate clusters in the plot of $\log M$ vs $\log L$
- Three outcast or anomalies in the plot of $\log L$ vs $z$
- An almost linear tendency in the plot of $\log M$ vs $\sigma_{*}$

---
### Scatter Matrix

An easiest way to plot multiple figures from the information in a dataframe is using the `pandas.plotting.scatter_matrix` function. It draws a matrix with scatter plots of all the combination of features in the dataframe. In the diagonal, it draws a histogram of each feature. Complete information at

https://pandas.pydata.org/docs/reference/api/pandas.plotting.scatter_matrix.html

```python
pd.plotting.scatter_matrix(df, figsize=(15,15))
plt.show()
```

We can also create a scatter matrix using only some of the features of the dataframe.

```python
attributes=['z', 'sigma*', 'FWHM', 'logL', 'logM']
pd.plotting.scatter_matrix(df[attributes], figsize=(10,10))
plt.show()
```

---
Now, we will look deep into the behavior of $\log M$ vs $\sigma_{*}$. We will introduce two columns to the dataframe with the logarithm of $\sigma_{*}$ and its error,

\begin{align}
f(x) =& \log_{10} x = \frac{\ln x}{\ln 10}\\
\Delta f = &\frac{1}{\ln 10} \frac{\Delta x}{x}
\end{align}

```python
df['log_sigma*'] = np.log10(df['sigma*'])
df['log_e_sigma*'] = df['e_sigma*']/(np.log(10.)*df['sigma*'])

df.head()
```

      Name
      z
      sigma*
      e_sigma*
      n_sigma*
      FWHM
      e_FWHM
      logL
      e_logL
      logM
      E_logM
      e_logM
      log_sigma*
      log_e_sigma*

      0
      SDSS J000805.62+145023.4
      0.0454
      140.0
      27.0
      NaN
      7610.0
      380.0
      41.13
      0.04
      7.7
      NaN
      0.1
      2.146128
      0.083757

      1
      SDSS J004236.86-104921.8
      0.0419
      78.4
      10.0
      NaN
      1960.0
      97.0
      41.58
      0.14
      6.7
      NaN
      0.1
      1.894316
      0.055395

      2
      SDSS J011703.58+000027.3
      0.0456
      98.8
      16.0
      NaN
      2270.0
      110.0
      41.45
      0.08
      6.8
      NaN
      0.1
      1.994757
      0.070331

      3
      SDSS J020459.25-080816.0
      0.0772
      121.0
      9.4
      a
      3720.0
      180.0
      41.13
      0.05
      7.0
      NaN
      0.1
      2.082785
      0.033739

      4
      SDSS J020615.99-001729.1
      0.0426
      216.0
      30.0
      NaN
      3860.0
      190.0
      41.91
      0.07
      7.5
      NaN
      0.1
      2.334454
      0.060319

Now we plot the data and error bars.

```python
plt.errorbar(df['sigma*'], df['logM'], df['e_logM'], df['e_sigma*'], fmt='k.')
plt.xlabel(r'$\sigma_{*}$')
plt.ylabel(r'$\log M$')
plt.show()
```

Or in log-log scale,

```python
plt.errorbar(df['log_sigma*'], df['logM'], df['e_logM'], df['log_e_sigma*'], fmt='k.')
plt.xlabel(r'$\log \sigma_{*}$')
plt.ylabel(r'$\log M$')
plt.show()
```

03. Matplotlib/Examples/Example 02/Example 02.ipynb


# CHEM 1000 - Spring 2022
Prof. Geoffrey Hutchison, University of Pittsburgh

## Graded Homework 4

For this homework, we'll focus on:
- optimizing multi-variable functions (using Calculus FTW!)
- optimizing multi-variable functions using `scipy.optimize`
---

As a reminder, you do not need to use Python to solve the problems. If you want, you can use other methods, just put your answers in the appropriate places.

To turn in, either download as Notebook (.ipynb) or Print to PDF and upload to Gradescope.

Make sure you fill in any place that says YOUR CODE HERE or "YOUR ANSWER HERE", as well as your name and collaborators (i.e., anyone you discussed this with) below:

```python
NAME = ""
COLLABORATORS = ""
```

### Optimizing Functions

Unfortunately, optimizing functions tend to fall into two categories...
- Things that are super-easy to do analytically with Calculus (yay!)
- Things that are too hard and we need to use numerical methods like `scipy.optimize`

As I mentioned, there are whole areas of mathematics and computer science devoted to optimizing functions, depending on how much information you have (e.g., can you work out a gradient or Hessian). Much of this has become more important as machine learning and neural networks become important (e.g., the function you want to optimize is "how well can this robot car drive?").

I'll also point out that a ***lot*** of science and chemistry turn into optimization problems:
- how do I change my synthesis conditions to get the highest yield of this new molecule?
- how do I find a solvent with high solubility for this?
- how do I increase the fluorescence efficiency of this LED screen?
- how do we minimize the spread of COVID without a vaccine?
- how do we find a polymer with the best performance as a solar cell?
- how do we maximize energy storage in this battery?
- (etc.)

### Part A: Himmelblau's function

Here's another function designed to be easy to solve by Calculus, and hard to do it with numerical methods. So we'll do it the easy way:

$$
f(x, y)=\left(x^{2}+y-11\right)^{2}+\left(x+y^{2}-7\right)^{2}
$$

1. Find the maxima. What is the value of $f(x,y)$ at this point.
2. Find all minima.

Show work... (You can do this with Sympy, Wolfram Alpha, Pen-and-Paper.. whatever works best.)
(You can create extra cells / work area in the notebook, but please put the answer where indicated.

```python
from sympy import init_session
init_session()
```

```python
# if you have an error, make sure you run the cell above this
f = (x**2 + y - 11)**2 + (x + y**2 - 7)**2
dfdx = diff(f, x)
dfdy = diff(f, y)
```

### Constrained Optimization

Okay, so you may think "but how does any of this help me in the real world?"

Let's say you're playing a video game. Careful Internet research has determined the "effective health" is a combination of health and armor - both of which you can buy to improve:

$$
E = \frac{H(100+A)}{150}
$$

H is health, which costs 5 gold per unit, and A is armor, which costs 20 gold per unit.

You have 3600 gold, and you need to optimize the effectiveness E of your health and armor to survive as long as possible against the enemy team. How much of each should you buy?

(Hint, you want to write this as a Lagrange multiplier problem...)

Again, you can add more cells, do this by hand, etc. but please put your answer and explanations into the box.

```python
h, a, lam = symbols('h a lam', real=True)
E = (h * (100 + a)) / 150
g = h*5 + a*20 - 3600

# more work needed
health =
armor =
```

Ten minutes into the game, you have 500 health and 10 armor left.

After spending all your gold to start, you've managed to earn another 500 gold, and prices are the same.

Again the goal is to maximize the effectiveness E. Notice that you don't want to maximize the effectiveness of what you *purchase* -- you want to maximize the total effectiveness E of your resulting health and armor.

How much of each should you buy now?

```python
health_purchase =
armor_purchase =
```

### Water Stretch-Bend

In our flipped lecture, I went through the water potential energy function:

$$
V(r, \theta) = V_{bond} + V_{angle} + V_{str-bend}
$$

Where:
$$
V_{bond} = 265.79(1 - e^{-2.2534(R - 0.96242)})^2
$$

$$
V_{angle} = 167.16 - 3.217\theta + 0.01548\theta^2
$$

$$
V_{s-b} = 0.041 \left(\theta-103.908\right)[2\left(R- 0.9624 \right)]
$$

(I'll admit, I made up the 0.041.)

Minimize the potential energy. I suggest using Calculus...

```python
bond = 265.79*(1 - exp((-2.2534)*(r - 0.96242)))**2
angle = 167.16 - 3.217*theta + 0.01548*theta**2
stretch_bend = 0.041*(theta - 103.908)*(2*(r - 0.9624))
V =
```

<div class="alert alert-block alert-info">

**Concept**: Based only on the stretch-bend term, what happens to the bonds as the angle increases?

YOUR ANSWER HERE

</div>