# Señales de tiempo continuo

## Señales estándar

Algunas [señales](https://en.wikipedia.org/wiki/Signal_%28electrical_engineering%29) desempeñan un rol importante en las aplicaciones del **procesamiento de señales**, **el control automático**, **las telecomunicaciones**, etc. y se usan para caracterizar las propiedades de los sistemas Lineales e Invariantes en el Tiempo (LTI). A continuación se introducen e ilustran estas señales estándar.

### Exponencial compleja

La señal exponencial compleja en el dominio del tiempo $t$ se define matemáticamente con la [función exponencial compleja](https://en.wikipedia.org/wiki/Exponential_function#Complex_plane)

\begin{equation}
x(t) = e^{s t}
\end{equation}

donde $s = \sigma + j \omega$ describe una frecuencia compleja con $\sigma, \omega \in \mathbb{R}$ y $j$ tal que $(j^2=-1)$.

Esta señal se usa para representar señales armónicas. Usando la [formula de Euler](https://es.wikipedia.org/wiki/F%C3%B3rmula_de_Euler), la expresión anterior se puede reescribir como:

\begin{equation}
x(t) = e^{(\sigma + j \omega) t}  = e^{(\sigma) t} \cdot e^{(j \omega) t} = e^{\sigma t} \cos(\omega t) + j e^{\sigma t} \sin(\omega t)
\end{equation}

Ahora, las partes real e imaginaria de la señal son ponderados por funciones conseno y seno de frecuencia $\omega = 2 \pi f$. Para $t>0$, el peso en función del tiempo  $e^{\sigma t}$ es

* exponencialmente decreciente si $\sigma < 0$,
* constante (1) si $\sigma = 0$,
* exponencialmente creciente si  $\sigma > 0$,

Así, la señal exponencial compleja se usa para modelar señales armónicas con amplitudes constantes o exponencialmente crecientes o decrecientes.

**Ejemplo**

Las variables que se requieren son $t$, $\omega$ and $\sigma$ y se definen como variables simbólicas reales.

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

t, sigma, omega = sym.symbols('t sigma omega', real=True)
s = sigma + 1j*omega
x = sym.exp(s*t)
x
```

Ahora se definen valores para $s = \sigma + j \omega$.

```python
y = x.subs({omega: 0, sigma: 1})
plt.rcParams['figure.figsize'] = 9, 3
sym.plot(sym.re(y), (t, 0, 20), ylabel=r'Re{$e^{st}$}')
sym.plot(sym.im(y), (t, 0, 20), ylabel=r'Im{$e^{st}$}')
```

**Ejercicio**

* Defina valores para `omega` y `sigma` de manera que pueda crear señales decrecientes, constantes y crecientes con distinta frecuencia angular.

```python
## Escriba su código aquí
## creciente
y = x.subs({omega: 0.5, sigma: 0.1})
plt.rcParams['figure.figsize'] = 9, 3
sym.plot(sym.re(y), (t, 0, 20), ylabel=r'Re{$e^{st}$}')
sym.plot(sym.im(y), (t, 0, 20), ylabel=r'Im{$e^{st}$}')
```

### Impulso de Dirac

Esta es una de las señales más importantes. Se usa para caracterizar y modelar los sistemas LTI. Se define según la [función delta de Dirac](https://es.wikipedia.org/wiki/Delta_de_Dirac). Se denota como $\delta(t)$ y se describe completamente por el efecto en otra función. A continuación las propiedades más relevantes:

1.  \begin{equation}
    \int_{-\infty}^{\infty} \delta(t) \cdot x(t) \cdot dt = x(0)
    \end{equation} donde $x(t)$ debe ser diferenciable en $t=0$. Esto implica que $\int_{-\infty}^{\infty} \delta(t) \cdot dt= 1$.

2.  \begin{equation}
    x(t) \cdot \delta(t) = x(0) \cdot \delta(t)
    \end{equation}
    donde $x(t)$ debe ser diferenciable en $t=0$.

3.  \begin{equation}
    a \cdot \delta(t) + b \cdot \delta(t) = (a+b) \cdot \delta(t)
    \end{equation}

4. \begin{equation}
    \delta(a t) = \frac{1}{|a|} \delta(t)
    \end{equation}
    donde $a \in \mathbb{R} \setminus 0$. Esto implica que el impulso de Dirac tiene simetría par.

5. \begin{equation}
    \int_{-\infty}^{\infty} \frac{d \delta(t)}{dt} \cdot x(t) \; dt = - \frac{d x(t)}{dt} \bigg\vert_{t = 0}
    \end{equation}

6.  \begin{equation}
    \int_{-\infty}^{\infty} \delta(\tau) \cdot x(t - \tau)  \, d\tau = x(t)
    \end{equation}

La última función es la [convolución](https://es.wikipedia.org/wiki/Convoluci%C3%B3n) y se tratará después.

**Ejemplo**

Primero se define la señal delta de Dirac.

```python
delta = sym.DiracDelta(t)
delta
```

Observe la primera propiedad con una función arbitraria $f(t)$.

```python
f = sym.Function('f')(t)
sym.integrate(delta*f, (t, -sym.oo, sym.oo))
```

**Ejercicio**

* Configure la función $\delta(t-\tau)$ y verifique el ejemplo anterior.

```python
## Escriba su código aquí
tau = sym.symbols('tau', real=True)
deltaTau = sym.DiracDelta(t-tau)
deltaTau*f
```

```python
sym.integrate(deltaTau*f, (t, -sym.oo, sym.oo))
```

### Escalón

La señal escalón se define según la [función escalón](https://es.wikipedia.org/wiki/Funci%C3%B3n_escal%C3%B3n_de_Heaviside)

\begin{equation}
\epsilon(t) = \begin{cases} 0 & t<0 \\  1 & t > 0 \end{cases}
\end{equation}

El escalón y el impulso de dirac se relacionan mediante:

\begin{equation}
\epsilon(t) = \int_{-\infty}^{t} \delta(\tau) \; d\tau
\end{equation}

**Ejemplo**

Se define y grafica la función escalón. `Sympy` llama Heaviside a la función y la denota como $\theta(t)$.

```python
step = sym.Heaviside(t)
step
```

```python
plt.rcParams['figure.figsize'] = 9, 3
sym.plot(step, (t, -2, 2), ylim=[-0.2, 1.2], ylabel=r'$\epsilon(t)$');
```

**Ejercicio**

Construya una señal armónica $\cos(\omega t)$ con $\omega=2$ que se "enciende" en el instante $t=0$.

\begin{equation}
x(t) = \cos(\omega t) \cdot \epsilon(t)
\end{equation}

```python
step = sym.Heaviside(t)
x = sym.cos(omega*t) * step
plt.rcParams['figure.figsize'] = 9, 3
sym.plot(x.subs(omega,2), (t, -10, 20), ylim=[-1.2, 1.2], ylabel=r'$x(t)$',xlabel=r'$t_{seg}$')
```

**Ejemplo**

Basado en la función escalón, se define un pulso rectangular de ancho $a$.

```python
a = sym.symbols('a', real=True)
t = sym.symbols('t', real=True)

def rect(t,a):
    Esc1 = sym.Heaviside(t+a)
    Esc2 = sym.Heaviside(t-a)
    return Esc1 - Esc2
r = rect(t,a)
plt.rcParams['figure.figsize'] = 9, 3
sym.plot(sym.Heaviside(t+a).subs(a,0.5), (t, -2, 2), ylim=[-1.2, 1.2], ylabel=r'$Esc1(t)$')
sym.plot(sym.Heaviside(t-a).subs(a,0.5), (t, -2, 2), ylim=[-1.2, 1.2], ylabel=r'$Esc2(t)$')
sym.plot(r.subs(a,0.5), (t, -2, 2), ylim=[-1.2, 1.2], ylabel=r'$rect(t)$')

```

**Ejercicio**

Defina y grafique una función armónica $\cos(\omega t)$ with $\omega=5$ que se "encienda" en $t=-1$ y se "apague" en $t=1$.

```python
## Escriba su código aquí
x = sym.cos(omega*t) * rect(t,1)
y = x.subs({omega: 5})
plt.rcParams['figure.figsize'] = 9, 3
sym.plot(sym.re(y), (t, -10, 10), ylabel=r'Re{$e^{st}$}')
sym.plot(sym.im(y), (t, -10, 10), ylabel=r'Im{$e^{st}$}')
```

## Operaciones con señales

Las señales estándar permiten formar señales de estructuras más complejas mediante operaciones como la superposición, el desplazamiento temporal, el escalamiento, etc.

### Superposición

Considere que $x(t)$ es la superposición ponderada de las señales $x_\text{A}(t)$ y $x_\text{B}(t)$, es decir, la relación está dada por:

\begin{equation}
x(t) = A \cdot x_\text{A}(t) + B \cdot x_\text{B}(t)
\end{equation}

donde $A, B \in \mathbb{C}$.

**Ejercicio**

Suponga que $x(t) = A \cdot cos(\omega_{A}t) + B \cdot cos(\omega_{B}t)$. Puede observarse que se superponen dos señales coseno con distintas amplitudes y frecuencias.

```python
t = sym.symbols('t', real=True)

A = 0.25
omA = 13
B = .75
omB = 3.5

xA = sym.cos(omA*t)
xB = sym.cos(omB*t)
x = A*xA + B*xB

plt.rcParams['figure.figsize'] = 9, 3
pA=sym.plot(A*xA, (t, -5, 5), ylim=[-1.2, 1.2], xlabel=r't', show=True, line_color='red')
pB=sym.plot(B*xB, (t, -5, 5), ylim=[-1.2, 1.2], show=True, line_color='green')
pA.extend(pB)
pA.show()
sym.plot(x,(t, -5, 5), ylim=[-1.2, 1.2], show=True, line_color='blue')
```

### Desplazamiento temporal

Considere que $y(t)$ es una señal con la misma forma y valores de la señal $x_(t)$ pero están corridas en el tiempo.

\begin{equation}
y(t) = x(t-\tau)
\end{equation}
donde $\tau \in \mathbb{R}$.

En este caso, se dice que la señal $x(t)$ se ha:

* desplazado a la derecha (*retardo*) si $\tau > 0$
* desplazado a la izquierda (*adelanto*) si $\tau < 0$

**Ejercicio**

Suponga que se combinan 3 pulsos rectangulares para formar una "escalera" de pocos nivele.

\begin{equation}
x(t) = \text{rect}\left(t - \frac{1}{2} \right) + \frac{2}{3} \cdot \text{rect}\left(t-\frac{3}{2} \right) + \frac{1}{3} \cdot \text{rect} \left(t-\frac{5}{2} \right)
\end{equation}

```python
rect = sym.Heaviside(t + 1/2) - sym.Heaviside(t - 1/2)

xA = rect.subs(t, t-1/2)
xB = 2/3*rect.subs(t, t-3/2)
xC = 1/3*rect.subs(t, t-5/2)
x = xA + xB + xC

plt.rcParams['figure.figsize'] = 9, 3

sym.plot(rect, (t, -1, 5), ylim=[-0.2, 1.2], ylabel='$x(t)$');
sym.plot(xA, (t, -1, 5), ylim=[-0.2, 1.2], ylabel='$x(t)$');
sym.plot(xB, (t, -1, 5), ylim=[-0.2, 1.2], ylabel='$x(t)$');
sym.plot(xC, (t, -1, 5), ylim=[-0.2, 1.2], ylabel='$x(t)$');
sym.plot(x, (t, -1, 5), ylim=[-0.2, 1.2], ylabel='$x(t)$');
```

### Escalamiento temporal

Esta operación permite alterar la rapidez aparente del tiempo dada la forma de una señal. Se define como:

\begin{equation}
y(t) = x(a \cdot t)
\end{equation}

donde $a \in \mathbb{R}$.

En este caso, se dice que la señal $x(t)$ se ha:

* estirado si $ 0 < a < 1$
* comprimido si $ 1 < a $
* invertido si $ a < 0$

**Ejercicio**

Tome la señal anterior y aplique una escala temporal $a$ tal que $y(t) = x(a \cdot t)$.

```python

```

**Ejercicio**

Modifique el ejemplo anterior para que la señal tenga:
- Compresión temporal y retardo
- Inversión temporal y adelanto

Realice distintas pruebas con otras formas de señal.


# Direct Inversion of the Iterative Subspace

When solving systems of linear (or nonlinear) equations, iterative methods are often employed.  Unfortunately, such methods often suffer from convergence issues such as numerical instability, slow convergence, and significant computational expense when applied to difficult problems.  In these cases, convergence accelleration methods may be applied to both speed up, stabilize and/or reduce the cost for the convergence patterns of these methods, so that solving such problems become computationally tractable.  One such method is known as the direct inversion of the iterative subspace (DIIS) method, which is commonly applied to address convergence issues within self consistent field computations in Hartree-Fock theory (and other iterative electronic structure methods).  In this tutorial, we'll introduce the theory of DIIS for a general iterative procedure, before integrating DIIS into our previous implementation of RHF.

## I. Theory

DIIS is a widely applicable convergence acceleration method, which is applicable to numerous problems in linear algebra and the computational sciences, as well as quantum chemistry in particular.  Therefore, we will introduce the theory of this method in the general sense, before seeking to apply it to SCF.

Suppose that for a given problem, there exist a set of trial vectors $\{\mid{\bf p}_i\,\rangle\}$ which have been generated iteratively, converging toward the true solution, $\mid{\bf p}^f\,\rangle$.  Then the true solution can be approximately constructed as a linear combination of the trial vectors,
$$\mid{\bf p}\,\rangle = \sum_ic_i\mid{\bf p}_i\,\rangle,$$
where we require that the residual vector
$$\mid{\bf r}\,\rangle = \sum_ic_i\mid{\bf r}_i\,\rangle\,;\;\;\; \mid{\bf r}_i\,\rangle
=\, \mid{\bf p}_{i+1}\,\rangle - \mid{\bf p}_i\,\rangle$$
is a least-squares approximate to the zero vector, according to the constraint
$$\sum_i c_i = 1.$$
This constraint on the expansion coefficients can be seen by noting that each trial function ${\bf p}_i$ may be represented as an error vector applied to the true solution, $\mid{\bf p}^f\,\rangle + \mid{\bf e}_i\,\rangle$.  Then
\begin{align}
\mid{\bf p}\,\rangle &= \sum_ic_i\mid{\bf p}_i\,\rangle\\
&= \sum_i c_i(\mid{\bf p}^f\,\rangle + \mid{\bf e}_i\,\rangle)\\
&= \mid{\bf p}^f\,\rangle\sum_i c_i + \sum_i c_i\mid{\bf e}_i\,\rangle
\end{align}
Convergence results in a minimization of the error (causing the second term to vanish); for the DIIS solution vector $\mid{\bf p}\,\rangle$ and the true solution vector $\mid{\bf p}^f\,\rangle$ to be equal, it must be that $\sum_i c_i = 1$.  We satisfy our condition for the residual vector by minimizing its norm,
$$\langle\,{\bf r}\mid{\bf r}\,\rangle = \sum_{ij} c_i^* c_j \langle\,{\bf r}_i\mid{\bf r}_j\,\rangle,$$
using Lagrange's method of undetermined coefficients subject to the constraint on $\{c_i\}$:
$${\cal L} = {\bf c}^{\dagger}{\bf Bc} - \lambda\left(1 - \sum_i c_i\right)$$
where $B_{ij} = \langle {\bf r}_i\mid {\bf r}_j\rangle$ is the matrix of residual vector overlaps.  Minimization of the Lagrangian with respect to the coefficient $c_k$ yields (for real values)
\begin{align}
\frac{\partial{\cal L}}{\partial c_k} = 0 &= \sum_j c_jB_{jk} + \sum_i c_iB_{ik} - \lambda\\
&= 2\sum_ic_iB_{ik} - \lambda
\end{align}
which has matrix representation
\begin{equation}
\begin{pmatrix}
  B_{11} & B_{12} & \cdots & B_{1m} & -1 \\
  B_{21} & B_{22} & \cdots & B_{2m} & -1 \\
  \vdots  & \vdots  & \ddots & \vdots  & \vdots \\
  B_{n1} & B_{n2} & \cdots & B_{nm} & -1 \\
  -1 & -1 & \cdots & -1 & 0
\end{pmatrix}
\begin{pmatrix}
c_1\\
c_2\\
\vdots \\
c_n\\
\lambda
\end{pmatrix}
=
\begin{pmatrix}
0\\
0\\
\vdots\\
0\\
-1
\end{pmatrix},
\end{equation}

which we will refer to as the Pulay equation, named after the inventor of DIIS.  It is worth noting at this point that our trial vectors, residual vectors, and solution vector may in fact be tensors of arbitrary rank; it is for this reason that we have used the generic notation of Dirac in the above discussion to denote the inner product between such objects.

## II. Algorithms for DIIS
The general DIIS procedure, as described above, has the following structure during each iteration:
#### Algorithm 1: Generic DIIS procedure
1. Compute new trial vector, $\mid{\bf p}_{i+1}\,\rangle$, append to list of trial vectors
2. Compute new residual vector, $\mid{\bf r}_{i+1}\,\rangle$, append to list of trial vectors
3. Check convergence criteria
    - If RMSD of $\mid{\bf r}_{i+1}\,\rangle$ sufficiently small, and
    - If change in DIIS solution vector $\mid{\bf p}\,\rangle$ sufficiently small, break
4. Build **B** matrix from previous residual vectors
5. Solve Pulay equation for coefficients $\{c_i\}$
6. Compute DIIS solution vector $\mid{\bf p}\,\rangle$

For SCF iteration, the most common choice of trial vector is the Fock matrix **F**; this choice has the advantage over other potential choices (e.g., the density matrix **D**) of **F** not being idempotent, so that it may benefit from extrapolation.  The residual vector is commonly chosen to be the orbital gradient in the AO basis,
$$g_{\mu\nu} = ({\bf FDS} - {\bf SDF})_{\mu\nu},$$
however the better choice (which we will make in our implementation!) is to orthogonormalize the basis of the gradient with the inverse overlap metric ${\bf A} = {\bf S}^{-1/2}$:
$$r_{\mu\nu} = ({\bf A}^{\rm T}({\bf FDS} - {\bf SDF}){\bf A})_{\mu\nu}.$$
Therefore, the SCF-specific DIIS procedure (integrated into the SCF iteration algorithm) will be:
#### Algorithm 2: DIIS within an SCF Iteration
1. Compute **F**, append to list of previous trial vectors
2. Compute AO orbital gradient **r**, append to list of previous residual vectors
3. Compute RHF energy
3. Check convergence criteria
    - If RMSD of **r** sufficiently small, and
    - If change in SCF energy sufficiently small, break
4. Build **B** matrix from previous AO gradient vectors
5. Solve Pulay equation for coefficients $\{c_i\}$
6. Compute DIIS solution vector **F_DIIS** from $\{c_i\}$ and previous trial vectors
7. Compute new orbital guess with **F_DIIS**

## III. Implementation

In order to implement DIIS, we're going to integrate it into an existing RHF program.  Since we just-so-happened to write such a program in the last tutorial, let's re-use the part of the code before the SCF integration which won't change when we include DIIS:

```python
# ==> Basic Setup <==
# Import statements
import psi4
import numpy as np

# Memory specification
psi4.set_memory(int(5e8))
numpy_memory = 2

# Set output file
psi4.core.set_output_file('output.dat', False)

# Define Physicist's water -- don't forget C1 symmetry!
mol = psi4.geometry("""
O
H 1 1.1
H 1 1.1 2 104
symmetry c1
""")

# Set computation options
psi4.set_options({'basis': 'cc-pvdz',
                  'scf_type': 'pk',
                  'e_convergence': 1e-8})

# Maximum SCF iterations
MAXITER = 40
# Energy convergence criterion
E_conv = 1.0e-6
D_conv = 1.0e-3
```

```python
# ==> Static 1e- & 2e- Properties <==
# Class instantiation
wfn = psi4.core.Wavefunction.build(mol, psi4.core.get_global_option('basis'))
mints = psi4.core.MintsHelper(wfn.basisset())

# Overlap matrix
S = np.asarray(mints.ao_overlap())

# Number of basis Functions & doubly occupied orbitals
nbf = S.shape[0]
ndocc = wfn.nalpha()

print('Number of occupied orbitals: %d' % ndocc)
print('Number of basis functions: %d' % nbf)

# Memory check for ERI tensor
I_size = (nbf**4) * 8.e-9
print('\nSize of the ERI tensor will be %4.2f GB.' % I_size)
if I_size > numpy_memory:
    psi4.core.clean()
    raise Exception("Estimated memory utilization (%4.2f GB) exceeds allotted memory \
                     limit of %4.2f GB." % (I_size, numpy_memory))

# Build ERI Tensor
I = np.asarray(mints.ao_eri())

# Build core Hamiltonian
T = np.asarray(mints.ao_kinetic())
V = np.asarray(mints.ao_potential())
H = T + V
```

    Number of occupied orbitals: 5
    Number of basis functions: 24

    Size of the ERI tensor will be 0.00 GB.

```python
# ==> CORE Guess <==
# AO Orthogonalization Matrix
A = mints.ao_overlap()
A.power(-0.5, 1.e-16)
A = np.asarray(A)

# Transformed Fock matrix
F_p = A.dot(H).dot(A)

# Diagonalize F_p for eigenvalues & eigenvectors with NumPy
e, C_p = np.linalg.eigh(F_p)

# Transform C_p back into AO basis
C = A.dot(C_p)

# Grab occupied orbitals
C_occ = C[:, :ndocc]

# Build density matrix from occupied orbitals
D = np.einsum('pi,qi->pq', C_occ, C_occ, optimize=True)

# Nuclear Repulsion Energy
E_nuc = mol.nuclear_repulsion_energy()
```

Now let's put DIIS into action.  Before our iterations begin, we'll need to create empty lists to hold our previous residual vectors (AO orbital gradients) and trial vectors (previous Fock matrices), along with setting starting values for our SCF energy and previous energy:

```python
# ==> Pre-Iteration Setup <==
# SCF & Previous Energy
SCF_E = 0.0
E_old = 0.0
```

Now we're ready to write our SCF iterations according to Algorithm 2.  Here are some hints which may help you along the way:

#### Starting DIIS
Since DIIS builds the approximate solution vector $\mid{\bf p}\,\rangle$ as a linear combination of the previous trial vectors $\{\mid{\bf p}_i\,\rangle\}$, there's no need to perform DIIS on the first SCF iteration, since there's only one trial vector for DIIS to use!

#### Building **B**
1. The **B** matrix in the Lagrange equation is really $\tilde{\bf B} = \begin{pmatrix} {\bf B} & -1\\ -1 & 0\end{pmatrix}$.
2. Since **B** is the matrix of residual overlaps, it will be a square matrix of dimension equal to the number of residual vectors.  If **B** is an $N\times N$ matrix, how big is $\tilde{\bf B}$?
3. Since our residuals are real, **B** will be a symmetric matrix.
4. To build $\tilde{\bf B}$, make an empty array of the appropriate dimension, then use array indexing to set the values of the elements.

#### Solving the Pulay equation
1. Use built-in NumPy functionality to make your life easier.
2. The solution vector for the Pulay equation is $\tilde{\bf c} = \begin{pmatrix} {\bf c}\\ \lambda\end{pmatrix}$, where $\lambda$ is the Lagrange multiplier, and the right hand side is $\begin{pmatrix} {\bf 0}\\ -1\end{pmatrix}$.

```python
# Start from fresh orbitals
F_p =  A.dot(H).dot(A)
e, C_p = np.linalg.eigh(F_p)
C = A.dot(C_p)
C_occ = C[:, :ndocc]
D = np.einsum('pi,qi->pq', C_occ, C_occ, optimize=True)

# Trial & Residual Vector Lists
F_list = []
DIIS_RESID = []

# ==> SCF Iterations w/ DIIS <==
print('==> Starting SCF Iterations <==\n')

# Begin Iterations
for scf_iter in range(1, MAXITER + 1):
    # Build Fock matrix
    J = np.einsum('pqrs,rs->pq', I, D, optimize=True)
    K = np.einsum('prqs,rs->pq', I, D, optimize=True)
    F = H + 2*J - K

    # Build DIIS Residual
    diis_r = A.dot(F.dot(D).dot(S) - S.dot(D).dot(F)).dot(A)

    # Append trial & residual vectors to lists
    F_list.append(F)
    DIIS_RESID.append(diis_r)

    # Compute RHF energy
    SCF_E = np.einsum('pq,pq->', (H + F), D, optimize=True) + E_nuc
    dE = SCF_E - E_old
    dRMS = np.mean(diis_r**2)**0.5
    print('SCF Iteration %3d: Energy = %4.16f dE = % 1.5E dRMS = %1.5E' % (scf_iter, SCF_E, dE, dRMS))

    # SCF Converged?
    if (abs(dE) < E_conv) and (dRMS < D_conv):
        break
    E_old = SCF_E

    if scf_iter >= 2:
        # Build B matrix
        B_dim = len(F_list) + 1
        B = np.empty((B_dim, B_dim))
        B[-1, :] = -1
        B[:, -1] = -1
        B[-1, -1] = 0
        for i in range(len(F_list)):
            for j in range(len(F_list)):
                B[i, j] = np.einsum('ij,ij->', DIIS_RESID[i], DIIS_RESID[j], optimize=True)

        # Build RHS of Pulay equation
        rhs = np.zeros((B_dim))
        rhs[-1] = -1

        # Solve Pulay equation for c_i's with NumPy
        coeff = np.linalg.solve(B, rhs)

        # Build DIIS Fock matrix
        F = np.zeros_like(F)
        for x in range(coeff.shape[0] - 1):
            F += coeff[x] * F_list[x]

    # Compute new orbital guess with DIIS Fock matrix
    F_p =  A.dot(F).dot(A)
    e, C_p = np.linalg.eigh(F_p)
    C = A.dot(C_p)
    C_occ = C[:, :ndocc]
    D = np.einsum('pi,qi->pq', C_occ, C_occ, optimize=True)

    # MAXITER exceeded?
    if (scf_iter == MAXITER):
        psi4.core.clean()
        raise Exception("Maximum number of SCF iterations exceeded.")

# Post iterations
print('\nSCF converged.')
print('Final RHF Energy: %.8f [Eh]' % SCF_E)
```

    ==> Starting SCF Iterations <==

    SCF Iteration   1: Energy = -68.9800327333871337 dE = -6.89800E+01 dRMS = 1.16551E-01
    SCF Iteration   2: Energy = -69.6472544393141675 dE = -6.67222E-01 dRMS = 1.07430E-01
    SCF Iteration   3: Energy = -75.7919291462249021 dE = -6.14467E+00 dRMS = 2.89274E-02
    SCF Iteration   4: Energy = -75.9721892296711019 dE = -1.80260E-01 dRMS = 7.56446E-03
    SCF Iteration   5: Energy = -75.9893690602363563 dE = -1.71798E-02 dRMS = 8.74982E-04
    SCF Iteration   6: Energy = -75.9897163367029691 dE = -3.47276E-04 dRMS = 5.35606E-04
    SCF Iteration   7: Energy = -75.9897932415930200 dE = -7.69049E-05 dRMS = 6.21200E-05
    SCF Iteration   8: Energy = -75.9897956274068633 dE = -2.38581E-06 dRMS = 2.57879E-05
    SCF Iteration   9: Energy = -75.9897957845313670 dE = -1.57125E-07 dRMS = 1.72817E-06

    SCF converged.
    Final RHF Energy: -75.98979578 [Eh]

Congratulations! You've written your very own Restricted Hartree-Fock program with DIIS convergence accelleration!  Finally, let's check your final RHF energy against  Psi4:

```python
# Compare to Psi4
SCF_E_psi = psi4.energy('SCF')
psi4.compare_values(SCF_E_psi, SCF_E, 6, 'SCF Energy')
```

    	SCF Energy........................................................PASSED

    True

## References
1. P. Pulay. *Chem. Phys. Lett.* **73**, 393-398 (1980)
2. C. David Sherrill. *"Some comments on accellerating convergence of iterative sequences using direct inversion of the iterative subspace (DIIS)".* Available at: vergil.chemistry.gatech.edu/notes/diis/diis.pdf. (1998)


# Summary of Quantum Operations

 In this section we will go into the different operations that are available in Qiskit Terra. These are:
- Single-qubit quantum gates
- Multi-qubit quantum gates
- Measurements
- Reset
- Conditionals
- State initialization

We will also show you how to use the three different simulators:
- unitary_simulator
- qasm_simulator
- statevector_simulator

```python
# Useful additional packages
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
from math import pi
```

```python
from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister, execute
from qiskit.tools.visualization import circuit_drawer
from qiskit.quantum_info import state_fidelity
from qiskit import BasicAer

backend = BasicAer.get_backend('unitary_simulator')
```

## Multi-Qubit Gates

### Mathematical Preliminaries

The space of quantum computer grows exponential with the number of qubits. For $n$ qubits the complex vector space has dimensions $d=2^n$. To describe states of a multi-qubit system, the tensor product is used to "glue together" operators and basis vectors.

Let's start by considering a 2-qubit system. Given two operators $A$ and $B$ that each act on one qubit, the joint operator $A \otimes B$ acting on two qubits is

$$\begin{equation}
	A\otimes B =
	\begin{pmatrix}
		A_{00} \begin{pmatrix}
			B_{00} & B_{01} \\
			B_{10} & B_{11}
		\end{pmatrix} & A_{01} 	\begin{pmatrix}
				B_{00} & B_{01} \\
				B_{10} & B_{11}
			\end{pmatrix} \\
		A_{10} 	\begin{pmatrix}
					B_{00} & B_{01} \\
					B_{10} & B_{11}
				\end{pmatrix} & A_{11} 	\begin{pmatrix}
							B_{00} & B_{01} \\
							B_{10} & B_{11}
						\end{pmatrix}
	\end{pmatrix},
\end{equation}$$

where $A_{jk}$ and $B_{lm}$ are the matrix elements of $A$ and $B$, respectively.

Analogously, the basis vectors for the 2-qubit system are formed using the tensor product of basis vectors for a single qubit:
$$\begin{equation}\begin{split}
	\left|{00}\right\rangle &= \begin{pmatrix}
		1 \begin{pmatrix}
			1  \\
			0
		\end{pmatrix} \\
		0 \begin{pmatrix}
			1  \\
			0
		\end{pmatrix}
	\end{pmatrix} = \begin{pmatrix} 1 \\ 0 \\ 0 \\0 \end{pmatrix}~~~\left|{01}\right\rangle = \begin{pmatrix}
	1 \begin{pmatrix}
	0 \\
	1
	\end{pmatrix} \\
	0 \begin{pmatrix}
	0  \\
	1
	\end{pmatrix}
	\end{pmatrix} = \begin{pmatrix}0 \\ 1 \\ 0 \\ 0 \end{pmatrix}\end{split}
\end{equation}$$

$$\begin{equation}\begin{split}\left|{10}\right\rangle = \begin{pmatrix}
	0\begin{pmatrix}
	1  \\
	0
	\end{pmatrix} \\
	1\begin{pmatrix}
	1 \\
	0
	\end{pmatrix}
	\end{pmatrix} = \begin{pmatrix} 0 \\ 0 \\ 1 \\ 0 \end{pmatrix}~~~ 	\left|{11}\right\rangle = \begin{pmatrix}
	0 \begin{pmatrix}
	0  \\
	1
	\end{pmatrix} \\
	1\begin{pmatrix}
	0  \\
	1
	\end{pmatrix}
	\end{pmatrix} = \begin{pmatrix} 0 \\ 0 \\ 0 \\1 \end{pmatrix}\end{split}
\end{equation}.$$

Note we've introduced a shorthand for the tensor product of basis vectors, wherein $\left|0\right\rangle \otimes \left|0\right\rangle$ is written as $\left|00\right\rangle$. The state of an $n$-qubit system can described using the $n$-fold tensor product of single-qubit basis vectors. Notice that the basis vectors for a 2-qubit system are 4-dimensional; in general, the basis vectors of an $n$-qubit sytsem are $2^{n}$-dimensional, as noted earlier.

### Basis vector ordering in Qiskit

Within the physics community, the qubits of a multi-qubit systems are typically ordered with the first qubit on the left-most side of the tensor product and the last qubit on the right-most side. For instance, if the first qubit is in state $\left|0\right\rangle$ and second is in state $\left|1\right\rangle$, their joint state would be $\left|01\right\rangle$. Qiskit uses a slightly different ordering of the qubits, in which the qubits are represented from the most significant bit (MSB) on the left to the least significant bit (LSB) on the right (big-endian). This is similar to bitstring representation on classical computers, and enables easy conversion from bitstrings to integers after measurements are performed. For the example just given, the joint state would be represented as $\left|10\right\rangle$. Importantly, _this change in the representation of multi-qubit states affects the way multi-qubit gates are represented in Qiskit_, as discussed below.

The representation used in Qiskit enumerates the basis vectors in increasing order of the integers they represent. For instance, the basis vectors for a 2-qubit system would be ordered as $\left|00\right\rangle$, $\left|01\right\rangle$, $\left|10\right\rangle$, and $\left|11\right\rangle$. Thinking of the basis vectors as bit strings, they encode the integers 0,1,2 and 3, respectively.

### Controlled operations on qubits

A common multi-qubit gate involves the application of a gate to one qubit, conditioned on the state of another qubit. For instance, we might want to flip the state of the second qubit when the first qubit is in $\left|0\right\rangle$. Such gates are known as _controlled gates_. The standard multi-qubit gates consist of two-qubit gates and three-qubit gates. The two-qubit gates are:
- controlled Pauli gates
- controlled Hadamard gate
- controlled rotation gates
- controlled phase gate
- controlled u3 gate
- swap gate

The three-qubit gates are:
- Toffoli gate
- Fredkin gate

## Three-qubit gates

There are two commonly-used three-qubit gates. For three qubits, the basis vectors are ordered as

$$\left|000\right\rangle, \left|001\right\rangle, \left|010\right\rangle, \left|011\right\rangle, \left|100\right\rangle, \left|101\right\rangle, \left|110\right\rangle, \left|111\right\rangle,$$

which, as bitstrings, represent the integers $0,1,2,\cdots, 7$. Again, Qiskit uses a representation in which the first qubit is on the right-most side of the tensor product and the third qubit is on the left-most side:

$$\left|abc\right\rangle : \underset{\text{qubit 2}}{\left|a\right\rangle}\otimes \underset{\text{qubit 1}}{\left|b\right\rangle}\otimes \underset{\text{qubit 0}}{\left|c\right\rangle}.$$

### Toffoli gate ($ccx$ gate)

The [Toffoli gate](https://en.wikipedia.org/wiki/Quantum_logic_gate#Toffoli_(CCNOT)_gate) flips the third qubit if the first two qubits (LSB) are both $\left|1\right\rangle$:

$$\left|abc\right\rangle \rightarrow \left|bc\oplus a\right\rangle \otimes \left|b\right\rangle \otimes \left|c\right\rangle.$$

In matrix form, the Toffoli gate is
$$
C_{CX} =
\begin{pmatrix}
1 & 0 & 0 & 0 & 0 & 0 & 0 & 0\\
0 & 1 & 0 & 0 & 0 & 0 & 0 & 0\\
0 & 0 & 1 & 0 & 0 & 0 & 0 & 0\\
0 & 0 & 0 & 0 & 0 & 0 & 0 & 1\\
0 & 0 & 0 & 0 & 1 & 0 & 0 & 0\\
0 & 0 & 0 & 0 & 0 & 1 & 0 & 0\\
0 & 0 & 0 & 0 & 0 & 0 & 1 & 0\\
0 & 0 & 0 & 1 & 0 & 0 & 0 & 0
\end{pmatrix}.
$$

```python
q = QuantumRegister(3)
```

```python
qc = QuantumCircuit(q)
qc.ccx(q[0], q[1], q[2])
qc.draw(output='mpl')
```

```python
job = execute(qc, backend)
job.result().get_unitary(qc, decimals=3)
```

    array([[1.+0.j, 0.+0.j, 0.+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, 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, 0.+0.j, 0.+0.j, 0.+0.j],
           [0.+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, 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, 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, 0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j],
           [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j]])

### Controlled swap gate (Fredkin Gate)

The [Fredkin gate](https://en.wikipedia.org/wiki/Quantum_logic_gate#Fredkin_(CSWAP)_gate), or the _controlled swap gate_, exchanges the second and third qubits if the first qubit (LSB) is $\left|1\right\rangle$:

$$ \left|abc\right\rangle \rightarrow \begin{cases} \left|bac\right\rangle~~\text{if}~c=1 \cr \left|abc\right\rangle~~\text{if}~c=0 \end{cases}.$$

In matrix form, the Fredkin gate is

$$
C_{\mathrm{SWAP}} =
\begin{pmatrix}
1 & 0 & 0 & 0 & 0 & 0 & 0 & 0\\
0 & 1 & 0 & 0 & 0 & 0 & 0 & 0\\
0 & 0 & 1 & 0 & 0 & 0 & 0 & 0\\
0 & 0 & 0 & 0 & 0 & 1 & 0 & 0\\
0 & 0 & 0 & 0 & 1 & 0 & 0 & 0\\
0 & 0 & 0 & 1 & 0 & 0 & 0 & 0\\
0 & 0 & 0 & 0 & 0 & 0 & 1 & 0\\
0 & 0 & 0 & 0 & 0 & 0 & 0 & 1
\end{pmatrix}.
$$

```python
qc = QuantumCircuit(q)
qc.cswap(q[0], q[1], q[2])
qc.draw(output='mpl')
```

```python
job = execute(qc, backend)
job.result().get_unitary(qc, decimals=3)
```

    array([[1.+0.j, 0.+0.j, 0.+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, 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, 0.+0.j, 0.+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],
           [0.+0.j, 0.+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, 0.+0.j, 0.+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, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j]])

-------------

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

    UsageError: Line magic function `%qiskit_version_table` not found.

This code is a part of Qiskit
© Copyright IBM 2017, 2019.

This code is licensed under the Apache License, Version 2.0. You may
obtain a copy of this license in the LICENSE.txt file in the root directory
of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.

Any modifications or derivative works of this code must retain this
copyright notice, and modified files need to carry a notice indicating
that they have been altered from the originals.

```python

```


```python
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import beta, gamma, norm, binom, uniform, t
import numdifftools as nd
from sklearn.linear_model import LinearRegression as linreg
import sympy as sym
from scipy.optimize import brentq, minimize
from datetime import datetime as dt
```

* Please email me (ianzhang@connect.hku.hk) if you find any mistakes in the script.

# Bayesian Inference

$\pi(\theta|\mathbf{x})\propto L(\theta|\mathbf{x})\pi(\theta)=\prod_{i=1}^nf(x_i|\theta)\pi(\theta)$, what will happen if $n$ is very large?

## Normal example

Assume model $f(x|\mu)=N(x|\mu, \tau)$ and prior $\pi(\mu)=N(\mu|\mu_0, \tau_0)$, where $\tau$ and $\tau_0$ are the precisions (reciprocal of the variance).

With $n$ samples $\mathbf{x}=(x_1,\dots,x_n)$ from the model, we have the posterior:
$$
\pi(\mu|\mathbf{x})=N(\mu|\frac{n\tau\overline{x}+\tau_0\mu_0}{n\tau+\tau_0}, n\tau+\tau_0)
$$

A prior is just like the regularization in Machine Learning. It serves as bias–variance tradeoff method and always results in more bias but less variance.

## Beta prior and Binomial model (Conjugate prior example)

Assume model $f(y|p)=Binomial(y|n,p)$ and prior $\pi(p)=Beta(p|\alpha, \beta)$.

With $1$ sample $y$ from the Binomial model (or equivalently $n$ samples $\mathbf{x}=(x_1,\dots,x_n)$ ($y=\sum_{i=1}^nx_i)$ from a Bernoulli model $f(x|p)=Bernoulli(x|p)$), we have the posterior:
$$
\pi(p|y)=\pi(p|\mathbf{x})=Beta(p|\alpha+y, \beta+n-y)
$$

```python
np.random.seed(19971107)
a, b = 0.5, 0.5
p = 0.7
fig, axs = plt.subplots(2, 2, figsize = (15, 10))
axs = axs.flatten()

xx = np.linspace(0, 1, 1000)
axs[0].plot(p, 0, 'x', label='true p')
axs[0].plot(xx, beta.pdf(x=xx, a=a, b=b), label='prior')

for i, n in enumerate([10, 50, 1000]):
    y = binom.rvs(n=n, p=p)
    posterior = beta(a=a+y, b=b+n-y)
    axs[0].plot(xx, posterior.pdf(xx), label='posterior (n={})'.format(n))

    loc = posterior.mean()
    scale = posterior.std()
    xx_zoomin = np.linspace(loc - 4*scale, loc + 4*scale, 1000)
    axs[i+1].plot(xx_zoomin, posterior.pdf(xx_zoomin), label='posterior (n={})'.format(n))
    axs[i+1].plot(xx_zoomin, norm.pdf(xx_zoomin, loc=loc, scale=scale), label='normal'.format(n))

for ax in axs:
    ax.legend()

plt.show()
```

Under certain regularity conditions, the posterior distribution approaches a Gaussian as the number of samples grows (refer to this book http://www.stat.columbia.edu/~gelman/book/). This justifies the application of Laplace's method.

**Think further:** Can you construct a simple example where this Gaussian phenomenon wouldn't appear? Is there any other Gaussian phenomenon appear in your example?

## Laplace approximation

Assume the data set is big enough to make the posterior $\pi(\theta|\textbf{x})$ looks like a normal. Set $\theta^*$ to be the posterior mode, which means $\frac{\partial}{\partial\theta}\pi(\theta^*|\textbf{x})=0$. We know that there exists a $\sigma^2$ so that
$$
\pi(\theta|\textbf{x})\approx\frac{\pi(\theta^*|\textbf{x})}{N(\theta^*|\theta^*, \sigma^2)}N(\theta|\theta^*, \sigma^2)
$$
To derive the $\sigma^2$, we take a $\log$ on both sides and have the Taylar's expansion (https://en.wikipedia.org/wiki/Taylor_series) of the left side of the above equation at $\theta^*$:
$$
\log\pi(\theta^*|\textbf{x})+\frac{\partial}{\partial\theta}\log\pi(\theta^*|\textbf{x})(\theta-\theta^*)+\frac{1}{2}\frac{\partial^2}{\partial\theta^2}\log\pi(\theta^*|\textbf{x})(\theta-\theta^*)^2\approx\log\pi(\theta|\textbf{x})\approx\log\pi(\theta^*|\textbf{x})-\frac{1}{2\sigma^2}(\theta-\theta^*)^2
$$
Note that $\frac{\partial}{\partial\theta}\log\pi(\theta^*|\textbf{x})=0$, so if we take $\sigma^2=(-\frac{\partial^2}{\partial\theta^2}\log\pi(\theta^*|\textbf{x}))^{-1}$, all kinds of inference based on $\pi(\theta|\textbf{x})$ can be approximated based on the normal distribution (multiplied by a constant). For example:
$$
\mathbb{E}_{\pi(\theta|\mathbf{x})}[h(\Theta)]\approx\frac{\pi(\theta^*|\textbf{x})}{N(\theta^*|\theta^*, \sigma^2)}\mathbb{E}_{N(\theta|\theta^*, \sigma^2)}[h(\Theta)]
$$
for a function $h(\theta)$.

Another interesting thing is that, as, on the above reasoning, we didn't use the condition that the posterior was normalized. If $\pi(\theta|\textbf{x})$ is unnormalized, we can actually set $h(\theta)=1$ to approximate the normalizing constant
$$
Z\approx\hat{Z}=\frac{\pi(\theta^*|\textbf{x})}{N(\theta^*|\theta^*, \sigma^2)}
$$

* Try using the Laplace approximation to estimate the normalizing constant of the beta posterior $Beta(p|\alpha+y, \beta+n-y)$, which is actually already normalized. Draw the absolute error $|\hat{Z}-1|$ against $n$ based on the loglog plot (https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.loglog). What is the convergence rate? (hints: you can use the 'numdifftools' package (https://pypi.org/project/numdifftools/) and 'LinearRegression' from 'sklearn.linear_model' (https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression); if $err = cn^{-r}$, $\log(err)=\log(c)-r\log(n)$)

```python

```

**Think further:** Is this convergence rate true for other estimations? Try some examples.

# Monte Carlo

## (Naive) Monte Carlo Integration

```python
x = sym.Symbol('x')
h = (x+sym.sin(20*x)+sym.cos(1000*x))**2
h_int = sym.integrate(h)
print('h(x) =')
display(h)
print('Integral of h(x) =')
display(h_int)

h = sym.lambdify(x, h)
h_int = sym.lambdify(x, h_int)
mu = h_int(1) - h_int(0)
print('Integrate h(x) from 0 to 1: {}'.format(mu))

xx = np.linspace(0, 1, 1000)
fig, ax = plt.subplots(figsize = (9, 5))
ax.plot(xx, h(xx), label='$h(x)$')
ax.legend()
plt.show()
```

For the highly irregular integrand $h(x)$, Monte Carlo (MC) will be your only choice to do numerical integration.

Based on the LLT (Law of Large Numbers):
$$
\mu=\int_a^bh(x)\mathrm{d}x=(b-a)\mathbb{E}_{U(a,b)}[h(X)]\approx\hat{\mu}=\frac{(b-a)}{n}\sum_{i=1}^nh(x_i), x_i\sim U(a,b)
$$

Based on the CLT (Central Limit Theorem):
$$
\sqrt{n}\frac{(\hat{\mu}-\mu)}{\sigma}\to N(0, 1), \sigma^2=(b-a)^2\mathrm{Var}_{U(a,b)}[h(X)]
$$

which gives the $95\%$ confidence interval (C.I.):
$$
[\hat{\mu}-\frac{1.96\hat{\sigma}}{\sqrt{n}}, \hat{\mu}+\frac{1.96\hat{\sigma}}{\sqrt{n}}], \hat{\sigma}^2=(b-a)^2\hat{\mathrm{Var}}[h(x_i)]
$$
where $\hat{\mathrm{Var}}[h(x_i)]$ means the sample variance.

Always construct the C.I. when you do estimation as it contains more information. The popularity of the confidence interval also comes from a theorem that, under some conditions, it converges with a speed of $n^{-1}$, while the point estimate normally only converges at the rate of $n^{-0.5}$ (refer to this book https://statweb.stanford.edu/~owen/mc/).

* Estimate the integration of $h(x)$ from 0 to 1. Draw the point estimates and the corresponding confidence intervals against differents sample sizes $n$ in a single plot, where you should use the true value $\mu$ as a reference line. Draw the errors of the point estimates against $n$ in a loglog plot and check its convergence rate.

```python

```

**Think further:** Here I assigned weights for the linear regression, do you know why? Try what will happen without the weights. How to do the linear regression to estimate the convergence rate properly without the weights?

## Sampling - the core of MC

Sampling is process to transform an distribution (or an integrable nonnegative function) $\pi(x)$ to a series of points $\{x_1,\dots,x_n\}$, which make it easier to estimate all kinds of the global properties of the distribution, like the mean, the variance, the covariance, the probability in an area, the quantile, the number of modes (local maximum points), the rough positions of all modes. In a word, the samples can represent the distribution $\pi(x)$:
$$
\hat{\Pi}(x)=\frac{1}{n}\sum_{i=1}^n\mathbf{1}(x_i\le x)\to\mathbb{E}_\pi[\mathbf{1}(X\le x)]=\Pi(x)
$$

There are three amazing points about the MC:
1. It is universal. Besides the above global properties, MC can be used as an alternative for optimization in the Machine Learning with a simple but powerful techique called the Annealing, while optimization can't replace MC.
2. It is robust. MC estimation generally has a convergence rate of $n^{-0.5}$ which wouldn't be influenced by the roughness of related functions or the curse of dimensionality. On the other hand, if you develop an algorithm with convergence rate worse than $n^{-0.5}$, your algorithm is even worse than random!
3. It is modest. Although lack of accuracy, different the optimization methodologies, MC provides many tools to evaluate the reliability of an estimation, like the CLT and the Bootstrap.

### The Inversion Method

A random variable $X$ with a reversible cdf $F(x)$ can be sampled by $x_i=F^{-1}(u_i), u_i\sim U(0, 1), i=1,\dots,n$. This is a fundamental important method as all the initial pseudo random numbers generated by the computer always follows $U(0, 1)$.

* Try to generate standard normal random samples if you only have the cdf of the normal distribution. Compare the samples with the normal pdf. (hints: use the 'norm.cdf' and the 'brentq' function from 'scipy.optimize' (https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.brentq.html)))

```python

```

### Transformation

Sampling based on the relationships between different variables. The Inversion Method can be viewed as a special case of the Transformation. For more Information, check this https://en.wikipedia.org/wiki/Relationships_among_probability_distributions.

### Sequential Simulation

Sampling based on the dependency between variables. A typical example is the mixture model $p(x)=\sum_{j=1}^mw_jp_j(x), \sum_{j=1}^mw_j=1$. To draw $n$ samples from $p(x)$, we firstly simulate $(n_1,\dots,n_m)\sim Multinomial(n;w_1,\dots,w_m)$, and than, draw $n_j$ samples from $p_j(x)$. Combining the $m$ groups of samples together, we finally have the $n$ samples from the mixture model.

### Rejection Sampling

Another fundamental important method to draw i.i.d. samples. It generally says that we can generate variable $Y$ from variable $X$ as long as the density of $X$ covers that of $Y$. Or, if we put it another way, variable $X$ can generate any variables whose densities can be covered by the $X$'s density. The spirit of Rejection Sampling actually coincide that of the axiom schema of specification in the set theory (https://en.wikipedia.org/wiki/Axiom_schema_of_specification). Essentially, it says that any definable subclass of a set is a set. People considered it the most important axiom of the set theory as it avoided the famous Russell's paradox (https://en.wikipedia.org/wiki/Russell%27s_paradox).

**Think further:** Check the axioms in ZFC (a standard set theory system, https://encyclopediaofmath.org/index.php?title=ZFC) to see if there are any other related sampling schemes.

The procedure of Rejection Sampling (given the target distribution $\pi(x)$ and an envelope distribution $q(x)$):
1. Draw initial samples $\{x_1,\dots,x_m\}$ from $q(x)$, and calculate the corresponding ratios or weights $\{w_1,...,w_m\},w_j=w(x_j)=\pi(x_j)/q(x_j)$;
2. Calculate $C=\sup w(x)$, and accept each sample $x_j$ with the probability $p_j=w_j/C$ to obtain the final samples $\{x_1^*,\dots,x_n^*\}$.

There are two ways to do the sampling:
1. We can start the sampling with $m=1$, and increase $m$ until enough final samples are obtained (each sample only be accepted or rejected once).
2. Or because the acceptance rate is $1/C$, to obtain $n$ final samples, we can set $m=Cn$ and make a ‘refund for any overpayment or a supplemental payment for any deficiency’ (多退少补).

* Draw samples from a standard norm distribution based on the t distribution with 1 degree of freedom. Which of the above two sampling methods is faster? (hints: use the 'minimize' function from 'scipy.optimize' (https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html); use the 'datetime' package for timing (https://stackoverflow.com/questions/766335/python-speed-testing-time-difference-milliseconds)

```python

```

**Think further:** When $m$ is big, is it reasonable to replace $C$ in the step 2 by the $\max w_j$? What's the benefits if we do so? Will the resulting final samples still follow the target distribution? Justify your thoughts by mathematics or by simulations.

### Importance Sampling

Remember that sampling is to generate samples to represent the target distribution $\pi(x)$. Different from all the above sampling schemes, the Importance Sampling (IS) generates weighted samples to represent $\pi(x)$. With the proposal distribution $q(x)$ where $x_i$ comes from, we have the non-ratio version weighted samples $\{(x_1,w_1),\dots,(x_n,w_n)\}, w_i=w(x_i)/n$ or the ratio version weighted samples $\{(x_1,w_1),\dots,(x_n,w_n)\}, w_i=w(x_i)/\sum_{i=1}^nw(x_i)$, where $w(x)=\pi(x)/q(x)$ is called the likelihood ratio function or the importance weight function. Here one pair $(x_i,w_i)$, instead of $x_i$, is one single sample. And the ratio version is useful when $\pi(x)$ or $q(x)$ is unnormalized.

**Think further:** Note that the Rejection Sampling and the Importance Sampling both use the ratio function $w(x)$, so what's the relationship between them?

Just like before, under some mild conditions, the weighted samples represent the target distribution in the sense that
$$
\hat{\Pi}(x)=\sum_{i=1}^nw_i\mathbf{1}(x_i\le x)\to\mathbb{E}_q[\frac{\pi(X)}{q(X)}\mathbf{1}(X\le x)]=\mathbb{E}_\pi[\mathbf{1}(X\le x)]=\Pi(x)
$$
And also for a function $h(x)$:
$$
\mu=\mathbb{E}_\pi[h(X)]\approx\hat{\mu}=\sum_{i=1}^nw_ih(x_i)
$$
holds for both the ratio and non-ratio weighted samples.

For the estimator $\hat{\mu}$ with non-ratio weights, we have the asymptotic variance
$$
n\mathrm{Var}_q[\hat{\mu}]\to\mathbb{E}_q[(w(X)h(X)-\mu)^2]=\int\frac{(\pi(x)h(x)-\mu q(x))^2}{q(x)}\mathrm{d}x=\int\frac{(\pi(x)h(x))^2}{q(x)}\mathrm{d}x-\mu^2
$$
Straightly following Jensen’s inequality (https://en.wikipedia.org/wiki/Jensen%27s_inequality), the last expression gives the variance minimizer $q(x)\propto\pi(x)|h(x)|$. As for the estimator with ratio weights, based on the Delta method (https://en.wikipedia.org/wiki/Delta_method), we can derive the asymptotic variance
$$
n\mathrm{Var}_q[\hat{\mu}]\to\mathbb{E}_q[w(X)^2(h(X)-\mu)^2]=\int\frac{\pi(x)^2(h(x)-\mu)^2}{q(x)}\mathrm{d}x
$$
indicating an optimal proposal $q(x)\propto\pi(x)|h(x)-\mu|$ different from the previous one.

In the non-ratio version, IS can potentially give us the zero variance estimator if and only if $h(x)$ is always positive or negative on its support. In the ratio version, the asymptotic variance is zero if and only if $h(x)=c$ for a constant $c$, in which case, we don't even need to do the estimation.

* Let's return to the Laplace approximation example. This time, try using the non-ratio IS to estimate the normalizing constant of the beta posterior $Beta(p|\alpha+y, \beta+n-y)$ ($n$ here is not the sample size of IS samples), which is actually already normalized. Use the Laplace approximation to build the IS proposal, and do the IS estimation based on 100 samples for different $n$. Still, draw the absolute error $|\hat{Z}-1|$ against $n$ based on the loglog plot. What is the convergence rate?

```python

```

This time, I used a little technique called the Common Random Numbers (https://www0.gsb.columbia.edu/mygsb/faculty/research/pubfiles/4261/glasserman_yao_guidelines.pdf). I generated 'samples0' in advance and transformed it to 'samples' for every given IS normal proposal. This technique is especially useful when you want to compare a random value with respect to different parameters. Try what will happen if you don't use the Common Random Numbers.

**Think further:** Compare the codes here with that of the Laplace approximation example. Is the Laplace approximation looks like Importance Sampling with only one sample? Why is the performance of IS so different from that of the Laplace approximation?

```python

```

```python

```

STAT6011 Computational Statistics/Tut 2/Tut 2 (only questions).ipynb


```python
%matplotlib inline
import sympy.physics.mechanics as mech
from sympy import S,Rational,pi
import sympy as sp
```

```python
l1,l2,l3= sp.symbols(r'l1:4')
m1,m2,m3= sp.symbols(r'm1:4')
l,t,m,g= sp.symbols(r'l t m g')
q1,q2,q3 = mech.dynamicsymbols(r'q1:4')
q1d,q2d,q3d = mech.dynamicsymbols(r'q1:4', 1)

# Create and initialize the reference frame
N = mech.ReferenceFrame('N')
pointN = mech.Point('N*')
pointN.set_vel(N, 0)

# Create the points
point1 = pointN.locatenew('point_1', l1*(sp.sin(q1)*N.x-sp.cos(q1)*N.y))
point2 = point1.locatenew('point_2', l2*(sp.sin(q2)*N.x-sp.cos(q2)*N.y))
point3 = point2.locatenew('point_3', l3*(sp.sin(q3)*N.x-sp.cos(q3)*N.y))

# Set the points' velocities
point1.set_vel(N, point1.pos_from(pointN).dt(N))
point2.set_vel(N, point2.pos_from(pointN).dt(N))
point3.set_vel(N, point3.pos_from(pointN).dt(N))

# Create the particles
particle1 = mech.Particle('Particle_1',point1,m1)
particle2 = mech.Particle('Particle_2',point2,m2)
particle3 = mech.Particle('Particle_3',point3,m3)

# Set the particles' potential energy

# Define forces
forces=[(point1,-particle1.mass*g*N.y),(point2,-particle2.mass*g*N.y),(point3,-particle3.mass*g*N.y)]

# Construct the Lagrangian
L = mech.Lagrangian(N, particle1,particle2,particle3)

# Create the LagrangesMethod object
LM = mech.LagrangesMethod(L, [q1,q2,q3], hol_coneqs=None, forcelist=forces, frame=N)

# Form Lagranges Equations
ELeqns = LM.form_lagranges_equations()
ELeqns
```

$\displaystyle \left[\begin{matrix}g l_{1} m_{1} \sin{\left(\operatorname{q_{1}}{\left(t \right)} \right)} + g l_{1} m_{2} \sin{\left(\operatorname{q_{1}}{\left(t \right)} \right)} + g l_{1} m_{3} \sin{\left(\operatorname{q_{1}}{\left(t \right)} \right)} + l_{1}^{2} m_{1} \sin^{2}{\left(\operatorname{q_{1}}{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \operatorname{q_{1}}{\left(t \right)} + l_{1}^{2} m_{1} \cos^{2}{\left(\operatorname{q_{1}}{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \operatorname{q_{1}}{\left(t \right)} + l_{1} m_{2} \left(- l_{1} \sin{\left(\operatorname{q_{1}}{\left(t \right)} \right)} \left(\frac{d}{d t} \operatorname{q_{1}}{\left(t \right)}\right)^{2} + l_{1} \cos{\left(\operatorname{q_{1}}{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \operatorname{q_{1}}{\left(t \right)} - l_{2} \sin{\left(\operatorname{q_{2}}{\left(t \right)} \right)} \left(\frac{d}{d t} \operatorname{q_{2}}{\left(t \right)}\right)^{2} + l_{2} \cos{\left(\operatorname{q_{2}}{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \operatorname{q_{2}}{\left(t \right)}\right) \cos{\left(\operatorname{q_{1}}{\left(t \right)} \right)} + l_{1} m_{2} \left(l_{1} \sin{\left(\operatorname{q_{1}}{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \operatorname{q_{1}}{\left(t \right)} + l_{1} \cos{\left(\operatorname{q_{1}}{\left(t \right)} \right)} \left(\frac{d}{d t} \operatorname{q_{1}}{\left(t \right)}\right)^{2} + l_{2} \sin{\left(\operatorname{q_{2}}{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \operatorname{q_{2}}{\left(t \right)} + l_{2} \cos{\left(\operatorname{q_{2}}{\left(t \right)} \right)} \left(\frac{d}{d t} \operatorname{q_{2}}{\left(t \right)}\right)^{2}\right) \sin{\left(\operatorname{q_{1}}{\left(t \right)} \right)} + l_{1} m_{3} \left(- l_{1} \sin{\left(\operatorname{q_{1}}{\left(t \right)} \right)} \left(\frac{d}{d t} \operatorname{q_{1}}{\left(t \right)}\right)^{2} + l_{1} \cos{\left(\operatorname{q_{1}}{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \operatorname{q_{1}}{\left(t \right)} - l_{2} \sin{\left(\operatorname{q_{2}}{\left(t \right)} \right)} \left(\frac{d}{d t} \operatorname{q_{2}}{\left(t \right)}\right)^{2} + l_{2} \cos{\left(\operatorname{q_{2}}{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \operatorname{q_{2}}{\left(t \right)} - l_{3} \sin{\left(\operatorname{q_{3}}{\left(t \right)} \right)} \left(\frac{d}{d t} \operatorname{q_{3}}{\left(t \right)}\right)^{2} + l_{3} \cos{\left(\operatorname{q_{3}}{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \operatorname{q_{3}}{\left(t \right)}\right) \cos{\left(\operatorname{q_{1}}{\left(t \right)} \right)} + l_{1} m_{3} \left(l_{1} \sin{\left(\operatorname{q_{1}}{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \operatorname{q_{1}}{\left(t \right)} + l_{1} \cos{\left(\operatorname{q_{1}}{\left(t \right)} \right)} \left(\frac{d}{d t} \operatorname{q_{1}}{\left(t \right)}\right)^{2} + l_{2} \sin{\left(\operatorname{q_{2}}{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \operatorname{q_{2}}{\left(t \right)} + l_{2} \cos{\left(\operatorname{q_{2}}{\left(t \right)} \right)} \left(\frac{d}{d t} \operatorname{q_{2}}{\left(t \right)}\right)^{2} + l_{3} \sin{\left(\operatorname{q_{3}}{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \operatorname{q_{3}}{\left(t \right)} + l_{3} \cos{\left(\operatorname{q_{3}}{\left(t \right)} \right)} \left(\frac{d}{d t} \operatorname{q_{3}}{\left(t \right)}\right)^{2}\right) \sin{\left(\operatorname{q_{1}}{\left(t \right)} \right)}\\g l_{2} m_{2} \sin{\left(\operatorname{q_{2}}{\left(t \right)} \right)} + g l_{2} m_{3} \sin{\left(\operatorname{q_{2}}{\left(t \right)} \right)} + l_{2} m_{2} \left(- l_{1} \sin{\left(\operatorname{q_{1}}{\left(t \right)} \right)} \left(\frac{d}{d t} \operatorname{q_{1}}{\left(t \right)}\right)^{2} + l_{1} \cos{\left(\operatorname{q_{1}}{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \operatorname{q_{1}}{\left(t \right)} - l_{2} \sin{\left(\operatorname{q_{2}}{\left(t \right)} \right)} \left(\frac{d}{d t} \operatorname{q_{2}}{\left(t \right)}\right)^{2} + l_{2} \cos{\left(\operatorname{q_{2}}{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \operatorname{q_{2}}{\left(t \right)}\right) \cos{\left(\operatorname{q_{2}}{\left(t \right)} \right)} + l_{2} m_{2} \left(l_{1} \sin{\left(\operatorname{q_{1}}{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \operatorname{q_{1}}{\left(t \right)} + l_{1} \cos{\left(\operatorname{q_{1}}{\left(t \right)} \right)} \left(\frac{d}{d t} \operatorname{q_{1}}{\left(t \right)}\right)^{2} + l_{2} \sin{\left(\operatorname{q_{2}}{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \operatorname{q_{2}}{\left(t \right)} + l_{2} \cos{\left(\operatorname{q_{2}}{\left(t \right)} \right)} \left(\frac{d}{d t} \operatorname{q_{2}}{\left(t \right)}\right)^{2}\right) \sin{\left(\operatorname{q_{2}}{\left(t \right)} \right)} + l_{2} m_{3} \left(- l_{1} \sin{\left(\operatorname{q_{1}}{\left(t \right)} \right)} \left(\frac{d}{d t} \operatorname{q_{1}}{\left(t \right)}\right)^{2} + l_{1} \cos{\left(\operatorname{q_{1}}{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \operatorname{q_{1}}{\left(t \right)} - l_{2} \sin{\left(\operatorname{q_{2}}{\left(t \right)} \right)} \left(\frac{d}{d t} \operatorname{q_{2}}{\left(t \right)}\right)^{2} + l_{2} \cos{\left(\operatorname{q_{2}}{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \operatorname{q_{2}}{\left(t \right)} - l_{3} \sin{\left(\operatorname{q_{3}}{\left(t \right)} \right)} \left(\frac{d}{d t} \operatorname{q_{3}}{\left(t \right)}\right)^{2} + l_{3} \cos{\left(\operatorname{q_{3}}{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \operatorname{q_{3}}{\left(t \right)}\right) \cos{\left(\operatorname{q_{2}}{\left(t \right)} \right)} + l_{2} m_{3} \left(l_{1} \sin{\left(\operatorname{q_{1}}{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \operatorname{q_{1}}{\left(t \right)} + l_{1} \cos{\left(\operatorname{q_{1}}{\left(t \right)} \right)} \left(\frac{d}{d t} \operatorname{q_{1}}{\left(t \right)}\right)^{2} + l_{2} \sin{\left(\operatorname{q_{2}}{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \operatorname{q_{2}}{\left(t \right)} + l_{2} \cos{\left(\operatorname{q_{2}}{\left(t \right)} \right)} \left(\frac{d}{d t} \operatorname{q_{2}}{\left(t \right)}\right)^{2} + l_{3} \sin{\left(\operatorname{q_{3}}{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \operatorname{q_{3}}{\left(t \right)} + l_{3} \cos{\left(\operatorname{q_{3}}{\left(t \right)} \right)} \left(\frac{d}{d t} \operatorname{q_{3}}{\left(t \right)}\right)^{2}\right) \sin{\left(\operatorname{q_{2}}{\left(t \right)} \right)}\\g l_{3} m_{3} \sin{\left(\operatorname{q_{3}}{\left(t \right)} \right)} + l_{3} m_{3} \left(- l_{1} \sin{\left(\operatorname{q_{1}}{\left(t \right)} \right)} \left(\frac{d}{d t} \operatorname{q_{1}}{\left(t \right)}\right)^{2} + l_{1} \cos{\left(\operatorname{q_{1}}{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \operatorname{q_{1}}{\left(t \right)} - l_{2} \sin{\left(\operatorname{q_{2}}{\left(t \right)} \right)} \left(\frac{d}{d t} \operatorname{q_{2}}{\left(t \right)}\right)^{2} + l_{2} \cos{\left(\operatorname{q_{2}}{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \operatorname{q_{2}}{\left(t \right)} - l_{3} \sin{\left(\operatorname{q_{3}}{\left(t \right)} \right)} \left(\frac{d}{d t} \operatorname{q_{3}}{\left(t \right)}\right)^{2} + l_{3} \cos{\left(\operatorname{q_{3}}{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \operatorname{q_{3}}{\left(t \right)}\right) \cos{\left(\operatorname{q_{3}}{\left(t \right)} \right)} + l_{3} m_{3} \left(l_{1} \sin{\left(\operatorname{q_{1}}{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \operatorname{q_{1}}{\left(t \right)} + l_{1} \cos{\left(\operatorname{q_{1}}{\left(t \right)} \right)} \left(\frac{d}{d t} \operatorname{q_{1}}{\left(t \right)}\right)^{2} + l_{2} \sin{\left(\operatorname{q_{2}}{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \operatorname{q_{2}}{\left(t \right)} + l_{2} \cos{\left(\operatorname{q_{2}}{\left(t \right)} \right)} \left(\frac{d}{d t} \operatorname{q_{2}}{\left(t \right)}\right)^{2} + l_{3} \sin{\left(\operatorname{q_{3}}{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \operatorname{q_{3}}{\left(t \right)} + l_{3} \cos{\left(\operatorname{q_{3}}{\left(t \right)} \right)} \left(\frac{d}{d t} \operatorname{q_{3}}{\left(t \right)}\right)^{2}\right) \sin{\left(\operatorname{q_{3}}{\left(t \right)} \right)}\end{matrix}\right]$

```python

```

```python

```


## Tutorial 5. Introduction to functions

Created by Emanuel Flores-Bautista 2019.  All content contained in this notebook is licensed under a [Creative Commons License 4.0](https://creativecommons.org/licenses/by/4.0/). The code is licensed under a [MIT license](https://opensource.org/licenses/MIT).

```python
import numpy as np
import pandas as pd
import seaborn as sns
import scipy.stats as stats
import matplotlib.pyplot as plt
import TCD19_utils as TCD

TCD.set_plotting_style_2()

# Magic command to enable plotting inside notebook
%matplotlib inline
# Magic command to enable svg format in plots
%config InlineBackend.figure_format = 'svg'
```

The purpose of programming is to make our life easier by speeding up the process of making certain operations on data. So far we've used pre-built and customed made functions, but we still have to understand the structure and syntax of functions in python. **Functions** make code reusable and thus is one of the most useful parts of coding. From now on, we will focus on making functions after we've made our data analysis workflows in our  jupyter notebooks.

## Structure and syntax of customized functions in python

As we saw in the previous module, we can think of computational functions  the same way we think of math functions. Functions have **arguments** as inputs, they perform certain operations based on the arguments, and then **returns** and output.

Let's think of a simple function that makes the summation operation.

\begin{align}
f(x)= SUM(X) =  \sum_{i=1}^{n} x_{i} = x_{1} + x_{2} + ... + x_{n}
\end{align}

The function above takes in a vector $X$ as argument, and returns the sum over all of its elements.

To code it in python, the function has to be **defined** using the `def` keyword, followed by the function's name (automatically colored in blue) and a colon. The [PEP-8](https://realpython.com/python-pep8/) guideline recommends to name functions with more than one word should be linked by underscores (e.g. `my_function`).

After the colon, if you click enter, python automatically makes an [indentation](https://en.wikipedia.org/wiki/Indentation_style). After that it is highly recommended to have a doc string with a brief description of the function, its arguments, and its outputs. The doc string is usually written inside triple quotes. Furthermore, after the doc string, there is a block of code which is the operation of the function and finally, to get the result of the function, you need to write `return` and the name of the variable of your output, without parenthesis.

Thus the basic style of a function is the following:

    def my_function_name(args):

        """Docstring explaining the function"""
        block of code

        return result

Now we can write down the function of our summation function.

```python
def summation(x):

    """
    Takes in a list or array and returns the sum of its elements.
    """
    result = 0

    for element in x:

        result += element

    return result
```

That's it, we can now call our function.

```python
summation(np.array([1,2,3]))
```

Luckily, there are a whole bunch of built-in mathematical functions inside numpy and scipy and we can readily use. In general, if you want to do something that seems really common, someone has probably already coded it in python.

For example, the numpy module have built-in **methods** for the sum, mean, median, standard deviations and other operations in numpy arrays. To call this method we just have to write the dot syntax as everything in python is an object.

```python
x = np.array([1,2,3])

x.sum()
```

```python
summation(x) == x.sum()
```

## Built-in functions

Beside the functions inside the different python packages, python has several built-in functions. We have already seen functions like `len()`, `sorted()`, `max()`, `range()`, `print()`, `reversed()`, in addition to data type conversions like `list()`. The complete list of built-in functions can be found in this [link](https://docs.python.org/3/library/functions.html).

Another good programming conventions is the following:

> Never name a function with the same name as a python built-in function.

## Functions don't need to have arguments

After writing functions, it is also useful to know that you can build your own modules to avoid pasting functions to a notebook and making undesirably long notebooks. A module is contained in a file that ends in `.py`. Let's explore the little module I made for the workshop. We can load the `.py` file into the notebook using the `%load` command. After running it, it will become commented out followed by a hash. You can also comment out the `import` statements in order to speed up the processing.

```python
%load TCD19_utils.py
```

As you can see the plotting functions contained do not have arguments. This is an example to show that functions can be called without arguments and they will work !

```python
#Initialize linear spacr
y = np.linspace(0,1, 100)
```

```python
#Call plotting style function
set_plotting_style()
plt.plot(y)
plt.show()
```

```python
#Call plotting style function 2
set_plotting_style_2()
plt.plot(y)
plt.show()
```

## Keyword arguments

A neat feature when writing functions in python is that you don't need to specify the number of arguments you'll use in a function by using `*args` and `**kwargs`. This means you can pass a variable number of arguments to a function. In brief, `*args` help you to specify arguments to send a to send a non-keyworded variable-length argument list to functions. Moreover the `**kwargs` are used to pass a keyworded, variable-length argument dictionary to a function. Let's use an example for you to see their amazing usefulness.

```python
def multiply(*args):
    z = 1
    for num in args:
        z *= num
    print(z)
```

```python
multiply(44, 5)
multiply(10, 92.3)
multiply(2, 3, 44)
multiply(3, 5, 10, 6)
multiply(3,4 ,555, 73.4,3)
```

```python
def print_kwargs(**kwargs):
        print(kwargs)

print_kwargs(kwargs_1="Izamal", kwargs_2=4.5, kwargs_3=True)
```

```python

```


```python
import cirq
import matplotlib.pyplot as plt
```

After seeing how the QFT works and how it can be implemented efficiently, we want to briefly discuss an application of the QFT, namely the phase estimation.
Reading Ch.7.1, arXiv:quant-ph/9511026

The scenario is the following, we are given a unitary operator $U$ and an eigenvector $|\psi\rangle$ corresponding to an unknown eigenvalue $\lambda$. Our task is to compute (approximate) $\lambda$.
For this, we, of course, want to use the QFT. To do so we first notice that since $U$ is unitary we can write $\lambda = e^{2\pi i \phi}$, with $\phi \in [0,1)$.
Let's assume for a moment that we know $\phi$ can be written exact;y with n bits ($\phi = 0.\phi_1\phi_2...\phi_n$), then the phase estimation algoritm becomes clear:
\begin{theorem}
The phase can be calculated (estimated with high probability in the case where $\phi$ can not be written with n bits) Let $N = 2^n$

1. Start with $|\psi_1\rangle = |0\rangle^{n}|\psi\rangle$
2. Apply $H^{\otimes n} \otimes I$ to $|\psi_1\rangle$ giving $|\psi_2\rangle =\frac{1}{2^{n/2}}\sum^{N-1}_j|j\rangle|\psi\rangle$
3. Apply $|\psi_3\rangle = \tilde{U}|\psi_2\rangle = {2^{-n/2}}\sum^{N-1}_j|j\rangle U^j |\psi\rangle = {2^{-n/2}}\sum^{N-1}_j|j\rangle e^{2\pi i \phi j} |\psi\rangle = {2^{-n/2}}\sum^{N-1}_je^{2\pi i (2^n \phi) j/2^n} |j\rangle |\psi\rangle = \mathrm{QFT}|2^n\phi\rangle |\psi\rangle$
4. Apply $|\psi_4\rangle =(\mathrm{QFT}^{-1}_{2^N} \otimes I)|\psi_3\rangle = |2^n\phi\rangle |\psi\rangle$ and measure the first n-qubits
\end{theorem}
In case the phase can not be written precisely with $n$ bits, the algorithm will return $\hat{x}$, such that ${\hat{x}}/2^n$ is the closest integer multiple of $1/2^n$ to $\omega$ with probability $\frac{4}{\pi^2}$. See for example 7.1.1 for a detailed discussion.

We are now going to implement this algorithm for two cases.
1. the almost trivial case where U = Z and we are given $(|0\rangle)|1\rangle$ (which has eigenvalue $1, \phi = 0.0$ ($-1,\phi = 1/2$ corresponding to $\phi =0.1$ in binary))
2. The slightly more Interesting case of a phase gate $R_\phi$, here we can also play with the number of bits in the first register

```python
def phase_est_Z(one = True):
    c = cirq.Circuit()
    #only need two qubits
    q = cirq.LineQubit.range(2)
    # apply H to first qubit
    c.append([cirq.H(q[0])])
    #bring second into eigenstate |1>
    if one == True:
        c.append([cirq.X(q[1])])
    #apply \tilde{U}=cU
    c.append(cirq.CZ(q[0],q[1]))
    #c.append(cirq.Z(q[1]))
    #apply inverse qft (see 5. for implementation of qft)
    c.append(cirq.qft(q[0])**-1)
    #measure first register
    c.append(cirq.ops.measure(q[0]))
    return c

```

```python
phase_est_Z()
```

0: ───H───@───qft^-1───M───
          │
1: ───X───@────────────────

```python
s=cirq.Simulator()
c =phase_est_Z(True)
```

```python
# eigenvalue -1 so we expect phase of \pi which corresponds to \phi =1
s.simulate(c)
```

    measurements: 0=1
    output vector: |11⟩

```python
# eigenvalue 1 so we expect phase of 0 which corresponds to \phi =0
s=cirq.Simulator()
c =phase_est_Z(False)
```

```python
s.simulate(c)
```

    measurements: 0=0
    output vector: |00⟩

Let us now turn to the more interesting case, where we will estimate the phase of
\begin{equation}
R_\phi = \begin{bmatrix} 1 &0 \\ 0 & e^{2\pi i \phi}
\end{bmatrix}= Z^{2\phi}
\end{equation}
This can now be implemented in a straightforward manner using controlled $Z$ gates.
We want to have the follwoing unitary transformation
\begin{equation}
\tilde{U}|j\rangle|\psi\rangle  =|j\rangle U^j |\psi\rangle,
\end{equation}
where we understand $j$ as the integer corresponding to it's binary representation (e.g. $3 = 1*2^1+1*2^0 = 11$) hence to every qubit of the register (numbered $i=0,1,...n-1$) we assign the correspoding controlled gate $cZ^{2^{n-i}}= cR^{2^{n-i-1}}$. It should become now clear, that for every number we will get the right number of $U$. To demonstrate this let's stick with our example $3 = 1*2^1+1*2^0 = 11$. If our rgeister is in the state $|11\rangle$ we will apply $R^{2^{2-0-1}}R^{2^{2-1-1}} =R^{2^{1}}R^{2^{0}}= R^3 $.

```python
def phase_est_R_circuit(n, phi, one = True):
    c = cirq.Circuit()
    #only need two qubits
    q = cirq.LineQubit.range(n+1)
    # apply H to first qubit
    c.append([cirq.H(q[i]) for i in range(n)])
    #bring second into eigenstate |1>
    if one == True:
        c.append([cirq.X(q[n])])
    #apply \tilde{U} see text for explanation
    c.append([(cirq.Z(q[n])**(phi*(2**(n-i)))).controlled_by(q[i]) for i in range(n)])
    #apply inverse qft (see 5. for implementation of qft)
    c.append(cirq.qft(*[q[i] for i in range(n)])**-1)
    #measure first register
    c.append([cirq.ops.measure(*q[0:n], key='first_reg')])
    return c

```

```python
s=cirq.Simulator()
c = phase_est_R_circuit(3,3/8,True)
c
```

0: ───H───@───────────────────qft^-1───M('first_reg')───
          │                   │        │
1: ───H───┼───@───────────────#2───────M────────────────
          │   │               │        │
2: ───H───┼───┼──────@────────#3───────M────────────────
          │   │      │
3: ───X───Z───S^-1───Z^0.75─────────────────────────────

```python
def phase_est_R(n ,phi, one =True, n_measurements = 100):
    s=cirq.Simulator()
    c = phase_est_R_circuit(n, phi, True)
    measured = []
    for i in range(n_measurements):
        m = s.simulate(c)
        x = sum([2**(-i-1)*m.measurements['first_reg'][i] for i in range(n)])
        measured.append(x)
    estimate = max(set(measured), key = measured.count)
    return measured, estimate
```

```python
#3/8 = 1*2^-1+0*2^-2*1+1*2^-3 so can be represented with 3 bits
x = phase_est_R(3,3/8,True)
plt.hist(x[0])
x[1]
```

```python
#1/3 can't be represented with finite number of bits
x = phase_est_R(3,1/3,True)
plt.hist(x[0])
x[1]
```

As we see we get the closest estimate to $1/3$ that can be represented with the number of bits.

Let's see how the results improve with increasing number of bits $n$

```python
x = phase_est_R(10,1/3,True)
plt.hist(x[0])
x[1]
```

```python
x = phase_est_R(12,1/3,True)
plt.hist(x[0])
x[1]
```

```python
x = phase_est_R(15,1/3,True,  n_measurements = 30)
plt.hist(x[0])
x[1]
```

```python

```


# Evolutionary game theory - solutions

1. Assume the frequency dependent selection model for a population with two types of individuals: $x=(x_1, x_2)$ such that $x_1 + x_2 = 1$. Obtain all the stable distribution for the sytem defined by the following fitness functions:

   For all of the functions in question, $x=(0, 1)$ and $x=(1, 0)$ are equilibria. There is a 3rd potential equilibria given by $f_1(x) = f_2(x)$. This is bookwork: https://vknight.org/gt/chapters/11/#Frequency-dependent-selection

   1. $f_1(x)=x_1 - x_2\qquad f_2(x)=x_2 - 2 x_1$

      $f_1(x)=f_2(x)\Rightarrow x_1 - x_2 = x_2 - 2x_1 \Rightarrow 3x_1 = 2x_2$ which gives (using the fact that $x_1 + x_2=1$ single solution: $(x_1, x_2)=(2/5, 3/5)$

```python
import sympy as sym
x_1 = sym.symbols("x_1")
sym.solveset(3 * x_1 - 2 * (1 - x_1), x_1)
```

    {2/5}

`B`. $f_1(x)=x_1x_2 - x_2\qquad f_2(x)=x_2 - x_1 + 1/2$

$f_1(x)=f_2(x)\Rightarrow x_1x_2 - x_2 = x_2 - x_1 + 1/2$ setting $x=x_1$ so that $1 - x = x_2$ gives: $x - x ^ 2 - 1 + x = 1 - x - x + 1/2$ which corresponds to:

$$-x ^ 2 + 4 x - 5/2=0$$

This has solution $x=2 \pm \sqrt{6}/2$, thus $(x_1, x_2) = (2 - \sqrt{6}/2, -1 + \sqrt{6}/2)$ is the only set of solutions for which $1 \geq x_1 \geq 0$ and $1\geq x_2 \geq 0$.

```python
x = sym.symbols("x", positive=True)
res = sym.solveset(- x ** 2 + 4 * x - sym.S(5) / 2, x)
res
```

    {-sqrt(6)/2 + 2, sqrt(6)/2 + 2}

```python
for sol in list(res):
    print(sol, float(sol), float(1 - sol))
```

    -sqrt(6)/2 + 2 0.775255128608411 0.22474487139158905
    sqrt(6)/2 + 2 3.224744871391589 -2.224744871391589

`C`. $f_1(x)=x_1 ^ 2 \qquad f_2(x)=x_2^2$

$f_1(x)=f_2(x)\Rightarrow x_1 = \pm x_2$ which has a single solution $(x_1, x_2)=(1/2, 1/2)$.

`2`. For the following games, obtain all the stable distributions for the evolutionary game:

   For bother of these we aim to find $x$ such that $(1, 0)Ax^T=(0, 1)Ax^T$.

   1. $A = \begin{pmatrix}2 & 4 \\ 5 & 3\end{pmatrix}$

      $(1, 0)Ax^T=(0, 1)Ax^T\Rightarrow 2x+4(1-x)=5x+3(1-x)\Rightarrow x=1/4$

   2. $A = \begin{pmatrix}1 & 0 \\ 0 & 1\end{pmatrix}$

      $(1, 0)Ax^T=(0, 1)Ax^T\Rightarrow x=1-x\Rightarrow x=1/2$

`3`. Define:
   1. mutated population.

      Bookwork: https://vknight.org/gt/chapters/11/#Mutated-population

   2. Evolutionary stable strategies

      Bookwork: https://vknight.org/gt/chapters/11/#Evolutionary-Stable-Strategies

`4`. State and prove the general condition for ESS theorem.

   Bookwork: https://vknight.org/gt/chapters/11/#General-condition-for-ESS

`5`. Using the general condition for ESS theorem identify what strategies are evolutionarily stable for the games of question 2.

   For both these games we start by identifying the Nash equilibria:

   1. $(A, A^T) = (\begin{pmatrix}2 & \underline{4} \\ \underline{5} & 3\end{pmatrix}, \begin{pmatrix}2 & \underline{5} \\ \underline{4} & 3\end{pmatrix})$

      The pure NE are given by $\{((1, 0), (0, 1)), ((0, 1), (1, 0))\}$, these are not symmetric so do not correspond to ESS. The mixed nash equilibrium corresonds to the stationary point calculated previously: $x=1/4$.

      For a general $y$, we have $u(x, x)=u(y,x)$ (by the defnining calculation of $x$). Thus, we look at the second condition:

      $$u(x, y)=1/4(2y+4(1-y)) + 3/4(5y+3(1-y))=1/4(4-2y) + 1/4(6y+9)=1/4(4y+13)$$

      $$u(y, y)=y(2y+4(1-y)) + (1-y)(5y+3(1-y))=y(4-2y) + (1-y)(2y+3)=-4y^2+3y+3$$

      thus:

      $$u(x, y) - u(y, y) = 4y^2 - 2y + 1/4=\frac{(4y-1)^2}{4}$$

      which is $>0$ for $y\ne 1/4$, thus $x=(1/4, 3/4)$ is an ESS.

   2. $(A, A^T) = (\begin{pmatrix}\underline{1} & 0 \\ 0 & \underline{1}\end{pmatrix}, \begin{pmatrix}\underline{1} & 0 \\ 0 & \underline{1}\end{pmatrix})$

      The pure NE are given by $\{((1, 0), (1, 0)), ((0, 1), (0, 1))\}$, these are symmetric, for both we have $u(x, x)=1$ which will be $>u(y, x)$ for all $y\ne x$ as $x$ is a best response to itself. Thus, these are both ESS.

      We now consider $x=1/2$. For a general $y$, we have $u(x, x)=u(y,x)$ (by the defnining calculation of $x$). Thus, we look at the second condition:

      $$u(x, y)=1/2(y + 1 - y)=1/2$$

      $$u(y, y)=y^2 + (1-y)^2=2y^2-2y+1$$

      thus:

      $$u(x, y) - u(y, y) = -2y^2 + 2 y - 1/2=-\frac{(2y-1)^2}{2}$$

      which is $<0$ for $y\ne 1/2$, thus $x=(1/2, 1/2)$ is not an ESS.


# Optimización con restriccións: extremos condicionados

Para resolver problemas de optimización (cálculo de máximos e mínimos absolutos) sobre unha rexión do espazo pechada e acoutada, debemos ter en conta as restriccións que supón traballar nestes dominios e non en todo o espazo (de dúas ou tres dimensións). Esto tipo de cálculos son os que se consideran na optimización con restriccións e neste caso, o cálculo dos extremos (máximos e mínimos) se califican como extremos condicionados (frente ás restriccións que teñen que verificar). Como xa puidemos estudar en nas sesións de pizarra, o cálculo dos extremos relativos baséase no uso de multiplicadores de Lagrange.

Nesta práctica volveremos a usar o módulo **Sympy** para calcular os extremos e para representar as funcións empregaremos os módulos **Matplotlib** e **Numpy**.

### Obxectivos:

- Cálculo de extremos condicionados sobre unha curva do plano
- Cálculo de extremos condicionados sobre unha rexión pechada do plano
- Cálculo de extremos condicionados con varias restriccións

## Cálculo de extremos condicionados sobre unha curva do plano
Nesta práctica usaremos tanto o módulo **Sympy**, como tamén **Numpy** e **Matplotlib**. Así que debemos importalos para o resto do guión de prácticas:

```python
import sympy as sp
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
```

Como xa aconteceu en prácticas anteriores, debemos facer unha implementación propia para calcular o gradiente dunha función escalar $f$. Para iso usaremos a relación que xa coñecemos entre a matriz Xacobiana $Df$ dunha función escalar e o vector (columna) gradiente $\nabla f$, isto é $\nabla f=Df^{t}$:

```python
gradient = lambda f, v: sp.transpose(sp.Matrix([f]).jacobian(v))
```

Para ilustrar o uso dos multiplicadores de Lagrange, imos a calcular os valores máximo e mínimo absolutos da función $f(x,y,z)=x+y$ sobre os puntos da elipse $(x-1)^2/64+(y-2)^2/36=1$:

```python
x, y, l = sp.symbols('x y l', real=True) # define as variables simbólicas x, y, l
f = sp.Lambda((x,y), x+y) # función a optimizar
g = sp.Lambda((x,y), (x-1)**2/64 + y**2/36) # restricción

# Cálculo de puntos críticos
grad_f = gradient(f(x,y),(x,y))
grad_g = gradient(g(x,y),(x,y))
sol = sp.solve((sp.Eq(grad_f[0],l*grad_g[0]),sp.Eq(grad_f[1],l*grad_g[1]),sp.Eq(g(x,y),1)), (x,y,l))
for p in sol:
    print('Critical point (x,y,lambda)=',p,'; f(x,y)=', sp.N(f(*p[0:2])))
```

    Critical point (x,y,lambda)= (-27/5, -18/5, -5) ; f(x,y)= -9.00000000000000
    Critical point (x,y,lambda)= (37/5, 18/5, 5) ; f(x,y)= 11.0000000000000

Para comprobar visualmente o tipo de puntos críticos que posúe esta función, a podemos representar gráficamente:

```python
# Nube de puntos para o cálculo das curvas de nivel
N = 100
xvec = np.linspace(-10, 10, N)
yvec = np.linspace(-10, 10, N)
X, Y = np.meshgrid(xvec, yvec)
F = sp.lambdify((x,y),f(x,y),"numpy")

# Representación gráfica da función f
plt.contourf(X, Y, F(X,Y))
plt.colorbar()
plt.xlabel('x')
plt.ylabel('y')
plt.axis('square')

# Representación gráfica da restricción
xvec = np.linspace(-7, 9, N)
sol_curve = sp.solve(sp.Eq(g(x,y),1),y) # despexar y en función de x en g(x,y)=1
for c in sol_curve:
    curve = sp.lambdify(x,c,"numpy")
    plt.plot(xvec, curve(xvec),'r')

# Representación gráfica dos extremos relativos
for p in sol:
    plt.plot(p[0],p[1],'ko')

plt.show()
```

### **Exercicio 9.1**
Calcula a distancia mínima do punto $(0,4)$ á parábola $x^2-4y=0$. Xustifica porque é posible asegurar que o mínimo é absoluto.

```python
# O TEU CÓDIGO AQUÍ
```

## Cálculo de extremos condicionados sobre unha rexión do plano

Para ilustrar o uso dos multiplicadores de Lagrange, imos a calcular os valores máximo e mínimo absolutos da función $f(x,y,z)=x^2+y^2$ dentro da elipse $(x-1)^2/64+(y-2)^2/36\le 1$. Para facer isto, debemos proceder en dúas fases:

Primeiro, calculamos os candidatos a extremos relativos sobre a fronteira da rexión $(x-1)^2/64+(y-2)^2/36 = 1$:

```python
x, y, l = sp.symbols('x y l', real=True) # define as variables simbólicas x, y, l
f = sp.Lambda((x,y), x**2 + y**2) # función a optimizar
g = sp.Lambda((x,y), (x-1)**2/64 + y**2/36) # restricción

# Cálculo de puntos críticos
grad_f = gradient(f(x,y),(x,y))
grad_g = gradient(g(x,y),(x,y))
sol = sp.solve((sp.Eq(grad_f[0],l*grad_g[0]),sp.Eq(grad_f[1],l*grad_g[1]),sp.Eq(g(x,y),1)), (x,y,l))
for p in sol:
    print('Critical point (x,y,lambda)=',p,'; f(x,y)=', sp.N(f(*p[0:2])))
```

    Critical point (x,y,lambda)= (-7, 0, 56) ; f(x,y)= 49.0000000000000
    Critical point (x,y,lambda)= (-9/7, -18*sqrt(5)/7, 36) ; f(x,y)= 34.7142857142857
    Critical point (x,y,lambda)= (-9/7, 18*sqrt(5)/7, 36) ; f(x,y)= 34.7142857142857
    Critical point (x,y,lambda)= (9, 0, 72) ; f(x,y)= 81.0000000000000

En segundo lugar, para completar o estudo dos extremos relativos, tamén debemos comprobar cales son os puntos críticos da función no interior da rexión elíptica $(x-1)^2/64+(y-2)^2/36 < 1$:

```python
# Cálculo de puntos críticos
sol_int = sp.solve((sp.Eq(grad_f[0],0),sp.Eq(grad_f[1],0)), (x,y))
for p in sol_int:
    print('Critical point (x,y)=(',sol_int[x],sol_int[y],'); f(x,y)=', sp.N(f(sol_int[x],sol_int[y])))
```

    Critical point (x,y)=( 0 0 ); f(x,y)= 0
    Critical point (x,y)=( 0 0 ); f(x,y)= 0

Para comprobar visualmente o tipo de puntos críticos que posúe esta función, a podemos representar gráficamente:

```python
# Nube de puntos para o cálculo das curvas de nivel
N = 100
xvec = np.linspace(-10, 10, N)
yvec = np.linspace(-10, 10, N)
X, Y = np.meshgrid(xvec, yvec)
F = sp.lambdify((x,y),f(x,y),"numpy")

# Representación gráfica da función f
plt.contourf(X, Y, F(X,Y))
plt.colorbar()
plt.xlabel('x')
plt.ylabel('y')
plt.axis('square')

# Representación gráfica da restricción
xvec = np.linspace(-7, 9, N)
sol_curve = sp.solve(sp.Eq(g(x,y),1),y) # despexar y en función de x en g(x,y)=1
for c in sol_curve:
    curve = sp.lambdify(x,c,"numpy")
    plt.plot(xvec, curve(xvec),'r')

# Representación gráfica dos extremos relativos na fronteira
for p in sol:
    plt.plot(p[0],p[1],'ko')

# Representación gráfica dos extremos relativos no interior
plt.plot(sol_int[x],sol_int[y],'co')

plt.show()
```

### **Exercicio 9.2**
Calcula os extremos absolutos da función $f(x,y)=2x^2-3y^2-2x$ no círculo centrado en $(0,0)$ e de radio $\sqrt{5}$ (tendo en conta tanto o interior como a súa fronteira).

```python
# O TEU CÓDIGO AQUÍ
```

## Cálculo de extremos condicionados con varias restriccións
O cálculo de extremos condicionados con varias restriccións posúe a única complexidade de manexar varios multiplicadores de Lagrange ao mesmo tempo, cada un deles asociado a cada unha das diferentes restriccións do problema. Vexamos un exemplo que involucra dúas restriccións: Calcular o máximo e mínimo absoluto da función $f(x,y,z)=20+2x+2y+z^2$ na intersección da esfera $x^2+y^2+z^2=11$ e o plano $x+y+z=3$:

```python
x, y, z, l1, l2 = sp.symbols('x y z l1 l2', real=True) # define as variables simbólicas x, y, l
f = sp.Lambda((x,y,z), 20 + 2*x +2*y + z**2) # función a optimizar
g1 = sp.Lambda((x,y,z), x**2+y**2+z**2) # restricción =11
g2 = sp.Lambda((x,y,z), x+y+z) # restricción =3

# Cálculo de puntos críticos
grad_f = gradient(f(x,y,z),(x,y,z))
grad_g1 = gradient(g1(x,y,z),(x,y,z))
grad_g2 = gradient(g2(x,y,z),(x,y,z))

sol = sp.solve((sp.Eq(grad_f[0],l1*grad_g1[0]+l2*grad_g2[0]),
                sp.Eq(grad_f[1],l1*grad_g1[1]+l2*grad_g2[1]),
                sp.Eq(grad_f[2],l1*grad_g1[2]+l2*grad_g2[2]), sp.Eq(g1(x,y,z),11), sp.Eq(g2(x,y,z),3)), (x,y,z,l1,l2))
for p in sol:
    print('Critical point (x,y,z,lambda1, lambda2)=',p,'; f(x,y)=', sp.N(f(*p[0:3])))
```

    Critical point (x,y,z,lambda1, lambda2)= (-1, 3, 1, 0, 2) ; f(x,y)= 25.0000000000000
    Critical point (x,y,z,lambda1, lambda2)= (3, -1, 1, 0, 2) ; f(x,y)= 25.0000000000000
    Critical point (x,y,z,lambda1, lambda2)= (1 - 2*sqrt(3)/3, 1 - 2*sqrt(3)/3, 1 + 4*sqrt(3)/3, 2/3, 2/3 + 8*sqrt(3)/9) ; f(x,y)= 30.3333333333333
    Critical point (x,y,z,lambda1, lambda2)= (1 + 2*sqrt(3)/3, 1 + 2*sqrt(3)/3, 1 - 4*sqrt(3)/3, 2/3, 2/3 - 8*sqrt(3)/9) ; f(x,y)= 30.3333333333333

### **Exercicio 9.3**
Calcular o máximo e mínimo absoluto da función $f(x,y,z)=20+2x+2y+z^2$ na intersección do cilindro de radio $1$ e eixe $OZ$ e o plano $x-z=1$:

```python
# O TEU CÓDIGO AQUÍ
```


# Random Signals

*This jupyter notebook is part of a [collection of notebooks](../index.ipynb) on various topics of Digital Signal Processing. Please direct questions and suggestions to [Sascha.Spors@uni-rostock.de](mailto:Sascha.Spors@uni-rostock.de).*

## Stationary Random Processes

### Definition

When the average statistical properties of a random process do not depend on the time index $k$, this process is termed as [*stationary random process*](https://en.wikipedia.org/wiki/Stationary_process). This can be expressed formally as

\begin{equation}
E\{ f(x[k_1], x[k_2], \dots) \} = E\{ f(x[k_1 + \Delta], x[k_2 + \Delta], \dots) \}
\end{equation}

where $\Delta \in \mathbb{Z}$ denotes an arbitrary shift of the sample index $k$. Above condition has to hold for all possible mapping functions $f(\cdot)$. From this definition it becomes clear that

* random signals of finite length and
* deterministic signals $s[k] \neq C$ with $C \in \mathbb{R}$

cannot be stationary random processes in a strict sense. However, in practice it is often assumed to be sufficient if above condition holds within a finite interval of sample indexes of a random signal.

### Cumulative Distribution Functions and Probability Density Functions

It follows from above definition of a stationary process, that the univariate cumulative distribution function (CDF) of a stationary random process does not depend on the time index $k$

\begin{equation}
P_x(\theta, k) = P_x(\theta)
\end{equation}

the same holds for the univariate probability density function (PDF)

\begin{equation}
p_x(\theta, k) = p_x(\theta)
\end{equation}

The bivariate CDF of two stationary random signals $x[k]$ and $y[k]$ depends only on the difference $\kappa = k_x - k_y$

\begin{equation}
P_{xy}(\theta_x, \theta_y, k_x, k_y) = P_{xy}(\theta_x, \theta_y, \kappa)
\end{equation}

The same holds for the bivariate PDF

\begin{equation}
p_{xy}(\theta_x, \theta_y, k_x, k_y) = p_{xy}(\theta_x, \theta_y, \kappa)
\end{equation}

### First-Order Ensemble Averages

According to above definition of stationarity, for the first-order ensemble average of a stationary process the following relation must hold

\begin{equation}
E\{ f(x[k]) \} = E\{ f(x[k + \Delta]) \}
\end{equation}

hence it does not depend on the time index $k$. For the linear mean we consequently yield

\begin{equation}
\mu_x[k] = \mu_x
\end{equation}

and for the variance

\begin{equation}
\sigma_x^2[k] = \sigma_x^2
\end{equation}

### Cross- and Auto-Correlation Function

It follows by introducing the PDF's properties of a stationary process into the definition of the cross-correlation function (CCF) and auto-correlation function (ACF) that

\begin{equation}
\varphi_{xy}[k_x, k_y] = \varphi_{xy}[\kappa] = E\{ x[k] \cdot y[k-\kappa]\} = E\{ x[k+\kappa] \cdot y[k]\}
\end{equation}

and

\begin{equation}
\varphi_{xx}[k_1, k_2] = \varphi_{xx}[\kappa] = E\{ x[k] \cdot x[k-\kappa]\} = E\{ x[k+\kappa] \cdot x[k]\}
\end{equation}

where for the latter $\kappa = k_1 - k_2$.

## Wide-Sense Stationary Random Processes

### Definition

The definition of a stationary random process in the previous section must hold for any mapping function $f(\cdot)$. In practice this cannot be checked in a strict sense. For a wide-sense stationary (WSS) random process the conditions for stationarity must hold only for linear mappings. This leads to the following two conditions a WSS random process has to fulfill

\begin{equation}
E\{ x[k_1] \cdot x[k_2] \} = E\{ x[k_1 + \Delta] \cdot x[k_2 + \Delta] \}
\end{equation}

and

\begin{equation}
E\{ x[k] \} = E\{ x[k + \Delta] \}
\end{equation}

A random signal of finite length cannot be WSS in a strict sense.

### Example - Evaluation of Wide-Sense Stationarity

From above definition of a WSS process it is evident that it is sufficient to check the time dependence of the linear mean $x_\mu[k]$ and the auto-correlation function $\varphi_{xx}[k_1, k_2]$. Both quantities are estimated and plotted for two different random processes.

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

L = 64  # number of random samples
N = 1000  # number of sample functions

# generate sample functions
np.random.seed(1)
x = np.random.normal(size=(N, L))
x1 = x + np.tile(np.cos(2*np.pi/L*np.arange(L)), [N,1])
h = 2*np.fft.irfft([1,1,1,0,0,0])
x2 = np.asarray([np.convolve(x[n,:], h, mode='same') for n in range(N)])

# compute and plot results
def compute_plot_results(x):

    # estimate linear mean by ensemble average
    mu = 1/N * np.sum(x, 0)
    # estimate the auto-correlation function
    acf = np.zeros((L, L))
    for n in range(L):
        for m in range(L):
            acf[n, m] = 1/N * np.sum(x[:, n]*x[:, m], 0)

    plt.subplot(121)
    plt.stem(mu)
    plt.title(r'Estimate of linear mean $\hat{\mu}_x[k]$')
    plt.xlabel(r'$k$')
    plt.ylabel(r'$\hat{\mu}[k]$')
    plt.axis([0, L, -1.5, 1.5])

    plt.subplot(122)
    plt.pcolor(np.arange(L), np.arange(L), acf, vmin=-2, vmax=2)
    plt.title(r'Estimate of ACF $\hat{\varphi}_{xx}[k_1, k_2]$')
    plt.xlabel(r'$k_1$')
    plt.ylabel(r'$k_2$')
    plt.colorbar()
    plt.autoscale(tight=True)

plt.figure(figsize = (10, 4))
plt.gcf().suptitle('Random Process 1', fontsize=12, y=1.05)
compute_plot_results(x1)

plt.figure(figsize = (10, 4))
plt.gcf().suptitle('Random Process 2', fontsize=12, y=1.05)
compute_plot_results(x2)
```

**Exercise**

* Which process can be assumed to be WSS? Why?
* Increase the number `N` of sample functions. Do the results support your initial assumption?

Inspection of the estimated linear mean $\hat{\mu}_x[k]$ for the first random process reveals that it is highly dependent on the sample index $k$. The same holds for the ACF which depends on both sample indexes $k_1$ and $k_2$. For the second random process, the estimated linear mean can be assumed to be approximately zero. Its ACF depends approximately only on the difference $\kappa = k_1 - k_2$, as can be concluded from the diagonal structures in the plot. Increasing the number of sample functions $N$ increases the certainty of the estimates in the statistical sense.

## Higher-Order Temporal Averages

Ensemble averages are defined as the average across all sample functions $x_n[k]$ for a particular time index $k$. So far, averaging over the sample index $k$ of one sample function was not considered. Such averaging is termed commonly as temporal averaging since the sample index $k$ represents time in many cases. For a stationary process, the higher-order temporal average along the $n$-th sample function is defined as

\begin{equation}
\overline{ f(x_n[k], x_n[k-\kappa_1], x_n[k-\kappa_2], \dots) } = \lim_{K \to \infty} \frac{1}{2K + 1} \sum_{k = -K}^{K} f(x_n[k], x_n[k-\kappa_1], x_n[k-\kappa_2], \dots)
\end{equation}

where $\kappa_i \in \mathbb{Z} \; \forall i$. The introduced statistical measures, like the linear mean, variance and correlation functions, can be computed for each sample function by temporal averaging by using the respective mapping functions $f(\cdot)$ introduced above. The linear mean as temporal average of the $n$-th sample function $x_n[k]$ is for instance given by

\begin{equation}
\overline{x_n[k]} = \lim_{K \to \infty} \frac{1}{2K + 1} \sum_{k = -K}^{K} x_n[k]
\end{equation}

## Ergodic Random Processes

An [ergodic process](https://en.wikipedia.org/wiki/Ergodic_process) is a stationary random process whose higher-order temporal averages of all sample functions are equal to the ensemble averages

\begin{equation}
\overline{ f(x_n[k], x_n[k-\kappa_1], x_n[k-\kappa_2], \dots) } = E\{  f(x[k], x[k-\kappa_1], x[k-\kappa_2], \dots)  \} \;\; \forall n
\end{equation}

This implies that all higher-order temporal averages are equal. Any sample function from the process represents the average statistical properties of the entire process. The ensemble averages for a stationary and ergodic random process are given by the temporal averages of one sample function. This result is very important for the practical computation of statistical properties of random signals.

## Wide-Sense Ergodic Random Processes

### Definition

As for a WSS process, the conditions for ergodicity have to hold only for linear mappings $f(\cdot)$. Under the assumption of a WSS process, the following two conditions have to be met by a wide-sense ergodic random process

\begin{equation}
\overline{ x_n[k] \cdot x_n[k-\kappa] } = E\{  x[k] \cdot x[k-\kappa]  \} \;\; \forall n
\end{equation}

and

\begin{equation}
\overline{ x_n[k] } = E\{  x[k] \} \;\; \forall n
\end{equation}

### Example - Evaluation of Wide-Sense Ergodicity

In the following example, the linear mean and autocorrelation function are computed as temporal and ensemble averages for three random processes. The plots show the estimated temporal averages $\overline{ x_n[k] }$ and $\overline{ x_n[k] \cdot x_n[k-\kappa] }$ on the right-hand side of the sample functions $x_n[k]$. Note, the linear mean as temporal average is a scalar value whose value is illustrated by a bar plot. The estimated ensemble averages $E\{  x[k] \}$ and $E\{  x[k_1] \cdot x[k_2]  \}$ are shown below the sample functions to indicate the averaging across sample functions.

```python
L = 64  # number of random samples
N = 10000  # number of sample functions

# generate sample functions
np.random.seed(11)
x = np.random.normal(size=(N, L))
k = np.arange(L)
x1 = x + np.tile(np.cos(2*np.pi/L*k), [N, 1])
x2 = x + np.tile([np.ones(L), -np.ones(L)], [N//2, 1])
x3 = x + np.ones([N, L])

# function to compute and plot results
def compute_plot_results(x):

    # estimate linear mean by ensemble average
    mu = 1/N * np.sum(x, 0)
    # estimate the auto-correlation function by ensemble average
    acf = np.zeros((L, L))
    for n in range(L):
        for m in range(L):
            acf[n, m] = 1/N * np.sum(x[:, n]*x[:, m], 0)
    # estimate linear mean as temporal average
    mut = 1/L * np.sum(x, 1)
    # estimate the auto-correlation function as temporal average
    acft = np.zeros((N, L))
    for n in range(N):
        acft[n, :] = np.correlate(x[n, :], x[n, :], mode='same')
    kappa = np.arange(L) - L//2

    for n in range(2):
        plt.figure(figsize = (10, 5))
        plt.subplot(131)
        plt.stem(x[n, :])
        plt.title(r'Sample function $x_%d[k]$'%n)
        plt.xlabel(r'$k$')
        plt.ylabel(r'$x_%d[k]$'%n)
        plt.axis([0, L, -4, 4])

        plt.subplot(132)
        plt.bar(-0.4, mut[n])
        plt.title(r'Linear mean $\overline{ x_%d[k] }$'%n)
        plt.ylabel(r'$\hat{\mu}_{x,%d}$'%n)
        plt.axis([-.5, .5, -1.5, 1.5])

        plt.subplot(133)
        plt.stem(kappa, acft[n, :])
        plt.title(r'Autocorrelation $\overline{ x_%d[k] \cdot x_%d[k-\kappa] }$'%(n,n))
        plt.xlabel(r'$\kappa$')
        plt.ylabel(r'$\hat{\varphi}_{xx,%d}[\kappa]$'%n)
        plt.axis([-L//2, L//2, -30, 150])
        plt.tight_layout()

    plt.figure(figsize = (10, 5))
    plt.subplot(131)
    plt.stem(mu)
    plt.title(r'Linear mean $E\{  x[k] \}$')
    plt.xlabel(r'$k$')
    plt.ylabel(r'$\hat{\mu}[k]$')
    plt.axis([0, L, -1.5, 1.5])

    plt.figure(figsize = (4, 4))
    plt.pcolor(k, k, acf, vmin=-2, vmax=2)
    plt.title(r'ACF $E\{  x[k_1] \cdot x[k_2]  \}$')
    plt.xlabel(r'$k_1$')
    plt.ylabel(r'$k_2$')
    plt.colorbar()
    plt.autoscale(tight=True)
```

**Random Process 1**

```python
compute_plot_results(x1)
```

**Random Process 2**

```python
compute_plot_results(x2)
```

**Random Process 3**

```python
compute_plot_results(x3)
```

**Exercise**

* Which process can be assumed to be stationary and/or ergodic? Why?

The linear mean $\mu_x[k]$ estimated by the ensemble average of the first process depends on the sample index $k$. Consequently, this process is not stationary and also not ergodic. Inspection of the linear mean and auto-correlation function estimated by the ensemble averages of the second random process reveals that this processed can be assumed to be WSS. The linear mean as temporal average of the sample functions is not constant with respect to the sample functions. Consequently, this process can be assumed to be WSS but not ergodic. The third process can be assumed to be wide-sense stationary and ergodic.

**Copyright**

This notebook is provided as [Open Educational Resource](https://en.wikipedia.org/wiki/Open_educational_resources). Feel free to use the notebook for your own 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: *Sascha Spors, Digital Signal Processing - Lecture notes featuring computational examples, 2016-2017*.