# Open Science Prize: Supplementary Material

This notebook is meant to provide a little more information about the Open Science Prize, but mostly, this notebook is a launching point from which the motivated learner can find open access sources with even more detailed information.

## 1 The Heisenberg Spin Model
In the open prize notebook, the Hamiltonian you are simulating is defined as the [Heisenberg XXX model](https://en.wikipedia.org/wiki/Quantum_Heisenberg_model#XXX_model) for 3 spins in a line:
$$
H_{\text{Heis3}} = \sigma_x^{(0)}\sigma_x^{(1)} + \sigma_x^{(1)}\sigma_x^{(2)} + \sigma_y^{(0)}\sigma_y^{(1)} + \sigma_y^{(1)}\sigma_y^{(2)} + \sigma_z^{(0)}\sigma_z^{(1)} + \sigma_z^{(1)}\sigma_z^{(2)}.
$$

### 1-1 Why call it XXX?
The XXX model is one of a family of spin models known as [Heisenberg spin models](https://en.wikipedia.org/wiki/Quantum_Heisenberg_model). In some sense, the most general form of Heisenberg model is often refered to as the XYZ model. The name 'XYZ' is used because the three pair-wise operators $\sigma_x\sigma_x$, $\sigma_y\sigma_y$, and $\sigma_z \sigma_z$ in the Hamitlonian have different coefficients $J_x$, $J_y$, and $J_z$ respectively. In the case where $J_x = J_y = J_z$, the model is labeled the 'XXX' model.

### 1-2 Numerically computing the matrix representation
To compute the matrix representation of $H_{\text{Heis3}}$, we are actually missing some pieces namely the identity operator $I$ and the [tensor product](https://en.wikipedia.org/wiki/Tensor_product#Tensor_product_of_linear_maps) $\otimes$ symbol. They are both often left out in when writing a Hamiltonian, but they are implied to be there. Writing out the full $H_{\text{Heis3}}$ including the identity operators and tensor product symbols
$$
H_{\text{Heis3}} = \sigma_x^{(0)}\otimes\sigma_x^{(1)}\otimes I^{(2)} + I^{(0)} \otimes\sigma_x^{(1)}\otimes\sigma_x^{(2)} + \sigma_y^{(0)}\otimes\sigma_y^{(1)}\otimes I^{(2)} + I^{(0)} \otimes \sigma_y^{(1)}\otimes\sigma_y^{(2)} + I^{(0)} \otimes\sigma_z^{(0)}\otimes\sigma_z^{(1)} + I^{(0)}\otimes\sigma_z^{(1)}\otimes\sigma_z^{(2)}.
$$
You can see why physicists don't write that all out all the time.

#### 1-2.1 Tensor product vs Kronecker product

A point of clarity about jargon. To numerically compute the [tensor product](https://en.wikipedia.org/wiki/Tensor_product#Tensor_product_of_linear_maps) of $\sigma_x\otimes\sigma_x$, as an example, we often have already chosen to be working with the matrix representation of the operators at hand ($\sigma_x = \begin{bmatrix} 0 & 1 \\ 1 & 0 \end{bmatrix}$ in this example). Because a computer works in the matrix representation, what a computer does is actually called a [Kronecker product](https://en.wikipedia.org/wiki/Kronecker_product#Examples). When doing numerically computations, Kronecker product is the name you would look up for the given software package you're using such as [Mathematica](https://reference.wolfram.com/language/ref/KroneckerProduct.html), [numpy](https://numpy.org/doc/stable/reference/generated/numpy.kron.html), or [Qiskit](https://qiskit.org/documentation/tutorials/operators/01_operator_flow.html#Pauli-operators,-sums,-compositions,-and-tensor-products).

Below is an example in Qiskit and numpy.

```python
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams.update({'font.size': 16})  # enlarge matplotlib fonts

# Import Qubit states Zero (|0>) and One (|1>), and Pauli operators (X, Y, Z)
from qiskit.opflow import Zero, One, I, X, Y, Z

# Suppress warnings
import warnings
warnings.filterwarnings('ignore')
```

```python
# Compute a kronecker product in qiskit
# Qiskit already knows what I and X are (the identity and Pauli-X operators), so to compute the kronecker product it's very simple: ^
IX_qiskit = (I^X)
print('Qiskit Kronecker product:\n', IX_qiskit.to_matrix())

print("----------------")

# Compute a kronecker product in numpy
X_numpy = np.array([[0,1],[1,0]], dtype=complex)
I_numpy = np.eye(2, dtype=complex)

IX_numpy = np.kron(I_numpy, X_numpy)
print('Numpy Kronecker product:\n', IX_numpy)
```

    Qiskit Kronecker product:
     [[0.+0.j 1.+0.j 0.+0.j 0.+0.j]
     [1.+0.j 0.+0.j 0.+0.j 0.+0.j]
     [0.+0.j 0.+0.j 0.+0.j 1.+0.j]
     [0.+0.j 0.+0.j 1.+0.j 0.+0.j]]
    ----------------
    Numpy Kronecker product:
     [[0.+0.j 1.+0.j 0.+0.j 0.+0.j]
     [1.+0.j 0.+0.j 0.+0.j 0.+0.j]
     [0.+0.j 0.+0.j 0.+0.j 1.+0.j]
     [0.+0.j 0.+0.j 1.+0.j 0.+0.j]]

## 2 Using OpFlow
Qiskit offers [functionality for mathematically working with quantum states and operators](https://qiskit.org/documentation/apidoc/opflow.html) called ```opflow``` with tutorials found [here](https://qiskit.org/documentation/tutorials/operators/index.html). Opflow is especially convenient when dealing with large numbers of qubits as tensor products can become unweildy when using numpy both in size and syntax. For example, to define $H_{\text{Heis3}}$, we could use ```numpy```'s ```numpy.kron(...)``` function to compute the tensor product as shown below.

```python
# Returns matrix representation of the XXX Heisenberg model for 3 spin-1/2 particles in a line (uses np.kron())
def H_heis3_np_kron():
    #iden is the identity matrix; sig_x, sig_y, and sig_z are Pauli matrices
    iden = np.eye(2,2)
    sig_x = np.array([[0,1],[1,0]])
    sig_y = np.array([[0,-1j],[1j,0]])
    sig_z = np.array([[0,1],[0,-1]])

    # Interactions (np.kron(A,B) is the tensor product of A and B)
    XXs = np.kron(iden, np.kron(sig_x, sig_x)) + np.kron(sig_x, np.kron(sig_x, iden))
    YYs = np.kron(iden, np.kron(sig_y, sig_y)) + np.kron(sig_y, np.kron(sig_y, iden))
    ZZs = np.kron(iden, np.kron(sig_z, sig_z)) + np.kron(sig_z, np.kron(sig_z, iden))

    # Sum interactions
    H = XXs + YYs + ZZs

    # Return Hamiltonian
    return H

# Returns matrix representation of the XXX Heisenberg model for 3 spin-1/2 particles in a line (uses opflow)
def H_heis3_opflow():

    # Interactions (I is the identity matrix; X, Y, and Z are Pauli matricies; ^ is a tensor product)
    XXs = (I^X^X) + (X^X^I)
    YYs = (I^Y^Y) + (Y^Y^I)
    ZZs = (I^Z^Z) + (Z^Z^I)

    # Sum interactions
    H = XXs + YYs + ZZs

    # Return Hamiltonian
    return H
```

Using opflow, however, the math is much easier to read and does not require recursive function calls, which can be confusing when generalizing a Hamiltonian to many qubits. Also, the operators do not need to be explicity computed in opflow saving memory. In opflow, the carrot symbol ```^``` denotes a tensor product. Important note, paranetheses are often needed to clarify the order of operations especially if doing other math operations such as ```+```. See below.

```python
# Example of incorrectly adding two PauliOp objects.
op = X^X + Z^Z
print('matrix dimensions of op:', op.to_matrix().shape)
```

    matrix dimensions of op: (8, 8)

The shape of ```op``` should be (4,4) since $X$ and $Z$ are 2x2, but without parentheses, the order of operations is not correct. Shown below is the correct way of adding the two tensored Pauli operators in opflow.

```python
# Example of correctly adding two PauliOp objects.
op = (X^X) + (Z^Z)
print('matrix dimensions of op:', op.to_matrix().shape)
```

    matrix dimensions of op: (4, 4)

## 3 Solution to the Time-Independent Schrödinger Equation

The [Schrödinger equation](https://en.wikipedia.org/wiki/Schrödinger_equation) is a foundational pillar of quantum mechanics. It relates how a quantum state $|\psi(t)\rangle$ changes in time given what the state was initially $|\psi(0)\rangle$. It's solution is relatively simple (more on how it's not simple in the next section) when the Hamiltonian $H$ that governs the state evolvution does not depend on time. Treating Schrödinger equation as the differential equation it is, we can use separation of variables to solve it

$$
\begin{align}
i \hbar \frac{d |\psi(t)\rangle}{dt} &= H|\psi(t)\rangle \\
|\psi(t)\rangle &= e^{-iHt / \hbar} |\psi(0)\rangle.
\end{align}
$$

## 4 Exponential of a Matrix
The solution to the [Schrödinger equation](https://en.wikipedia.org/wiki/Schrödinger_equation) for a Hamiltonian $H$ that does not depend on time (and $\hbar = 1$) is
$$
|\psi(t)\rangle = e^{-iHt}|\psi(0)\rangle.
$$
But, what does it mean to take $e$ to the power of an operator $H$? Let's briefly address this topic and give references where you can learn more.

Working with the the matrix representation of $H$, the time evolution $U_H(t) = e^{-iHt}$ can be numerically and algebraically evaluated using a [taylor series expansion](https://en.wikipedia.org/wiki/Taylor_series#Exponential_function) ($e^x = 1 + x + x^2/2! + x^3/3! + ... $) . This is often where computers step in. Although, single Pauli operators (e.g. $H=\sigma_x$) have a nice algebraic result. On a computer, many different software packages have methods for computing $e^A$ where $A$ is a matrix (see scipy's [expm](https://docs.scipy.org/doc/scipy/reference/generated/scipy.linalg.expm.html) or opflow's [.exp_i()](https://qiskit.org/documentation/tutorials/operators/01_operator_flow.html#Evolutions,-exp_i(),-and-the-EvolvedOp)).

We do need to be careful when the matrix that is being exponentiated is a sum of operators that do not commute ($[A,B] = AB - BA \neq 0$). For example, if $H = \sigma_x + \sigma_z$, we need to take care in working with $e^{it(\sigma_x + \sigma_z)}$ since the Pauli operatos $\sigma_x$ and $\sigma_z$ do not commute $[\sigma_x, \sigma_z] = \sigma_x \sigma_z - \sigma_z \sigma_x = -2i\sigma_y \neq0$. If we want to apply $e^{it(\sigma_x + \sigma_z)}$ on a quantum computer, however, we will want to decompose the sum in the exponential into a product of exponentials. Each product can then be implemented on the quantum computer as a single or two-qubit gate. Continuing the example, $\sigma_x$ and $\sigma_z$ do not commute ($[\sigma_x, \sigma_z] \neq 0$), so we cannot simply decompose the exponential into a product of exponentials $e^{-it(\sigma_x + \sigma_z)} \neq e^{-it\sigma_x}e^{-it\sigma_z}$. This is where approximation methods come in such as Trotterization (more on that in the next section).

## 5 Trotterization

The Open Science Prize notebook ends the decomposition of the unitary time evolution $U_{\text{Heis3}}(t)$ calculation with
$$
U_{\text{Heis3}}(t) \approx \left[XX^{(0,1)}(t/n) YY^{(0,1)}(t/n) ZZ^{(0,1)}(t/n) XX^{(1,2)}(t/n) YY^{(1,2)}(t/n) ZZ^{(1,2)}(t/n) \right]^{n}
$$
where $t$ is time and $n$ is the number of Trotter steps. (Remember, this is not the only possible Trotterization.)

The physical implementation of the two-qubit gates $XX(t)$, $YY(t)$, and $ZZ(t)$ are outlined in the Open Science Prize notebook, though without explanation. Here we provide a brief explanation with references where you can learn more.

Qiskit has a library of circuits that include such gates called the [circuit library](https://qiskit.org/documentation/apidoc/circuit_library.html), but not every gate in the library is native to a physical device. Nonnative gates (such as $XX$, $YY$, $ZZ$, or $ZX$) need to be decomposed into gates that are [native to the physical device](https://qiskit.org/documentation/tutorials/circuits_advanced/08_gathering_system_information.html#Configuration). You may even come across three-qubit gates like XXY or XYZ in some models. These interactions can be decomposition into native gates, or you may find it effective go further and [engineer a typically nonnative interaction](https://qiskit.org/textbook/ch-quantum-hardware/hamiltonian-tomography.html) using Pulse.

Assuming you want to only use native gates and not pulses, this review article is a useful starting point: F. Tacchino, et. al., *Quantum Computers as Universal Quantum Simulators: State-of-the-Art and Perspectives*, [Adv. Quantum Technol. *3* 3 (2020)](https://doi.org/10.1002/qute.201900052) \[[free arxiv version](https://arxiv.org/abs/1907.03505)\]

In this notebook, let's start with the [ZZ gate](https://qiskit.org/documentation/stubs/qiskit.circuit.library.RZZGate.html)
$$
\begin{align}
ZZ(2t) &= e^{-it\sigma_z \otimes \sigma_z} \\
ZZ(2t) &= \begin{bmatrix} e^{-it} & 0 & 0 & 0 \\ 0 & e^{it} & 0 & 0 \\ 0 & 0 & e^{it} & 0 \\ 0 & 0 & 0 & e^{-it} \\ \end{bmatrix}
\end{align}.
$$
This can be implemented in terms of two-qubit [CNOT gates](https://qiskit.org/documentation/stubs/qiskit.circuit.library.CXGate.html#qiskit.circuit.library.CXGate) sandwiching a single qubit rotation [$R_z(2t)$ rotation](https://qiskit.org/documentation/stubs/qiskit.circuit.library.RZGate.html) as shown below. (Note that $R_z$ is evaluated at the angle $2t$ instead of the typical $t$ to simplify writing the exponential terms.)

```python
from qiskit.circuit import Parameter
from qiskit import QuantumCircuit, QuantumRegister, execute

t = Parameter('t')
```

```python
# ZZ in two and single qubit gates
qc = QuantumCircuit(2)
qc.cnot(0,1)
qc.rz(2 * t, 1)
qc.cnot(0,1)

qc.draw(output='mpl')
```

For fun, let's check this mathematically by evaluating the circuit library ZZ gate and decomposed circuit at various time values.

```python
from qiskit.circuit.library import RXXGate, RYYGate, RZZGate, CXGate
from qiskit.quantum_info.operators import Operator

import qiskit.quantum_info as qi
```

```python
# time
time = np.pi/2  # try different time values as a check that the decomposition of ZZ into native gates is accurate

# Qiskit circuit library ZZ gate
qc = QuantumCircuit(2)
qc.append(RZZGate(time), [0,1])
qc_ZZ_matrix = qi.Operator(qc)

# Decomposed ZZ gate
CNOT = Operator(np.array([[1,0,0,0],[0,1,0,0],[0,0,0,1],[0,0,1,0]]))
Rz = Operator(np.array([[np.exp(-1j*time/2),0], [0,np.exp(1j*time/2)]]))
I_Rz = (Operator(I)^Rz)
#np_ZZ_matrix = CNOT@I_Rz@CNOT
np_ZZ_matrix = np.matmul(np.matmul(CNOT, I_Rz), CNOT)

# Confirm matrices are the same at time t
np.array_equal(qc_ZZ_matrix, np_ZZ_matrix)
```

    True

The $ZZ(t)$ gate can be rotated to the $XX(t)$ gate by adding single qubit rotations. Single qubit rotations such as $R_y(\pi/2) \sigma_z R_y(-\pi/2) = \sigma_x$ can transform $\sigma_z$ to $\sigma_x$. Consider computing the calculation yourself. Remember that Pauli matrices can be decomposed from an exponential form like so $R_z(2\theta)= \exp\left(-i\theta\sigma_z\right) = \cos(\theta)I - i \sin(\theta)\sigma_z$. Below is an abbreviated calculation to guide your own. Note that most tensor products with the identity have been omitted (e.g. $\sigma_z^{(0)} = \sigma_z^{(0)} \otimes I^{(1)}$), and for extra clarity, each operator has the qubit it acts on indexed in the superscript, so $\sigma_z^{(0)}$ would be the $\sigma_z$ operator acting on qubit $0$.

$$
\begin{align}
\left(R_y^{(0)}(\pi/2) \otimes R_Y^{(1)}(\pi/2)\right) ZZ(t)^{(0,1)}\left( R_y^{(0)}(-\pi/2) \otimes R_Y^{(1)}(-\pi/2)\right) &= \\
\exp\left[\frac{-i\pi}{4}(\sigma_y^{(0)} + \sigma_y^{(1)})\right] \exp\left[\frac{-it}{2} \sigma_z^{(0)} \otimes \sigma_z^{(1)}\right] \exp\left[\frac{i\pi}{4}(\sigma_y^{(0)} + \sigma_y^{(1)})\right] &= \\
\exp\left[\frac{-i\pi}{4}(\sigma_y^{(0)} + \sigma_y^{(1)})\right] \left[\cos(t/2)(I^{(0)} \otimes I^{(1)}) -i\sin(t/2)(\sigma_z^{(0)} \otimes \sigma_z^{(1)}) \right] \exp\left[\frac{i\pi}{4}(\sigma_y^{(0)} + \sigma_y^{(1)})\right] &= \\
\cos(t/2) \exp\left[\frac{-i\pi}{4}(\sigma_y^{(0)} + \sigma_y^{(1)})\right] (I^{(0)} \otimes I^{(1)}) \exp\left[\frac{i\pi}{4}(\sigma_y^{(0)} + \sigma_y^{(1)})\right] - i \exp\left[\frac{-i\pi}{4}(\sigma_y^{(0)} + \sigma_y^{(1)})\right] \sin(t/2)(\sigma_z \otimes \sigma_z) \exp\left[\frac{i\pi}{4}(\sigma_y^{(0)} + \sigma_y^{(1)})\right]  &= \\
\cos(t/2) (I^{(0)} \otimes I^{(1)}) - \frac{i \sin(t/2)}{4} \left[\left(I^{(0)} - i \sigma_y^{(0)}\right) \sigma_z^{(0)} \left(I^{(0)} + i \sigma_y^{(0)}\right)\right] \otimes \left[\left(I^{(1)} - i \sigma_y^{(1)}\right) \sigma_z^{(1)} \left(I^{(1)} + i \sigma_y^{(1)}\right)\right]  &= \\
\cos(t/2) (I^{(0)} \otimes I^{(1)}) - i \sin(t/2) (\sigma_x^{(0)} \otimes \sigma_x^{(1)})  &= \\
\exp\left[\frac{-it}{2} \sigma_x^{(0)} \otimes \sigma_x^{(1)}\right] &=  \\
&= XX(t) \\
\end{align}
$$

```python
# XX(t)
qc = QuantumCircuit(2)
qc.ry(np.pi/2,[0,1])
qc.cnot(0,1)
qc.rz(2 * t, 1)
qc.cnot(0,1)
qc.ry(-np.pi/2,[0,1])

qc.draw(output='mpl')
```

Let's check for fun again.

```python
# time
time = np.pi/6  # try different time values as a check that the decomposition of XX into native gates is accurate

# Qiskit circuit library XX gate
qc = QuantumCircuit(2)
qc.append(RXXGate(time), [0,1])
qc_XX_matrix = qi.Operator(qc).data

# Decomposed XX gate
Rz = Operator(np.array([[np.exp(-1j*time/2),0], [0,np.exp(1j*time/2)]]))
Ry = Operator(np.array([[np.cos(np.pi/4), -np.sin(np.pi/4)],[np.sin(np.pi/4), np.cos(np.pi/4)]]))
Ry_minus = Operator(np.array([[np.cos(-np.pi/4), -np.sin(-np.pi/4)],[np.sin(-np.pi/4), np.cos(-np.pi/4)]]))
I_Rz = (Operator(I)^Rz)

np_XX_matrix = (Ry^Ry)@CNOT@I_Rz@CNOT@(Ry_minus^Ry_minus)

# Confirm matrices are the same at time t
np.array_equal(np.round(qc_XX_matrix, 6), np.round(np_XX_matrix, 6))
```

    True

The $ZZ(t)$ gate can be rotated to the $YY(t)$ gate by adding single qubit rotations $R_x(\pi/2) \sigma_z R_x(-\pi/2) = -\sigma_y$ around the $ZZ$ gate just as was done with the $XX$ gate.

```python
# YY(t)
qc = QuantumCircuit(2)
qc.rx(np.pi/2,[0,1])
qc.cnot(0,1)
qc.rz(2 * t, 1)
qc.cnot(0,1)
qc.rx(-np.pi/2,[0,1])

qc.draw(output='mpl')
```

```python
# time
time = np.pi/6  # try different time values as a check that the decomposition of YY into native gates is accurate

# Qiskit circuit library YY gate
qc = QuantumCircuit(2)
qc.append(RYYGate(time), [0,1])
qc_YY_matrix = qi.Operator(qc).data

# Decomposed YY gate
Rz = Operator(np.array([[np.exp(-1j*time/2),0], [0,np.exp(1j*time/2)]]))
Rx = Operator(np.array([[np.cos(np.pi/4), -1j*np.sin(np.pi/4)],[-1j*np.sin(np.pi/4), np.cos(np.pi/4)]]))
Rx_minus = Operator(np.array([[np.cos(-np.pi/4), -1j*np.sin(-np.pi/4)],[-1j*np.sin(-np.pi/4), np.cos(-np.pi/4)]]))
I_Rz = (Operator(I)^Rz)

np_YY_matrix = (Rx^Rx)@CNOT@I_Rz@CNOT@(Rx_minus^Rx_minus)

# Confirm matrices are the same at time t
np.array_equal(np.round(qc_YY_matrix, 6), np.round(np_YY_matrix, 6))
```

    True

## 6 State Tomography
[State tomography](https://qiskit.org/documentation/tutorials/noise/8_tomography.html) is a method for determining the quantum state of a qubit, or qubits, even if the state is in a superposition or entangled. Repeatedly measuring a prepared quantum state may not be enough to determine the full state. For example, what if you want to know if the result of a quantum calculation is the state $|0\rangle -i|1\rangle$?

Repeated measurements in the $Z$-basis would return $|0\rangle$ 50% of the time and $|1\rangle$ 50% of the time. However, there are other states that give the same measurement result, such as $|0\rangle + |1\rangle$, $|0\rangle - |1\rangle$, and $|0\rangle + i|1\rangle$. How could you determine the state you have has the $-i$ phase you're looking for? This requires measurements in [different bases](https://qiskit.org/textbook/ch-states/single-qubit-gates.html#measuring). In state tomography, a quantum circuit is repeated with measurements done in different bases to exhaustively determine the full quantum state (including any phase information). The Open Science Prize this year is using this technique to determine the full quantum state after the quantum simulation. That state is then compared to the exact expected result to compute a fidelity. Although this fidelity only gives information on how well the quantum simulation produces one particular state, it's a more lightweight approach than a full [process tomography](https://qiskit.org/documentation/tutorials/noise/8_tomography.html) caluclation. In short, a high fidelity measured by state tomography doesn't gaurentee a high fidelity quantum simulation, but a low fidelity state tomography does imply a low fidelity quantum simulation.

# 7 Two-Qubit Gate with Pulse
[Qiskit Pulse](https://qiskit.org/documentation/apidoc/pulse.html) offers low-level control of a device's qubits. Pulse allows users to program the physical interactions happening on the superconducting chip. This can be a powerful tool for streamlining circuits \[1-4\], crafting new types of gates \[1-4\], getting [higher fidelity readout](https://www.youtube.com/watch?v=_1XTChcvbOs), and more. Of course, the increased level of control requires more understanding of [the physics of the qubit and how gates are physically generated on chip](https://qiskit.org/textbook/ch-quantum-hardware/transmon-physics.html). For someone new to Pulse, this may seem intimidating. However, there are great tutorials, and the [qiskit textbook](https://qiskit.org/textbook/ch-quantum-hardware/index-pulses.html) can slowly introduce you to these ideas with real and practically useful examples. For those more experienced with Pulse, it may be time to review papers such as this [general overview](https://arxiv.org/pdf/2004.06755.pdf). There will be other papers you find useful for this competition. Below, there is a small--and far from exhaustive--list to get you started \[1-4\].

To aid your efforts, we've provided a simple example of implementing a pulse-efficient [$R_{ZX}$ gate](https://qiskit.org/documentation/stubs/qiskit.circuit.library.RZXGate.html#qiskit.circuit.library.RZXGate) as outlined in paper [3](https://arxiv.org/pdf/2105.01063.pdf). Note that although the example below is interesting, it is not the only way to make a "pulse-efficient" two-qubit rotation gate.

\[1\] T. Alexander, et al., *Qiskit Pulse: Programming Quantum Computers Through the Cloud with Pulses*, [Quantum Sci. and Technol. **5** 044006 (2020)](https://arxiv.org/pdf/2004.06755.pdf)

\[2\] J. Stenger, et al., *Simulating the dynamics of braiding of Majorana zero modes using an IBM quantum computer*, [Phys. Rev. Research **3**, 033171 (2021)](https://arxiv.org/pdf/2012.11660.pdf)

\[3\] N. Earnest, et al., *Pulse-efficient circuit transpilation for quantum applications on cross-resonance-based hardware*, [arXiv:2105.01063 \[quant-ph\] (2021)](https://arxiv.org/pdf/2105.01063.pdf)

\[4\] S. Oomura, et al., *Design and application of high-speed and high-precision CV gate on IBM Q OpenPulse system*, [arXiv:2102.06117 \[quant-ph\] (2021)](https://arxiv.org/pdf/2102.06117.pdf)

[qiskit experiments repo](https://github.com/Qiskit/qiskit-experiments)

```python
# Import qiskit packages
from qiskit import IBMQ
from qiskit import schedule, transpile
from qiskit.tools.monitor import job_monitor

# load IBMQ Account data
# IBMQ.save_account(TOKEN)
provider = IBMQ.load_account()

provider = IBMQ.get_provider(hub='ibm-q-community', group='ibmquantumawards', project='open-science-22')
backend = provider.get_backend('ibmq_jakarta')
```

## 7.1 The [$R_{ZX}$ Gate](https://qiskit.org/documentation/stubs/qiskit.circuit.library.RZXGate.html#qiskit.circuit.library.RZXGate)
It's now time for an example of implementing a rotational two-qubit gate and how you might go about using Pulse to implement it as efficiently as possible. Remembering the dicussion in sections 4 and 5, we can write out the form of the $R_{ZX}(\theta)$ gate:

$$
R_{ZX}(2\theta) = \exp\left(-i \theta \sigma_z^{(0)} \otimes \sigma_x^{(1)}\right) = \cos(\theta)(I^{(0)}\otimes I^{(1)}) -i \sin(\theta)(\sigma_z^{(0)} \otimes \sigma_x^{(1)})
$$

Let's follow paper [\[3\]](https://arxiv.org/pdf/2105.01063.pdf)'s same line of reasoning. First, implement an $R_{ZX}$ using native circuit gates. Second, compare that circuit's performance to an $R_{ZX}$ gate contrusted from native pulses. Since the competition focuses on Jakarta's qubits 1, 3, and 5, let's just consider an $R_{ZX}$ gate between 1 and 3.

```python
# RZX gate circuit (see section 5 for details on why this is the circuit for RZX)
theta = 3*np.pi/11 # pick arbitrary rotation angle theta

qrs = QuantumRegister(4)
qc = QuantumCircuit(qrs)
qc.ry(-np.pi/2, 3)
qc.cx(1, 3)
qc.rz(theta, 3)
qc.cx(1, 3)
qc.ry(np.pi/2, 3)
print(qc.draw())

qct = transpile(qc, backend)  # display the typical circuit diagram
schedule(qct, backend).draw()  # display what the pulse schedule looks like for the circuit
```

Now that we have an $R_{ZX}$ circuit implementation (shown above in circuit and pulse schedule forms), let's make the "pulse-efficient" version of $R_{ZX}$. (Spoiler: it's only two pulses!)

```python
# The PassManager helps decide how a circuit should be optimized
# (https://qiskit.org/documentation/tutorials/circuits_advanced/04_transpiler_passes_and_passmanager.html)
from qiskit.transpiler import PassManager

# This function will pull pulse-level calibration values to build RZX gates and tell the PassManager to leave RZX gates alone
from qiskit.transpiler.passes import RZXCalibrationBuilderNoEcho
```

```python
# Equivalent circuit in terms of cross-resonance gates (https://qiskit.org/textbook/ch-quantum-hardware/cQED-JC-SW.html#6.-The-Cross-Resonance-Entangling-Gate)
qrs_pe = QuantumRegister(4)
qc_pe = QuantumCircuit(qrs_pe)
qc_pe.rzx(theta/2, 1, 3)
qc_pe.x(1)
qc_pe.rzx(-theta/2, 1, 3)
qc_pe.x(1)

# Add the pulse-level calibrations
pm = PassManager([RZXCalibrationBuilderNoEcho(backend)])
qc_pulse_efficient = pm.run(qc_pe)

schedule(qc_pulse_efficient, backend).draw()  # show pulse schedule
```

```python
# Compare the schedule durations
print('Duration of standard CNOT-based circuit:')
print(schedule(qct, backend).duration)
print('Duration of pulse-efficient circuit:')
print(schedule(qc_pulse_efficient, backend).duration)
```

    Duration of standard CNOT-based circuit:
    3776
    Duration of pulse-efficient circuit:
    1184

You've just optimized an $R_{ZX}$ gate using pulse-level control! The shorter circuit time for the pulse-efficient circuit is great news! The shorter the circuit, the less likely the qubit will decohere. This time savings, and fewer pulses to the qubits, will reduce errors. Let's quantify this reduction by measuring the [process fidelity](https://qiskit.org/documentation/tutorials/noise/8_tomography.html) of the two circuits. If we wanted to be thorough, we should do a [randomized benchmarking](https://qiskit.org/textbook/ch-quantum-hardware/randomized-benchmarking.html) as shown in Fig. 1 of paper [\[3\]](https://arxiv.org/pdf/2105.01063.pdf).

```python
# Process tomography functions
from qiskit.ignis.verification.tomography import process_tomography_circuits, ProcessTomographyFitter

# Get exact gate for comparison in process tomography
from qiskit.circuit.library import RZXGate

# Get the ideal unitary operator to compare to circuit output
target_unitary = qi.Operator(RZXGate(theta).to_matrix())

# Generate process tomography circuits and run
qpt_qcs = process_tomography_circuits(qc, [qrs[1], qrs[3]])
job = execute(qpt_qcs, backend, shots=8192)
```

```python
job_monitor(job)
```

    Job Status: job has successfully run

```python
# Extract tomography data
qpt_tomo = ProcessTomographyFitter(job.result(), qpt_qcs)

# Tomographic reconstruction
choi_fit_lstsq = qpt_tomo.fit(method='lstsq')
print('Average gate fidelity: F = {:.5f}'.format(qi.average_gate_fidelity(choi_fit_lstsq, target=target_unitary)))
```

    Average gate fidelity: F = 0.91322

Now for the pulse-efficient gate

```python
# Generate process tomography circuits and run
qpt_qcs = process_tomography_circuits(qc_pulse_efficient, [qrs_pe[1], qrs_pe[3]])
job = execute(qpt_qcs, backend, shots=8192)
```

```python
job_monitor(job)
```

    Job Status: job has successfully run

```python
# Extract tomography data so that counts are indexed by measurement configuration
qpt_tomo = ProcessTomographyFitter(job.result(), qpt_qcs)

# Tomographic reconstruction
choi_fit_lstsq = qpt_tomo.fit(method='lstsq')
print('Average gate fidelity: F = {:.5f}'.format(qi.average_gate_fidelity(choi_fit_lstsq, target=target_unitary)))
```

    Average gate fidelity: F = 0.92471

This is just one example of how Pulse could be used. Read [tutorials](https://qiskit.org/documentation/tutorials/circuits_advanced/05_pulse_gates.html), papers, and the [Qiskit youtube channel](https://www.youtube.com/c/qiskit) for more ideas.

```python
import qiskit.tools.jupyter
%qiskit_version_table
```

<h3>Version Information</h3><table><tr><th>Qiskit Software</th><th>Version</th></tr><tr><td><code>qiskit-terra</code></td><td>0.18.3</td></tr><tr><td><code>qiskit-aer</code></td><td>0.9.1</td></tr><tr><td><code>qiskit-ignis</code></td><td>0.6.0</td></tr><tr><td><code>qiskit-ibmq-provider</code></td><td>0.18.0</td></tr><tr><td><code>qiskit-aqua</code></td><td>0.9.5</td></tr><tr><td><code>qiskit</code></td><td>0.32.0</td></tr><tr><td><code>qiskit-nature</code></td><td>0.1.3</td></tr><tr><th>System information</th></tr><tr><td>Python</td><td>3.8.8 (default, Apr 13 2021, 12:59:45)
[Clang 10.0.0 ]</td></tr><tr><td>OS</td><td>Darwin</td></tr><tr><td>CPUs</td><td>8</td></tr><tr><td>Memory (Gb)</td><td>64.0</td></tr><tr><td colspan='2'>Mon Nov 29 09:01:00 2021 EST</td></tr></table>


## Optimal Power Flow
_**[Power Systems Optimization](https://github.com/east-winds/power-systems-optimization)**_

_by Michael R. Davidson, Jesse D. Jenkins, and Sambuddha Chakrabarti_

This notebook consists an introductory glimpse of and a few hands-on activities and demostrations of the Optimal Power Flow (OPF) problem—which minimizes the short-run production costs of meeting electricity demand from a given set of generators subject to various technical and flow limit constraints.

We will talk about a single-time period, simple generator, and line flow limit constraints (while modeling the network flows as dictated by the laws of physics). This is adds a layer of complexity and sophistication on top of the Economic Dispatch (ED) problem.

Since we will only discuss single time-period version of the problem, we will not be considering inter-temporal constraints, like ramp-rate limits. However, this model can easily be extended to allow for such constraints.

We will start off with some simple systems, whose solutions can be worked out manually without resorting to any mathematical optimization model and software. But, eventually we will be solving larger system, thereby emphasizing the importance of such software and mathematical models.

## Introduction to OPF

Optimal Power Flow (OPF) is a power system optimal scheduling problem which fully captures the physics of electricity flows, which adds a leyr of complexity, as well gives a more realistic version of the Economic Dispatch (ED) problem. It usually attempts to capture the entire network topology by representing the interconnections between the different nodes through transmission lines and also representing the electrical parameters, like the resistance, series reactance, shunt admittance etc. of the lines. however, the full-blown "AC" OPF turns out to be an extremely hard problem to solve (usually NP-hard). Hence, system operators and power marketers usually go about solving a linearized version of it, called the DC-OPF. The DC-OPF approximation works satisfactorily for bulk power transmission networks as long as such networks are not operated at the brink of instability or, under very heavily heavily loaded conditions.

## Single-time period, simple generator constraints
We will first examine the case where we are optimizing dispatch for a single snapshot in time, with only very simple constraints on the generators. $x^2$

$$
\begin{align}
		\mathbf{Objective\;Function:}\min_{P_g}\sum_{g\in{G}}C_{g}(P_{g})\longleftarrow\mathbf{power\;generation\; cost}\\
		\mathbf{Subject\;to:\:}{\underline{P}_{g}}\leqslant{P_{g}}\leqslant{{\overline{P}_{g}}},\;\forall{g\in{G}}\longleftarrow\mathbf{MW\; generation\; limits}\\
		P_{g(i)}-P_{d(i)}\longleftarrow\mathbf{real\; power\; injection}\notag\\=\sum_{j\in J(i)}B_{ij}(\theta_j-\theta_i),\;\forall{{i}\in\mathcal{N}}\\
		|P_{ij}|\leqslant{\overline{P}_{ij}},\;\forall{ij}\in{T}\longleftarrow\mathbf{MW\; line\; limit}\\
\end{align}
$$

The **decision variable** in the above problem is:

- $P_{g}$, the generation (in MW) produced by each generator, $g$
- $\theta_i$, $\theta_j$ the voltage phase angle of each bus/node, $i,j$

The **parameters** are:

- ${\underline{P}_{g}}$, the minimum operating bounds for the generator (based on engineering or natural resource constraints)
- ${\overline{P}_{g}}$, the maximum operating bounds for the generator (based on engineering or natural resource constraints)
- $P_{d(i)}$, the demand (in MW) at node $i$
- ${\overline{P}_{ij}}$, the line-flow limit for line connecting buses $i$ and $j$
- $B_{ij}$, susceptance for line connecting buses $i$ and $j$

just like the ED problem, here also, we can safely ignore fixed costs for the purposes of finding optimal dispatch.

With that, let's implement OPF.

# 1. Load packages¶

```julia
# New packages introduced in this tutorial (uncomment to download the first time)
import Pkg; Pkg.add("PlotlyBase")
using JuMP, GLPK
using Plots; plotly();
using VegaLite  # to make some nice plots
using DataFrames, CSV, PrettyTables
ENV["COLUMNS"]=120; # Set so all columns of DataFrames and Matrices are displayed
```

      Updating registry at `~/.julia/registries/General`
      Updating git-repo `https://github.com/JuliaRegistries/General.git`
    [1mFetching: [========================================>]  100.0 %.0 %13.1 %>                             ]  26.1 %Fetching: [================>                        ]  39.2 %                   ]  52.2 % [===========================>             ]  65.2 %78.2 % Resolving package versions...
     Installed LaTeXStrings ──────── v1.2.0
     Installed PlotlyBase ────────── v0.4.1
     Installed DocStringExtensions ─ v0.8.3
      Updating `~/.julia/environments/v1.3/Project.toml`
      [a03496cd] + PlotlyBase v0.4.1
      Updating `~/.julia/environments/v1.3/Manifest.toml`
      [ffbed154] + DocStringExtensions v0.8.3
      [b964fa9f] + LaTeXStrings v1.2.0
      [a03496cd] + PlotlyBase v0.4.1

    ┌ Info: Precompiling PlotlyBase [a03496cd-edff-5a9b-9e67-9cda94a718b5]
    └ @ Base loading.jl:1273

### 2. Load and format data

We will use data for IEEE 118 bus test case and two other test cases for a 3 bus and a 2 bus system:

- generator cost curve, power limit data, and connection-node
- load demand data with MW demand and connection node
- transmission line data with resistance, reactance, line MW capacity, from, and to nodes

```julia
datadir = joinpath("OPF_data")
# Note: joinpath is a good way to create path reference that is agnostic
# to what file system you are using (e.g. whether directories are denoted
# with a forward or backwards slash).
gen_info = CSV.read(joinpath(datadir,"Gen118.csv"), DataFrame);
line_info = CSV.read(joinpath(datadir,"Tran118.csv"), DataFrame);
loads = CSV.read(joinpath(datadir,"Load118.csv"), DataFrame);

# Rename all columns to lowercase (by convention)
for f in [gen_info, line_info, loads]
    rename!(f,lowercase.(names(f)))
end
```

```julia
#=
Function to solve Optimal Power Flow (OPF) problem (single-time period)
Inputs:
    gen_info -- dataframe with generator info
    line_info -- dataframe with transmission lines info
    loads  -- dataframe with load info
Note: it is always a good idea to include a comment blog describing your
function's inputs clearly!
=#
function OPF_single(gen_df, line_info, loads)
    OPF = Model(GLPK.Optimizer) # You could use Clp as well, with Clp.Optimizer

    # Define sets based on data
      # A set of all variable generators
    G_var = gen_df[gen_df[!,:is_variable] .== 1,:r_id]
      # A set of all non-variable generators
    G_nonvar = gen_df[gen_df[!,:is_variable] .== 0,:r_id]
      # Set of all generators
    G = gen_df.r_id
    # Extract some parameters given the input data
      # Generator capacity factor time series for variable generators
    gen_var_cf = innerjoin(gen_variable,
                    gen_df[gen_df.is_variable .== 1 ,
                        [:r_id, :gen_full, :existing_cap_mw]],
                    on = :gen_full)

    # Decision variables
    @variables(ED, begin
        GEN[G]  >= 0     # generation
        # Note: we assume Pmin = 0 for all resources for simplicty here
    end)

    # Objective function
    @objective(ED, Min,
        sum( (gen_df[i,:heat_rate_mmbtu_per_mwh] * gen_df[i,:fuel_cost] +
            gen_df[i,:var_om_cost_per_mwh]) * GEN[i]
                        for i in G_nonvar) +
        sum(gen_df[i,:var_om_cost_per_mwh] * GEN[i]
                        for i in G_var)
    )

    # Demand constraint
    @constraint(ED, cDemand,
        sum(GEN[i] for i in G) == loads[1,:demand])

    # Capacity constraint (non-variable generation)
    for i in G_nonvar
        @constraint(ED, GEN[i] <= gen_df[i,:existing_cap_mw])
    end

    # Variable generation capacity constraint
    for i in 1:nrow(gen_var_cf)
        @constraint(ED, GEN[gen_var_cf[i,:r_id] ] <=
                        gen_var_cf[i,:cf] *
                        gen_var_cf[i,:existing_cap_mw])
    end

    # Solve statement (! indicates runs in place)
    optimize!(ED)

    # Dataframe of optimal decision variables
    solution = DataFrame(
        r_id = gen_df.r_id,
        resource = gen_df.resource,
        gen = value.(GEN).data
        )

    # Return the solution and objective as named tuple
    return (
        solution = solution,
        cost = objective_value(ED),
    )
end
```

    OPF_single (generic function with 1 method)

```julia

```


ILI285/INF285 Computación Científica

COP-R

Initial Value Problem

Necesitamos encontrar una aproximación numérica de la siguiente Ecuación Diferencial Ordinaria de segundo orden para $t\in[0,T]$:

\begin{equation}
\begin{split}
y''(t) + y(t) &= 0,\\
y(0)=1,\; y'(0) &=0
\end{split}
\end{equation}

Antes de aplicar un método numérico, utilizaremos el siguiente cambio de variables:
\begin{align}
u &= y & \implies \quad u' &= y' =v\\
v &= y'& \implies \quad v' &= y''=-y=-u
\end{align}

para así generar $\mathbf{G}(t,\mathbf{u})=(v, -u)$, con $\mathbf{u}=(u,v)$. Por simplicidad se omite la variable temporal, pero tanto $u$ como $v$ dependen de $t$. Ahora, nuestro problema se puede reescribir de la siguiente manera:

\begin{equation}
\dot{\mathbf{u}} =
\begin{bmatrix}
\dfrac{du}{dt} \\ \dfrac{dv}{dt}
\end{bmatrix} =
\begin{bmatrix}
v \\ -u
\end{bmatrix} =
\mathbf{G}(t,\mathbf{u}).\tag{1}
\end{equation}

El problema $(1)$ puede ser resuelto numéricamente utilizando la regla del trapecio. Este método debe resolver la siguiente ecuación en cada iteración para $\mathbf{u}_{n+1}$:

\begin{equation}
\begin{split}
t_{n+1} &= t_{n} + h \\
\mathbf{u}_{n+1} &= \mathbf{u}_n + \frac{h}{2}
\left(\mathbf{G}(t_n, \mathbf{u}_{n}) + \mathbf{G}(t_{n+1},\mathbf{u}_{n+1})\right)
\end{split}
\end{equation}

donde $t_n = t_0 + h\,n$, con $n=0, 1, \dots, N$, $h=(t_N - t_0)/N$ y $[t_0=0,t_N=T]$ corresponden a una discretización de la variable temporal. Esta aproximación de la ecuación diferencial ordinaria $(1)$ tiene asociado un error numérico de orden $O(h^2)$, lo que es bastante bueno. En rigor, solo es necesario resolver la segunda ecuación en cada iteración es decir:

\begin{equation}
\mathbf{u}_{n+1} - \mathbf{u}_n - \frac{h}{2}
\left(\mathbf{G}(t_n, \mathbf{u}_{n}) + \mathbf{G}(t_{n+1},\mathbf{u}_{n+1})\right) = \mathbf{0},
\end{equation}

donde $\mathbf{u}_{n+1}$ es nuestro vector de incógnitas $(u_{n+1}, v_{n+1})$ y $\mathbf{u}_{n}=(u_n,v_n)$ son valores conocidos en la interación $n$. Otra forma de ver el problema es la siguiente:

\begin{equation}
\mathbf{F}(\mathbf{u}_{n+1}, \mathbf{u}_n) = \mathbf{0},
\end{equation}

la cual debe resolverse para cada iteración $n$, es decir, dado $\mathbf{u}_n$ debemos obtener $\mathbf{u}_{n+1}$.

**Consideración**: Este tipo de sistema dinámico se denomina autónomo porque si bien $u$ y $v$ dependen de $t$, no hay un efecto directo de $t$ en la función definida como $\mathbf{G}(t, \mathbf{u})$ por lo que se puede omitir y solo considerar $\mathbf{G}(\mathbf{u})$.

**Preguntas**
1. Obtenga $\|\mathbf{G}(\mathbf{u}_0)\|_2$, donde $\mathbf{u}_0=(u_0,v_0)=(y(0), y'(0))$
2. Obtenga $v_1$, considerando $t_0=0$, $t_N=2\pi$ y $N=50$, $\mathbf{u}_0=(u_0,v_0)=(y(0), y'(0))$.
3. Obtenga $u_{30}$, considerando $t_0=0$, $t_N=2\pi$ y $N=50$, $\mathbf{u}_0=(u_0,v_0)=(y(0), y'(0))$.

## Resolución

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

### Definición de $\mathbf{G}(\mathbf{u})=(v, -u)$

```python
def G(u):
    return np.array([u[1], -u[0]])
```

### Definición de $\mathbf{F}(\mathbf{u}_{n+1}, \mathbf{u}_n)$

\begin{equation}
    \begin{split}
        \mathbf{F}(\mathbf{u}_{n+1}, \mathbf{u}_n) & = \mathbf{u}_{n+1} - \mathbf{u}_n - \frac{h}{2}
            \left(\mathbf{G}(t_n, \mathbf{u}_{n}) + \mathbf{G}(t_{n+1},\mathbf{u}_{n+1})\right) \\
            &=
            \begin{bmatrix}
                u_{n+1} - u_{n} - 0.5\,h\,(v_n + v_{n+1})  \\
                v_{n+1} - v_{n} + 0.5\,h\,(u_n + u_{n+1})
            \end{bmatrix}\\
            &=
            \begin{bmatrix}
                1 & -0.5\, h \\
                0.5\, h & 1
            \end{bmatrix}
            \begin{bmatrix}
                u_{n+1} \\ v_{n+1}
            \end{bmatrix} -
            \begin{bmatrix}
                u_{n} + 0.5\,h v_{n} \\
                v_{n} - 0.5\,h u_{n} \\
            \end{bmatrix}
    \end{split}
\end{equation}

Como $\mathbf{F}(\mathbf{u}_{n+1}, \mathbf{u}_n)=\mathbf{0}$, esto no es más que un sistema de ecuaciones lineales

\begin{equation}
    \begin{split}
        \begin{bmatrix}
            1 & -0.5\, h \\
            0.5\, h & 1
        \end{bmatrix}
        \begin{bmatrix}
            u_{n+1} \\ v_{n+1}
        \end{bmatrix} &=
        \begin{bmatrix}
            u_{n} + 0.5\,h v_{n} \\
            v_{n} - 0.5\,h u_{n} \\
        \end{bmatrix} \\
        A\,\mathbf{x} &=\mathbf{b}
    \end{split}
\end{equation}

### Regla del Trapecio

Implementación de la **regla del trapecio**. Considerar que $A$ es fijo y solo depende de $h$. Además,
$\mathbf{b}$ se debe actualizar en cada iteración ya que depende de los valores de $\mathbf{u}_n$ y $\mathbf{G}(\mathbf{u}_n)$.

```python
def solveIVP(G, t, u0):
    N = t.shape[0]
    U = np.zeros((N, 2))
    U[0] = u0
    h = t[1] - t[0]
    A = np.array([[1, -0.5 * h], [0.5 * h, 1]])
    for n in range(N - 1):
        b = U[n] + 0.5 * h * G(U[n])
        U[n+1] = np.linalg.solve(A, b)
    return U
```

### Solución analítica

```python
y = lambda t: np.cos(t)
```

### Solución Numérica

```python
N = 50
t_0, t_N = 0, 2 * np.pi
t = np.linspace(t_0, t_N, N + 1)
u0 = np.array([1, 0])
```

```python
U = solveIVP(G, t, u0)
```

```python
plt.plot(t, y(t), 'b-', label=r"$y(t)$")
plt.plot(t, U[:,0], 'r--', label=r"$u(t)$")
plt.plot(t, U[:,1], 'g-.', label=r"$v(t)$")
plt.xlabel(r'$x$')
plt.ylabel(r'$y$')
plt.grid(True)
plt.legend()
plt.show()
```

### Preguntas

1. Obtenga $\|\mathbf{G}(\mathbf{u}_0)\|_2$, donde $\mathbf{u}_0=(u_0,v_0)=(y(0), y'(0))$.

```python
print(np.linalg.norm(G(u0)))
```

    1.0

2. Obtenga $v_1$, considerando $t_0=0$, $t_N=2\pi$ y $N=50$, $\mathbf{u}_0=(u_0,v_0)=(y(0), y'(0))$.

```python
print(U[1, 1])
```

    -0.1251695565411434

3. Obtenga $u_{30}$, considerando $t_0=0$, $t_N=2\pi$ y $N=50$, $\mathbf{u}_0=(u_0,v_0)=(y(0), y'(0))$.

```python
print(U[30, 0])
```

    -0.8119161913150047


# The Discrete-Time Fourier Transform

*This Jupyter notebook is part of a [collection of notebooks](../index.ipynb) in the bachelors module Signals and Systems, Comunications Engineering, Universität Rostock. Please direct questions and suggestions to [Sascha.Spors@uni-rostock.de](mailto:Sascha.Spors@uni-rostock.de).*

## Definition

The [discrete-time Fourier transform](https://en.wikipedia.org/wiki/Discrete-time_Fourier_transform) (DTFT) of a discrete signal $x[k]$ is defined as

\begin{equation}
X(e^{j \Omega}) = \sum_{k = -\infty}^{\infty} x[k] \, e^{-j \Omega k}
\end{equation}

where $\Omega \in \mathbb{R}$ denotes the normalized angular frequency. The DTFT maps a discrete signal $x[k]$ with $k \in \mathbb{Z}$ onto its continuous transform $X(e^{j \Omega}) = \mathcal{F}_* \{ x[k] \}$ with $\Omega \in \mathbb{R}$. It is frequently termed as *spectrum* of the discrete signal $x[k]$. The argument $e^{j \Omega}$ copes for the fact that the kernel $e^{-j \Omega k}$ of the DTFT is a [complex exponential signal](../discrete_signals/standard_signals.ipynb#Complex-Exponential-Signal) $z^{-k}$ with $z = e^{j \Omega}$. Other common definitions of the DTFT may be found in the literature. They differ with respect to the sign of the exponential function and normalization factors. The properties, theorems and transforms may differ from the ones given here, as a consequence.

A sufficient but not necessary condition for the existence of the DTFT is

\begin{equation}
\left|X(e^{j \Omega})\right| = \left| \sum_{k = -\infty}^{\infty} x[k] \, e^{-j \Omega k} \right|
\leq \sum_{k = -\infty}^{\infty} \left| x[k] \right| < \infty
\end{equation}

where the upper bound results from the [triangle inequality](https://en.wikipedia.org/wiki/Triangle_inequality). As sufficient condition for the existence of the DTFT, it follows that a signal $x[k]$ needs to be absolutely summable.

The DTFT is periodic with a period of $T_\text{p} = 2 \pi$,

\begin{equation}
X(e^{j \Omega}) = X(e^{j (\Omega + n \cdot 2 \pi)})
\end{equation}

for $n \in \mathbb{Z}$. This follows from the periodicity of its exponential kernel $e^{j \Omega k} = e^{j (\Omega + 2 \pi) k}$ for discrete $k \in \mathbb{Z}$. It is therefore sufficient to regard the DTFT in one period only. Typically the period $-\pi < \Omega < \pi$ is chosen. The information on the discrete signal $x[k]$ is contained in one period. As a consequence, the inverse discrete-time Fourier transform $x[k] = \mathcal{F}_*^{-1} \{ X(e^{j \Omega}) \}$ is defined as

\begin{equation}
x[k] = \frac{1}{2 \pi} \int_{-\pi}^{\pi} X(e^{j \Omega}) \, e^{j \Omega k} \; d \Omega
\end{equation}

### Transformation of the Dirac Impulse

The transform $\mathcal{F}_* \{ \delta[k] \}$ of the [Dirac impulse](../discrete_signals/standard_signals.ipynb#Dirac-Impulse) is derived by introducing $\delta[k]$ into the definition of the DTFT and exploiting the sifting property of the Dirac impulse

\begin{equation}
\mathcal{F}_* \{ \delta[k] \} = \sum_{k = -\infty}^{\infty} \delta[k] \, e^{-j \Omega k} = 1
\end{equation}

The transform of the Dirac impulse is equal to one. Hence, all normalized frequencies $\Omega$ are present with equal weight. This is an important property in the theory of discrete signals and systems, since the Dirac impulse is used to characterize linear time-invariant (LTI) systems by their [impulse response](../discrete_systems/impulse_response.ipynb) $h[k] = \mathcal{H} \{ \delta[k] \}$.

**Exercise**

* Derive the DTFT of a shifted Dirac impulse $\delta[k - \kappa]$.

### Transformation of the Rectangular Signal

The DTFT $X(e^{j \Omega}) = \mathcal{F}_* \{ \text{rect}_N[k] \}$ of the [rectangular signal](../discrete_signals/standard_signals.ipynb#Rectangular-Signal) is derived by introducing its definition into the definition of the DTFT. This results in

\begin{equation}
\mathcal{F}_* \{ \text{rect}[k] \} = \sum_{k = -\infty}^{\infty} \text{rect}[k] \, e^{-j \Omega k} =
\sum_{k = 0}^{N-1} e^{-j \Omega k} = e^{-j \Omega \frac{N-1}{2}} \cdot \frac{\sin \left(\frac{N \Omega}{2} \right)}{\sin \left( \frac{\Omega}{2} \right)}
\end{equation}

The latter equality has been derived by noting that the sum constitutes a [finite geometrical series](https://en.wikipedia.org/wiki/Geometric_series) with the common ratio $e^{-j \Omega}$. Note, that

\begin{equation}
\frac{\sin \left(\frac{N \Omega}{2} \right)}{\sin \left( \frac{\Omega}{2} \right)} \bigg\rvert_{\Omega = n \cdot 2 \pi} = N
\end{equation}

for $n \in \mathbb{Z}$ due to [L'Hôpital's rule](https://en.wikipedia.org/wiki/L'H%C3%B4pital's_rule).

**Example**

The DTFT $X(e^{j \Omega}) = \mathcal{F}_* \{ \text{rect}_N[k] \}$ of the rectangular signal is computed for a specific length $N$ by evaluating the finite series above using `SymPy`.

```python
%matplotlib inline
import sympy as sym
sym.init_printing()

W = sym.symbols('Omega', real=True)
k = sym.symbols('k', integer=True)

N = 5
X = sym.summation(sym.exp(-sym.I*W*k), (k, 0, N-1))
X
```

The magnitude $|X(e^{j \Omega})|$ and phase $\varphi(e^{j \Omega}) = \arg \{ X(e^{j \Omega}) \}$ of the spectrum is plotted for illustration

```python
sym.plot(sym.Abs(X), (W, -3*sym.pi, 3*sym.pi), xlabel=r'$\Omega$', ylabel=r'$| X(e^{j \Omega}) |$')
sym.plot(sym.arg(X), (W, -3*sym.pi, 3*sym.pi), xlabel=r'$\Omega$', ylabel=r'$\varphi(e^{j \Omega})$');
```

**Exercise**

* What happens to the magnitude/phase if you increase/decrease the length $N$ of the rectangular signal?

### Transformation of the Exponential Signal

The DTFT $X(e^{j \Omega}) = \mathcal{F}_* \{ e^{j \Omega_0 k} \}$ of the [harmonic exponential signal](../discrete_signals/standard_signals.ipynb#Complex-Exponential-Signal) with normalized frequency $\Omega_0$ is derived by introducing it into the definition of the DTFT. This results in

\begin{equation}
\mathcal{F}_* \{ e^{j \Omega_0 k} \} = \sum_{k = -\infty}^{\infty} e^{j \Omega_0 k} \, e^{-j \Omega k} =
\sum_{k = -\infty}^{\infty} e^{-j (\Omega - \Omega_0) k} = {\bot \!\! \bot \!\! \bot}\left( \frac{\Omega - \Omega_0}{2 \pi} \right)
\end{equation}

where for the last equality the [Fourier transform of the Dirac comb](../periodic_signals/spectrum.ipynb#The-Dirac-Comb) has been used. The DTFT of the exponential signal is a periodic series of shifted Dirac impulses, as

\begin{equation}
{\bot \!\! \bot \!\! \bot}\left( \frac{\Omega - \Omega_0}{2 \pi} \right) = 2 \pi \sum_{\mu = -\infty}^{\infty} \delta(\Omega - \Omega_0 - 2 \pi \mu)
\end{equation}

### Link to Fourier Transform of a Sampled Signal

The link between the Fourier transform of a sampled signal $x_\text{s}(t)$ and the DTFT of its discrete counterpart $x[k] = x(k T)$ is established in the following. Under the assumption of [ideal sampling](../sampling/ideal.ipynb#Model-of-Ideal-Sampling), the sampled signal reads

\begin{equation}
x_\text{s}(t) = \sum_{k = -\infty}^{\infty} x(k T) \cdot \delta(t - k T) = \sum_{k = -\infty}^{\infty} x[k] \cdot \delta(t - k T)
\end{equation}

where $x(t)$ denotes the continuous signal and $T$ the sampling interval. Introducing the sampled signal into the [definition of the Fourier transform](../fourier_transform/definition.ipynb) yields the spectrum of the sampled signal

\begin{equation}
X_\text{s}(j \omega) = \int_{-\infty}^{\infty} \sum_{k = -\infty}^{\infty} x[k] \cdot \delta(t - k T) \, e^{-j \omega t} \; dt = \sum_{k = -\infty}^{\infty} x[k] \, e^{-j \omega k T}
\end{equation}

where the last equality has been derived by changing the order of summation/integration and exploiting the [sifting property of the Dirac impulse](../continuous_signals/standard_signals.ipynb#Dirac-Impulse). Comparison with the definition of the DTFT yields

\begin{equation}
X_\text{s}(j \omega) = X(e^{j \Omega}) \big\rvert_{\Omega = \omega T}
\end{equation}

The spectrum of the sampled signal $X_\text{s}(j \omega)$ is equal to the DTFT of the discrete Signal $X(e^{j \Omega})$ for $\Omega = \omega T$. This result can be used to interpret the frequency axis of the DTFT.

### Interpretation of the Spectrum

It can be concluded from the definition of the inverse DTFT that a discrete signal $x[k]$ can be represented as a superposition of weighted harmonic exponential signals $X(e^{j \Omega}) \cdot e^{j \Omega k}$ for $-\pi < \Omega < \pi$. In general the spectrum $X(e^{j \Omega})$ will also have contributions for negative normalized angular frequencies $-\pi < \Omega < 0$. The concept of [negative frequencies](https://en.wikipedia.org/wiki/Negative_frequency) has no physical meaning. However, in the context of the DTFT with its complex kernel $e^{j \Omega k}$ negative frequencies are required to express complex and real-valued signals.

The DTFT of a discrete signal $x[k] \in \mathbb{C}$ is in general complex valued, $X(e^{j \Omega}) \in \mathbb{C}$. It is commonly represented by its real and imaginary part

\begin{equation}
X(e^{j \Omega}) = \Re \{ X(e^{j \Omega}) \} + j \cdot \Im \{ X(e^{j \Omega}) \}
\end{equation}

or by its magnitude and phase

\begin{equation}
X(e^{j \Omega}) = |X(e^{j \Omega})| \cdot e^{j \varphi(e^{j \Omega})}
\end{equation}

The magnitude spectrum $|X(e^{j \Omega})|$ provides insights into the composition of a signal in terms of its harmonic contributions. For a discrete signal which has been derived by sampling from a continuous signal, the normalized angular frequency $\Omega$ can be related to the

* angular frequency $\omega$ by $\Omega = \omega T$
* frequency $f$ by $\Omega = 2 \pi \frac{f}{f_\text{s}}$

where $T$ and $f_\text{s} = \frac{1}{T}$ denote the sampling interval and frequency, respectively. This result can be used to interpret the frequency of the DTFT as follows

**Copyright**

The notebooks are provided as [Open Educational Resource](https://de.wikipedia.org/wiki/Open_Educational_Resources). Feel free to use the notebooks for your own educational purposes. The text is licensed under [Creative Commons Attribution 4.0](https://creativecommons.org/licenses/by/4.0/), the code of the IPython examples under the [MIT license](https://opensource.org/licenses/MIT). Please attribute the work as follows: *Lecture Notes on Signals and Systems* by Sascha Spors.


# Covariate Shift

A fundamental assumption in almost all [supervised learning](https://en.wikipedia.org/wiki/Supervised_learning) methods is that training and test samples are drawn from the same [probability distribution](https://en.wikipedia.org/wiki/Probability_distribution). However, in practice, this assumption is rarely satisfied and standard machine learning models may not work as well as anticipated. [Covariate shift](https://www.quora.com/What-is-Covariate-shift) refers to the situation where the probability distribution of covariates changes between training and test data. *Shikhar Gupta*, Master's student in Data Science, gives the following great visual representation of this in [How Dissimilar are my Train and Test Data](https://towardsdatascience.com/how-dis-similar-are-my-train-and-test-data-56af3923de9b) in the **Towards Data Science** blog.

It is clear that if covariate shift is not accounted for correctly, it can lead to poor generalization.

So why are we even talking about covariate shift? Well, one of the causes of covariate shift is [sample selection bias](https://en.wikipedia.org/wiki/Selection_bias). [Dataset Shift in Machine Learning](http://www.acad.bg/ebook/ml/The.MIT.Press.Dataset.Shift.in.Machine.Learning.Feb.2009.eBook-DDU.pdf) states that "Sample selection bias occurs when the training data points {$x_i$} (the sample) do not accurately represent the distribution of the test scenario (the population) due to a selection process for each item that is (usually implicitly) dependent on the target variable $y_i$." If you recall, during [feature construction](3.0-build-features.ipynb), we made a fleeting mention of the selection bias that was introduced by the way we sampled the training and test / validation sets. The training set was selected to consist entirely of deceased physicists and the test / validation set was selected to consist entirely of living physicists. The feature building process has already hinted that the physicists in these datasets have different characteristics. Remember the different feature columns created by one-hot encoding?

The data was purposely sampled in this way due to the realization that the Nobel Prize in Physics cannot be awarded posthumously. The selection bias is an inherent part of the problem, as one of the goals of this project is to try to predict the next Physics Nobel Laureates, who obviously must be alive. As a result, the selection bias is something that we have to live with. The aim here is to see if we can formally detect whether a covariate shift occurs between the training and test data. It is important to note that we will be using the validation set as a *proxy* for the test data, since the true performance of the model will be evaluated on the latter, which is meant to be unseen data. Naturally, this doesn't take into account that there may also be a covariate shift between the validation and the test set. However, hopefully the random sampling process that we employed to divide the living physicists into the validation and test sets will have mitigated this.

```python
import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import make_scorer
from sklearn.metrics import matthews_corrcoef
from sklearn.model_selection import train_test_split
from sklearn.model_selection import GridSearchCV
from sklearn.utils import indices_to_mask

from src.features.features_utils import convert_categoricals_to_numerical
from src.models.metrics_utils import print_matthews_corrcoef
from src.stats.stats_utils import bootstrap_prediction
from src.stats.stats_utils import percentile_conf_int
from src.visualization.visualization_utils import plot_bootstrap_statistics
from src.visualization.visualization_utils import plot_logistic_regression_odds_ratio

%matplotlib inline
```

## Classifier Two-Sample Hypothesis Testing

A formal way of detecting whether there is a covariate shift between two samples (sets of identically and independently distributed examples) is to perform a [hypothesis test](https://en.wikipedia.org/wiki/Statistical_hypothesis_testing) known as a **two-sample test**. The goal of two-sample tests is to assess whether two samples, say $S_{train} \sim P$ and $S_{validation} \sim Q$, are drawn from the same probability distribution. Two-sample tests evaluate the difference between two distributions using the value of a [test statistic](https://en.wikipedia.org/wiki/Test_statistic) to either accept or reject the [null hypothesis](https://en.wikipedia.org/wiki/Null_hypothesis) $ H_0: P = Q$.

A rather elegant and informative way to perform a two-sample test is to train a binary classifier to distinguish between instances in $S_{train}$ and $S_{validation}$. The intuition is that if the null hypothesis is true, then the performance of such a binary classifier, as measured by an appropriate test statistic, will be approximately the same as random guessing. If the performance of such a classifier is better than chance-level, the null hypothesis is rejected in favor of the [alternative hypothesis](https://en.wikipedia.org/wiki/Alternative_hypothesis) $ H_1: P \neq Q$.

To test whether the null hypothesis $ H_0: P = Q$ is true, we will loosely follow the steps of *Lopez-Paz* and *Oquab* in [Revisiting Classifier Two-Sample Tests](https://arxiv.org/pdf/1610.06545). However, there are some notable differences to our approach here, which we will point out. Also, we will try not to over-burden the reader with heavy notation. We will be taking the following steps for both the original features and the features created from the topic modeling:

1. **Construct the dataset that is the union of the training and validation sets**, {$X = S_{train} \cup S_{validation}$, $y = y_{train} \cup y_{validation}$} where $X$ is the feature matrix and $y$ the target vector. Assign $y_{train} = 0$ and $y_{validation} = 1$ for all instances in the training and validation sets, respectively.
2. **Shuffle and split** $X$ **at random into disjoint training and test subsets** $X_{train}$ and $X_{test}$.
3. **Train a binary classifier** on $X_{train}$.
4. **Evaluate the performance of the binary classifier** by computing the Matthews Correlation Coefficient (MCC) as the test statistic on $X_{test}$:
\begin{equation}
\hat{t} \equiv MCC = \frac{TP \times TN - FP \times FN}{{\sqrt{(TP + FP)(TP + FN)(TN + FP)(TN + FN)}}}
\end{equation}
where TP is the number of true positives, TN the number of true negatives, FP the number of false positives and FN the number of false negatives. This differs from the classification accuracy test statistic that is used in the paper. Accuracy is not an appropriate metric for the same reasons mentioned during our creation of the [baseline model](5.0-baseline-model.ipynb). There are many more instances in the training set than the validation set. MCC is a metric that will account for this imbalance of classes.
5. **Accept or reject the null hypothesis** by computing a 95% [bootstrap](https://en.wikipedia.org/wiki/Bootstrapping_(statistics)) [confidence interval](https://en.wikipedia.org/wiki/Confidence_interval) for $\hat{t} \equiv MCC$. If the confidence interval contains the value of $\hat{t} \equiv MCC$ corresponding to chance-level performance (i.e. $MCC = 0$) then accept the null hypothesis, otherwise reject it. Again this differs from the [p-value](https://en.wikipedia.org/wiki/P-value) approach taken in the paper; non-parametrics are needed here as we do not know the distribution of $\hat{t} \equiv MCC$. Anyway, confidence intervals are more informative than p-values as they elucidate the magnitude and precision of the estimated effect. Furthemore, [p-values and confidence intervals always agree about statistical significance](http://blog.minitab.com/blog/adventures-in-statistics-2/understanding-hypothesis-tests-confidence-intervals-and-confidence-levels), so the substitution of a confidence interval for a p-value is warranted.

First, let's construct the dataset that is the union of the training and validation data, making sure to convert the categorical fields to a numerical form that is suitable for building machine learning models.

```python
train_features = pd.read_csv('../data/processed/train-features.csv')
train_features = convert_categoricals_to_numerical(train_features)
train_features.head()
```

```python
train_features_topics = pd.read_csv('../data/processed/train-features-topics.csv')
train_features_topics = convert_categoricals_to_numerical(train_features_topics)
train_features_topics.head()
```

```python
validation_features = pd.read_csv('../data/processed/validation-features.csv')
validation_features = convert_categoricals_to_numerical(validation_features)
validation_features.head()
```

```python
validation_features_topics = pd.read_csv('../data/processed/validation-features-topics.csv')
validation_features_topics = convert_categoricals_to_numerical(validation_features_topics)
validation_features_topics.head()
```

```python
X = train_features.append(validation_features)
assert(len(X) == len(train_features) + len(validation_features))
X.head()
```

```python
X_topics = train_features_topics.append(validation_features_topics)
assert(len(X_topics) == len(train_features_topics) + len(validation_features_topics))
X_topics.head()
```

```python
y = pd.Series(np.concatenate((np.zeros(len(train_features), dtype='int64'),
                              np.ones(len(validation_features), dtype='int64'))),
              index=train_features.index.append(validation_features.index))
assert(y.value_counts().equals(pd.Series([len(train_features), len(validation_features)], index=[0, 1])))
y.head()
```

Second, let's shuffle and split the dataset (in a stratified manner to maintain the class proportions) into disjoint training and test sets. We will use 80% of the data for training and 20% for testing.

```python
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, shuffle=True, stratify=y, random_state=0)
display(X_train.head())
display(X_test.head())
display(y_train.head())
y_test.head()
```

```python
X_train_topics = X_topics.loc[X_train.index, :]
X_test_topics = X_topics.loc[X_test.index, :]
display(X_train_topics.head())
X_test_topics.head()
```

Third, let's train a binary classifier on $X_{train}$. The choice of classifier is abitrary, so we will choose logistic regression, making sure to select the regularization parameter, $C$, and the regularization penalty, $L1$ or $L2$, via stratified 5-fold cross-validation. We will be using the MCC for scoring.

```python
def fit_logit_classifier(X, y):
    params = {'C': np.logspace(0, 1, 11), 'penalty': ['l1', 'l2']}
    mcc_scorer = make_scorer(matthews_corrcoef)
    clf = GridSearchCV(LogisticRegression(solver='liblinear'), param_grid=params, scoring=mcc_scorer,
                       cv=5, iid=False, return_train_score=True)
    clf.fit(X, y)
    return clf
```

```python
logit = fit_logit_classifier(X_train, y_train)
logit.best_params_, logit.best_estimator_
```

```python
logit_topics = fit_logit_classifier(X_train_topics, y_train)
logit_topics.best_params_, logit_topics.best_estimator_
```

Fourth, evaluate the performance of the classifier on $X_{test}$ using the MCC.

```python
print_matthews_corrcoef(
    matthews_corrcoef(y_train, logit.best_estimator_.predict(X_train)), 'Features', data_label='train')
test_mcc = matthews_corrcoef(y_test, logit.best_estimator_.predict(X_test))
print_matthews_corrcoef(test_mcc, 'Features', data_label='test')
print_matthews_corrcoef(
    matthews_corrcoef(y_train, logit_topics.best_estimator_.predict(X_train_topics)), 'Topics',
    data_label='train')
test_topics_mcc = matthews_corrcoef(y_test, logit_topics.best_estimator_.predict(X_test_topics))
print_matthews_corrcoef(test_topics_mcc, 'Topics', data_label='test')
```

Fifth, accept or reject the null hypothesis $ H_0: P = Q$ by computing a 95% bootstrap confidence interval.

```python
n_estimators = 1000
max_samples = 0.8
n_jobs = -1
alpha = 0.05
mccs = bootstrap_prediction(
    X, y, base_estimator=logit.best_estimator_, score_func=matthews_corrcoef, n_estimators=n_estimators,
    max_samples=max_samples, n_jobs=n_jobs, random_state=2)
conf_int = percentile_conf_int(mccs, alpha=alpha)
```

```python
stat_label = 'Matthews Correlation Coefficient (MCC)'
ax = plot_bootstrap_statistics(
    mccs, test_mcc, conf_int, alpha, 'test MCC:', stat_label,
    title='Features Bootstrap Matthews Correlation Coefficient (MCC) \nfor 1000 samples')
ax.set_xlim(0, 0.5)
ax.set_ylim(0, 300);
```

```python
mccs = bootstrap_prediction(
    X_topics, y, base_estimator=logit_topics.best_estimator_, score_func=matthews_corrcoef,
    n_estimators=n_estimators, max_samples=max_samples, n_jobs=n_jobs, random_state=3)
conf_int = percentile_conf_int(mccs, alpha=alpha)
```

```python
ax = plot_bootstrap_statistics(
    mccs, test_topics_mcc, conf_int, alpha, 'test MCC:', stat_label,
    title='Topics Bootstrap Matthews Correlation Coefficient (MCC) \nfor 1000 samples')
ax.set_xlim(0, 0.4)
ax.set_ylim(0, 300);
```

The figures illustrate that the distributions of the MCCs for the bootstrap samples are Gaussian-like for both the original features and the topics features. The MCC for the test set of the full dataset, along with the upper and lower values of the 95% confidence intervals, are shown. It is clear that for both sets of features, the confidence interval does not contain the value of chance-level performance ($MCC = 0$). Hence in both cases, there is sufficient evidence to reject the null hypothesis in favor of the alternate hypothesism, $ H_1: P \neq Q$. The conclusion is that the training and validation sets are drawn from different distributions. In other words, there is a covariate shift in both feature sets. We can see that the severity of the covariate shift has been reduced significantly by constructing features with topic modeling.

## Shifting Predictors

We can determine the predictors that exhibit a covariate shift by looking at the coefficients of the logistic regression models. Each coefficient represents the impact that the *presence* vs. *absence* of a predictor has on the [log odds ratio](https://en.wikipedia.org/wiki/Odds_ratio#Role_in_logistic_regression) of a physicist being from the validation set (as opposed to being from the training set). The change in [odds ratio](https://en.wikipedia.org/wiki/Odds_ratio) for each predictor can simply be computed by exponentiating its associated coefficient.

Formally, a change in odds ratio of 1 for a particular predictor indicates that it is not shifting, whereas a value greater than 1 indicates a shift. As it is likely that a lot of predictors will have odds ratios of slightly over 1, we will loosely define a shifting predictor as one that has a change in odds ratio greater than 1.2. This will give us an idea of the predictors that contribute the most to the covariate shift in the data. These are plotted in the charts below for the two sets of predictors.

```python
ax = plot_logistic_regression_odds_ratio(
    logit.best_estimator_.coef_, top_n=1.2, columns=X.columns, title='Features covariate shifting',
    plotting_context='talk')
ax.figure.set_size_inches(20, 30)
```

```python
ax = plot_logistic_regression_odds_ratio(
    logit_topics.best_estimator_.coef_, top_n=1.2, columns=X_topics.columns,
    title='Topics covariate shifting')
```

The charts certainly make intuitive sense as they illustrate the following changes over time:

1. Increase in globalization of physics.
2. Concentration of physics in the hubs of North America and Europe.
3. Coming to prominence of major American institutions as places of study and work.
4. Broadening of the research fields of theoretical physics and astronomy.

Now that we know which features are shifting, what should be done about it? A simple solution could be to drop these features. However, this would result in some loss of information and we do not yet know if these features are important in predicting our target. Furthermore, this would raise the question as to what should be the minimum change in odds ratio for identifying drifting features. The value of 1.2 chosen above was rather *ad hoc* as it was intended only to illustrate the features that contribute the most to the covariate shift in the data. It is clear that a more principled approach is needed to deal with the covariate shift in the data.


# Beispielprogramm: Matrixwertige Exponentialfuntion

Zu einer reellwertigen Matrix betrachten wir ein Anfangswertproblem für die lineare DGL
\begin{align}
\dot {\vec u}(t) &:= A {\vec u(t)},\quad t>0\\
\vec u(0)&= \vec u_0
\end{align}
Dessen Lösung ist durch $\vec u(t) = \exp(A t) \vec u_0$
explizit gegeben.

In der Vorlesung wurde behandelt, dass das Verhalten der Lösung durch die **Eigenwerte von A** bestimmt ist. Wir konzentrieren uns im folgenden auf den Spezialfall
$$A := \begin{pmatrix} a & b \\ c & d \end{pmatrix} \in \mathbb R^{2\times 2}$$
Aus diesen Fall kann bereits vieles abgeleitet werden.
Zunächst definieren wir die Matrix (**TODO:** Bitte ggf. ändern!):

```octave
A = [3 1.0; -1.0 -3]
```

    A =

       3   1
      -1  -3

Die Eigenwerte ergeben sich über die Nullstellen des charakteristischen Polynoms:

$$ 0= det(A - \lambda I)  = \begin{vmatrix} a-\lambda & b \\ c & d-\lambda \end{vmatrix} = (a-\lambda )(d-\lambda )-bc  = \lambda^2 - \lambda (a+b) + (ad - bc)$$
Über die $pq$-Formel sieht man, dass sich entweder (i) zwei reellwertige Eigenwerte $\lambda_1, \lambda_2 \in\mathbb R$
oder (ii) zwei komplex-konjugiert Eigenwerte
$$\lambda_{1/2} = \alpha \pm i \beta \in \mathbb C,$$
mit Real- und Imaginärteil $\alpha, \beta \in \mathbb R$ ergeben. Wir berechnen diese mit Octave:

```octave
[V,Lambda]=eig(A);
Lambda
V

alpha =  real(Lambda(1,1))
alpha2 =  real(Lambda(2,2))

beta = abs(imag(Lambda(1,1)))

```

    Lambda =

    Diagonal Matrix

       2.8284        0
            0  -2.8284

    V =

       0.98560  -0.16910
      -0.16910   0.98560

    alpha =  2.8284
    alpha2 = -2.8284
    beta = 0

Für den Eigenwert $\lambda = \alpha + i \beta$ folgt mit der Eulerschen Formel die Gleichung
$$
\exp(\lambda t) = \exp(\alpha t) \cdot \exp(i \beta t) =  \exp(\alpha t) \cdot \left( \cos (\beta t) + i \sin (\beta t) \right)
$$
Somit bestimmt der Realteil $\alpha$ das Wachstumsverhalten $\exp(\alpha t)$, während der Imaginärteil $\beta$ zu Oszillationen $\sin (\beta t)$ bzw. $\cos (\beta t)$ führt. Entsprechend definieren wir nachfolgend charakteristische **Zeitkonstanten**. Diese sind für Wachstum und Oszillationen durch $1/|Re \lambda|$ bzw. $2 \pi/|Im \lambda|$ gegeben. Um jeweils den schnellsten Prozess auflösen zu können, betrachten wir insgesamt das  Minimum.

```octave
tGrowth = abs(1.0/real(Lambda(1,1)))
tGrowth2 = abs(1.0/real(Lambda(2,2)))

if (beta > 0)
    # Ein Wachstums-/Zerfallprozess mit Oszillation
    tOscillation = 2.0*pi/beta
    tChar = min(tGrowth,tOscillation)
else
    # Zwei Wachstums-/Zerfallprozesse
    tChar=min(tGrowth, tGrowth2)
endif

```

    tGrowth =  0.35355
    tGrowth2 =  0.35355
    tChar =  0.35355

Obwohl die Lösung explizite Lösung der DGL bekannt ist, lösen wir numerisch ;-):

```octave
function udot=f_rhs(u,t, A)
    udot = A*u;
endfunction

u0 = [10,40];
tdiskret = linspace(0, 3.0*tChar, 500);
uDiskret = lsode(@(u,t) f_rhs(u,t,A), u0, tdiskret);
```

Wir wollen nun die Lösung auch graphisch darstellen:

```octave
# Funktion zur Ausgabe des Vektorfelds
function plot_field_2D(myrhs, spaceX1, spaceX2, name)

	 for j=1:length(spaceX2)
    	     for i=1:length(spaceX1)
    	     	 vec = myrhs([spaceX1(i);spaceX2(j)], 0.0);
        	 dX1(j,i) = vec(1);
        	 dX2(j,i) = vec(2);
    	     end
	end

	quiver(spaceX1,spaceX2,dX1,dX2);
endfunction
```

```octave
# Plot um Ursprung
min1 = min(uDiskret(:,1));
max1 = max(uDiskret(:,1));
abs1 = max(abs(min1), abs(max1));

min2 = min(uDiskret(:,2));
max2 = max(uDiskret(:,2));
abs2 = max(abs(min2), abs(max2));

spaceX1=linspace(-abs1, abs1, 20);
spaceX2=linspace(-abs2, abs2, 20);

hold on
xlabel("Komponente u_1")
ylabel("Komponente u_2")

plot_field_2D(@(u,t) f_rhs(u,t,A), spaceX1, spaceX2, "Zeitliche Entwicklung")
plot(uDiskret(:,1), uDiskret(:,2), "Color", "black")
plot (u0(1), u0(2), "Color", "black")

legend("Richtungsfeld", "Loesung", "Anfangswert")
```

Das Vorzeichen von $Re \lambda$ entscheidet darüber, ob ein Wachstums- oder Zerfallsprozess vorliegt. Für komplexe $\lambda$ erhalten wir, je nach Vorzeigen des Realteils, eine gedämpfte ($Re \lambda<0$) oder angefachte ($Re \lambda>0$) Schwingung.

```octave
xlabel("Zeit t")
ylabel("Komponente u_1, u_2")

hold on
plot(tdiskret, uDiskret(:,1), "Color", "blue")
plot(tdiskret, uDiskret(:,2), "Color", "red")

# Fuer komplexe Eigenwerte plotten wir die einhuellende Exponentialfunktion
if (beta > 0)
uGrowth = abs(norm(u0))*exp(tdiskret/tGrowth);
plot(tdiskret, uGrowth, "Color", "Black")
plot(tdiskret, -uGrowth, "Color", "Black")
legend("Komponente u_1", "Komponente u_2", "Einhuellende")
else
legend("Komponente u_1", "Komponente u_2")
endif

```

```octave

```


# Problem Model

Let the rocket have the mass $m$, velocity $v$ and altitude $h$. It will be subject to the gravitational acceleration $g$ and aerodynamic drag $D$. The engine thrust $u \in [0,u_{max}]$ is the control input and it naturally consumes propellant causing the mass $m$ to decrease, at a rate proportional to $\gamma=1/(g_0I_{sp})$.

$$
\begin{align}
\dot{h}&=v\\
\dot{v}&=(u-D)/m-g\\
\dot{m}&=-\gamma u
\end{align}
$$

The system is assumed to be fully observable, and the mass at start is $m_0$ and $m_1$ at $t_f$. Then,

$$
v(0) = v_0, \hspace{0.5cm} h(0) = h_0, \hspace{0.5cm} m(0) = m_0, \hspace{0.5cm} m(t_f) = m_1
$$

where $t_f$ is the final time. The optimization objective is,

$$
\max_{u(t)} h(t_f)
$$

subject to the aforementioned relations.

# Solutions

It is, for some drag and gravitational models, possible to find an analytical solution the the problem. In particular, with no drag the optimal solution is simply to apply maximum possible thrust all the time until all fuel is spent. In general, a solution can be found with the help of Pontryagin's maximum principle.

Assume that the aerodynamic drag depends on velocity and altitude, and that the gravity depends on altitude, $D=D(v,h)$, $g=g(h)$.

The Hamiltonian $H$ we seek to minimize is,

$$
H = \lambda_1 \left(\frac{u-D(v,h)}{m}-g(h)\right) + \lambda_2v - \lambda_3\gamma u
$$

with the Lagrange multiplier equations,

$$
\begin{align}
\dot{\lambda}_1&=\frac{D_v}{m}\lambda_1 - \lambda_2, &\lambda_1(t_f) &= 0\\
\dot{\lambda}_2&=\left(\frac{D_h}{m} + g_h\right)\lambda_1, &\lambda_2(t_f) &= -n_0+\lambda_1g_h\\
\dot{\lambda}_3&=\frac{u-D(v,h)}{m^2}\lambda_1, &\lambda_3(t_f) &= \mu_3
\end{align}
$$

The coefficient of the control input $u$ in the Hamiltonian is

$$
\sigma(t) = \frac{\lambda_1}{m}-\lambda_3\gamma
$$

Assume the form,

$$
\sigma(t) =
\begin{cases}
<0 & 0 \le t < t_1 \\
=0 & t_1 \le t < t_2 \\
>0 & t_2 \le t \le t_f
\end{cases}
$$

Minimization of $H$ implies that,

$$
u(t) =
\begin{cases}
u_{max} & 0 \le t < t_1 \\
0 & t_2 \le t \le t_f
\end{cases}
$$

The singular interval $[t_1, t_2)$ is less straightforward. There $\sigma(t) \equiv 0$, meaning that also $\dot{\sigma}(t)=\ddot{\sigma}(t)=\dddot{\sigma}(t) \equiv 0$ etc.

$$
\dot{\sigma}(t)=\frac{1}{m^2} \left( (D_v+\gamma D)\lambda_1 - m\lambda_2\right) \equiv 0
$$

Substitute with $\lambda_1=\gamma m \lambda_3$,

$$
\gamma (D_v+\gamma D) \lambda_3 - \lambda_2 \equiv 0
$$

Now remember $\ddot{\sigma}(t) \equiv 0$,

$$
\dot{\lambda}_3 \tilde{D} + \lambda_3 \frac{d\tilde{D}}{dt} - \frac{1}{\gamma} \dot{\lambda}_2 \equiv 0
$$

where $\tilde{D} = D_v+\gamma D$. By further substitution and rearrangements an expression for $u$ in feedback form can be obtained,

$$
u(t) = D + mg + m\frac{D_h-g\gamma \tilde{D} - \tilde{D}_h v + m g_h}{\gamma \tilde{D} + \tilde{D}_v}
$$

The condition for the singular solution is,

$$
-D -mg + \tilde{D}v = 0
$$

For details see [1].

The solution will be of the form $u(t) = u_{max}$ until the condition above is fulfilled, then the singular trajectory shall be used until all the fuel is spent, then $u(t) = 0$. If the condition is never met, $u(t)$ then switches directly to $u(t) = 0$ when out of fuel. That happens e.g. when there is no drag.

[1] T. Glad, L. Ljung, Control Theory - Multivariable and Nonlinear Methods, Taylor & Francis, 2000

An implementation of this controller is provided in <optimal.py>

```python
import numpy as np
from goddard_env import GoddardEnv
from optimal import DefaultControlled, OptimalController

rocket = DefaultControlled()
env = GoddardEnv(rocket)
oc = OptimalController(rocket)

state = env.reset()

state_log = [state]
extra_log = []

time = np.arange(0, 0.25, rocket.DT)

for _ in time:
    v, h, m = state
    (state, _, _, extra) = env.step(action=[oc.control(v, h, m)])
    state_log.append(state)
    extra_log.append(list(extra.values())[1:])

print("Maximum altitude reached: {}".format(env.maximum_altitude()))

import matplotlib.pyplot as plt

state_log = np.array(state_log)

f, (ax1, ax2, ax3, ax4) = plt.subplots(4, figsize=(12, 10))

ax1.plot(time, state_log[:-1,1])
ax1.grid(True)
ax1.set(xlabel='time [s]', ylabel='Altitude')

ax2.plot(time, state_log[:-1,0])
ax2.grid(True)
ax2.set(xlabel='time [s]', ylabel='Velocity')

ax3.plot(time, state_log[:-1,2])
ax3.grid(True)
ax3.set(xlabel='time [s]', ylabel='Rocket Mass')

ax4.plot(time, extra_log)
ax4.grid(True)
ax4.set(xlabel='time [s]', ylabel='Forces')
ax4.legend(env.extras_labels()[1:], loc='upper right')

plt.show()

env.close()
```


---

# Chapter 5: Statistics

---

## Descriptive Statistics
Usually, a normal primary tool to inlustrate the data is the Histogram, that can describes many statistic values.
The book introduces manual Histogram by splits the ``num_friedns`` into 100 bins. I modify some code to my style.

```python

num_friends = [100.0,49,41,40,25,21,21,19,19,18,18,16,15,15,15,15,14,14,13,13,13,13,12,12,11,
               10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,
               9,9,8,8,8,8,8,8,8,8,8,8,8,8,8,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,6,6,6,6,6,6,6,6,6,
               6,6,6,6,6,6,6,6,6,6,6,6,6,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,4,4,4,4,4,4,4,4,4,
               4,4,4,4,4,4,4,4,4,4,4,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,
               2,2,2,2,2,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]

from collections import Counter
import matplotlib.pyplot as plt

friend_counts = Counter(num_friends)
upper_X = int(max(num_friends) + 1)

xs = range(upper_X)                     # largest value is 100
ys = [friend_counts[x] for x in xs]     # height is just # of friends
upper_Y = max(ys) + 3
plt.bar(xs, ys)
plt.axis([0, upper_X, 0, upper_Y])
plt.title("Histogram of Friend Counts")
plt.xlabel("# of friends")
plt.ylabel("# of people")
plt.show()
```

```python
plt.bar(xs, ys,edgecolor='purple', color='None')
plt.show()
```

```python

```

```python

```

## Descriptive Statistic

The book shows ways to manual calculates descriptive statistics. Note that the ``linear_algebra.py`` from chapter four is copied to the note book directory.
* **ranking**: sorted data then extract the ranks
* **`mean(data)`**   : summary of data divided by number of data
* **`median(data)`** : the middle between max and min, the number or shape of data impacts directly this value such odd or even number of data
* **`quatile(data,p)`**: devides data into 10 ranges, quatile 4 is equals to percentil 25 equlas to decentile 2.5. Argument `p` is the decentile
* **`mode(data)`**   : the data points that most frequency
* **`data_range(data)`**  : the maximum minus minimum value
* **`variance(data)`**: variacne, sum sqaure of each data and mean devid by number of data minus degree of freedom, mostly use  N-1
* **`standard_deviation(data)`**: root of variance
* **`interquartile_range`** : Range between Third Quatile and Second Quatile, represent the middile reange of data

```python

num_points = len(num_friends)               # 204

assert num_points == 204

largest_value = max(num_friends)            # 100
smallest_value = min(num_friends)           # 1
range_value = largest_value - smallest_value

assert largest_value == 100
assert smallest_value == 1
assert range_value == 99

# ranking
sorted_values = sorted(num_friends)
smallest_value = sorted_values[0]           # 1
second_smallest_value = sorted_values[1]    # 1
second_largest_value = sorted_values[-2]    # 49

assert smallest_value == 1
assert second_smallest_value == 1
assert second_largest_value == 49

from typing import List

def mean(xs: List[float]) -> float:
    return sum(xs) / len(xs)

mean(num_friends)   # 7.333333

assert 7.3333 < mean(num_friends) < 7.3334

# The underscores indicate that these are "private" functions, as they're
# intended to be called by our median function but not by other people
# using our statistics library.
def _median_odd(xs: List[float]) -> float:
    """If len(xs) is odd, the median is the middle element"""
    return sorted(xs)[len(xs) // 2]

def _median_even(xs: List[float]) -> float:
    """If len(xs) is even, it's the average of the middle two elements"""
    sorted_xs = sorted(xs)
    hi_midpoint = len(xs) // 2  # e.g. length 4 => hi_midpoint 2
    return (sorted_xs[hi_midpoint - 1] + sorted_xs[hi_midpoint]) / 2

def median(v: List[float]) -> float:
    """Finds the 'middle-most' value of v"""
    return _median_even(v) if len(v) % 2 == 0 else _median_odd(v)

assert median([1, 10, 2, 9, 5]) == 5
assert median([1, 9, 2, 10]) == (2 + 9) / 2

assert median(num_friends) == 6

def quantile(xs: List[float], p: float) -> float:
    """Returns the pth-percentile value in x"""
    p_index = int(p * len(xs))
    return sorted(xs)[p_index]

assert quantile(num_friends, 0.10) == 1
assert quantile(num_friends, 0.25) == 3
assert quantile(num_friends, 0.75) == 9
assert quantile(num_friends, 0.90) == 13

def mode(x: List[float]) -> List[float]:
    """Returns a list, since there might be more than one mode"""
    counts = Counter(x)
    max_count = max(counts.values())
    return [x_i for x_i, count in counts.items()
            if count == max_count]

assert set(mode(num_friends)) == {1, 6}

# "range" already means something in Python, so we'll use a different name
def data_range(xs: List[float]) -> float:
    return max(xs) - min(xs)

assert data_range(num_friends) == 99

from linear_algebra import sum_of_squares

def de_mean(xs: List[float]) -> List[float]:
    """Translate xs by subtracting its mean (so the result has mean 0)"""
    x_bar = mean(xs)
    return [x - x_bar for x in xs]

def variance(xs: List[float]) -> float:
    """Almost the average squared deviation from the mean"""
    assert len(xs) >= 2, "variance requires at least two elements"

    n = len(xs)
    deviations = de_mean(xs)
    return sum_of_squares(deviations) / (n - 1)

assert 81.54 < variance(num_friends) < 81.55

import math

def standard_deviation(xs: List[float]) -> float:
    """The standard deviation is the square root of the variance"""
    return math.sqrt(variance(xs))

assert 9.02 < standard_deviation(num_friends) < 9.04

def interquartile_range(xs: List[float]) -> float:
    """Returns the difference between the 75%-ile and the 25%-ile"""
    return quantile(xs, 0.75) - quantile(xs, 0.25)

assert interquartile_range(num_friends) == 6

```

### Demonstrate the statistics values
Let's try to use it and then represent it in the graph.

```python
Ranges = data_range(num_friends)
Median = median(num_friends)
Mean = mean(num_friends)
_, Mode = mode(num_friends)
First_Quatile = quantile(num_friends,0.25)
Third_Quatile = quantile(num_friends,0.75)
IQR =interquartile_range(num_friends)
STD_DEV = standard_deviation(num_friends)
print(Third_Quatile -  First_Quatile,IQR)
print(STD_DEV)
```

    6 6
    9.03014473623248

Actually, data is not fit into normal distribution since the outline. Also, this chapter use ``list`` but some work seem too complicated, so I use numpy instead.

```python
import numpy as np
x = np.array(xs)
y = np.array(ys)
plt.figure(figsize=(16,9))
plt.plot(x,y, 'o-',label='data')
plt.plot([Median,Median],[0,upper_Y],ls='--', label='Median')
plt.plot([Mean,Mean],[0,upper_Y],ls='--', label='Mean')
plt.plot([Mode,Mode],[0,upper_Y],ls='--', label='Mode')
plt.fill_between(x, y, 0, where =  (x>First_Quatile) & (x<Third_Quatile), color = "g", alpha = 0.2, label='IQR')
plt.legend()
plt.show()
```

### Remove the outlines
Let see the statistics values when remove some outlines  and then plot it again.

```python
# delete 100.0,49,41,40, index is 0 to 3
Data = np.delete(np.array(num_friends),range(15))

friend_counts = Counter(Data)
upper_X = int(max(Data) + 1)
x = np.arange(upper_X)                     # largest value is 100
y = np.array([friend_counts[c] for c in x] )    # height is just # of friends
upper_Y = max(y) + 3

Ranges = data_range(Data)
Median = median(Data)
Mean = mean(Data)
_, Mode = mode(Data)
First_Quatile = quantile(Data,0.25)
Third_Quatile = quantile(Data,0.75)
IQR =interquartile_range(Data)
STD_DEV = standard_deviation(Data)

plt.figure(figsize=(16,9))
plt.plot(x,y, 'o-',label='data')
plt.plot([Median,Median],[0,upper_Y],ls='--', label='Median')
plt.plot([Mean,Mean],[0,upper_Y],ls='-', label='Mean')
plt.plot([Mode,Mode],[0,upper_Y],ls='--', label='Mode')
plt.fill_between(x, y, 0, where =  (x>First_Quatile) & (x<Third_Quatile), color = "g", alpha = 0.2, label='IQR')
plt.legend()
#plt.grid()
plt.show()
```

---

```python

```

```python

```

```python

```

```python

```

```python

```

## Covariance and Correlation
Besides each individual avarage and disribution, we can see two dataset comparisision by Covariance and Correlation.
Let see the data from the book without remove any outline, the more number of friend may be more number hours online.

Data seems not growth in the same direction (blue data) that not in line with  the expected (orange)

```python

num_friends = [100.0,49,41,40,25,21,21,19,19,18,18,16,15,15,15,15,14,14,13,13,13,13,12,12,11,
               10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,
               9,9,8,8,8,8,8,8,8,8,8,8,8,8,8,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,6,6,6,6,6,6,6,6,6,
               6,6,6,6,6,6,6,6,6,6,6,6,6,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,4,4,4,4,4,4,4,4,4,
               4,4,4,4,4,4,4,4,4,4,4,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,
               2,2,2,2,2,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]

daily_minutes = [1,68.77,51.25,52.08,38.36,44.54,57.13,51.4,41.42,31.22,34.76,54.01,38.79,47.59,
                 49.1,27.66,41.03,36.73,48.65,28.12,46.62,35.57,32.98,35,26.07,23.77,39.73,40.57,
                 31.65,31.21,36.32,20.45,21.93,26.02,27.34,23.49,46.94,30.5,33.8,24.23,21.4,27.94,
                 32.24,40.57,25.07,19.42,22.39,18.42,46.96,23.72,26.41,26.97,36.76,40.32,35.02,29.47,
                 30.2,31,38.11,38.18,36.31,21.03,30.86,36.07,28.66,29.08,37.28,15.28,24.17,22.31,30.17,
                 25.53,19.85,35.37,44.6,17.23,13.47,26.33,35.02,32.09,24.81,19.33,28.77,24.26,31.98,25.73,
                 24.86,16.28,34.51,15.23,39.72,40.8,26.06,35.76,34.76,16.13,44.04,18.03,19.65,32.62,35.59,
                 39.43,14.18,35.24,40.13,41.82,35.45,36.07,43.67,24.61,20.9,21.9,18.79,27.61,27.21,26.61,29.77,
                 20.59,27.53,13.82,33.2,25,33.1,36.65,18.63,14.87,22.2,36.81,25.53,24.62,26.25,18.21,28.08,19.42,
                 29.79,32.8,35.99,28.32,27.79,35.88,29.06,36.28,14.1,36.63,37.49,26.9,18.58,38.48,24.48,18.95,33.55,
                 14.24,29.04,32.51,25.63,22.22,19,32.73,15.16,13.9,27.2,32.01,29.27,33,13.74,20.42,27.32,18.23,35.35,
                 28.48,9.08,24.62,20.12,35.26,19.92,31.02,16.49,12.16,30.7,31.22,34.65,13.13,27.51,33.2,31.57,14.1,33.42,
                 17.44,10.12,24.42,9.82,23.39,30.93,15.03,21.67,31.09,33.29,22.61,26.89,23.48,8.38,27.81,32.35,23.84]

daily_hours = [dm / 60 for dm in daily_minutes]

plt.figure(figsize=(16,9))
#plt.plot(num_friends,daily_hours,'kp',markerfacecolor='b',markersize=12)
plt.plot(num_friends,daily_minutes,'o',markerfacecolor='b',markersize=12, alpha=0.5, label='Actual')
plt.plot(sorted(num_friends),sorted(daily_minutes),'o',markerfacecolor='pink',markersize=12, alpha=0.3, label='Perfect')

plt.xlabel('Number of  Friend',fontsize=14)
plt.ylabel('Daily Hour',fontsize=14)
plt.title('Correlation with outliter', fontsize=16)
plt.xticks(range(0,110,10))
plt.yticks(range(0,110,10))
plt.legend()
plt.show()

```

```python

```

The **Covariance** is compare variance the dataset, positive mean the value growth in the same direction while negative is in the opposite direction and value close to zero mean no siginficant. In general with N data set, the covariance can be compute from the covariance matrix

\begin{eqnarray*}
\text{Covariance Matrix:} \  \
\Sigma = \begin{bmatrix}
        \sigma_{1}^2 \ \sigma_{12} \ \dots \ \sigma_{1N} \\
        \sigma_{21} \ \sigma_{2}^2 \ \dots \ \sigma_{2N} \\
        \vdots \ \vdots \ \ddots \ \vdots \\
        \sigma_{N1} \ \sigma_{N2} \ \dots \ \sigma_{NM}^2
    \end{bmatrix}
\end{eqnarray*}

In the case of Number of friend (x) and dialy minute (y) the covariance is:

\begin{equation}
\begin{aligned}
\text{Covariance}_{x,y} &= \vec{x} \cdot \vec{y} \\ \\
    &= \sum_{i=0}^{i=N} {x_i \cdot y_i} \\ \\
    &= \sum_{i=0}^{i=N} {\sigma_x \cdot \sigma_y} \\ \\
    &=  \large \sum_{i=0}^{i=N} { \frac{ (x_i -\bar{x})(y_i - \bar{y})}{N-1} }
\end{aligned}
\tag{Covariance}\label{Covairance}
\end{equation}

```python
from linear_algebra import dot

def covariance(xs: List[float], ys: List[float]) -> float:
    assert len(xs) == len(ys), "xs and ys must have same number of elements"

    return dot(de_mean(xs), de_mean(ys)) / (len(xs) - 1)

assert 22.42 < covariance(num_friends, daily_minutes) < 22.43
assert 22.42 / 60 < covariance(num_friends, daily_hours) < 22.43 / 60

```

**Correlation** similar to the covariance but more stable.

\begin{equation}
\begin{aligned}
\text{Correlation}_{x,y} &= \frac{\vec{x} \cdot \vec{y}}{\sigma_x \sigma_y} \\ \\
    &= \sum_{i=0}^{i=N} \frac{x_i \cdot y_i}{\sigma_x \sigma_y} \\ \\
    &=  \large \frac{1}{N-1}\sum_{i=0}^{i=N} { \frac{ (x_i -\bar{x})(y_i - \bar{y})}{\sigma_x \sigma_y} }
\end{aligned}
\tag{Correlation}\label{Correlation}
\end{equation}

Coefficient of variance
\begin{equation}
\begin{aligned}
\text{Coefficient of Variation} = \frac{\sigma}{\bar{x}}
\end{aligned}
\tag{Coefficient of Variation}\label{Coefficient of Variation}
\end{equation}

```python
def correlation(xs: List[float], ys: List[float]) -> float:
    """Measures how much xs and ys vary in tandem about their means"""
    stdev_x = standard_deviation(xs)
    stdev_y = standard_deviation(ys)
    if stdev_x > 0 and stdev_y > 0:
        return covariance(xs, ys) / stdev_x / stdev_y
    else:
        return 0    # if no variation, correlation is zero

assert 0.24 < correlation(num_friends, daily_minutes) < 0.25
assert 0.24 < correlation(num_friends, daily_hours) < 0.25

outlier = num_friends.index(100)    # index of outlier

num_friends_good = [x
                    for i, x in enumerate(num_friends)
                    if i != outlier]

daily_minutes_good = [x
                      for i, x in enumerate(daily_minutes)
                      if i != outlier]

daily_hours_good = [dm / 60 for dm in daily_minutes_good]

assert 0.57 < correlation(num_friends_good, daily_minutes_good) < 0.58
assert 0.57 < correlation(num_friends_good, daily_hours_good) < 0.58

```

```python
plt.figure(figsize=(16,9))
#plt.plot(num_friends,daily_hours,'kp',markerfacecolor='b',markersize=12)
#plt.plot(num_friends,daily_hours,'o',markerfacecolor='b',markersize=12, alpha=0.2, label='Actual')
plt.plot(num_friends_good,daily_hours_good,'o',markerfacecolor='b',markersize=12, alpha=0.5, label='Exoected')
#plt.plot(sorted(num_friends_good),sorted(daily_hours_good),'o',markerfacecolor='pink',markersize=12, alpha=0.8, label='Exoected')

plt.xlabel('Number of  Friend',fontsize=14)
plt.ylabel('Daily Hour',fontsize=14)
#plt.xticks([])
#plt.yticks([])
plt.show()

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```