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

import matplotlib.pyplot as plt
from sympy.plotting.plot import MatplotlibBackend

def logx_plot(Hw, wrange, **kwargs):
    'create plot with logarithmic x-axis'
    p = sym.plot(Hw, wrange, **kwargs)
    backend = MatplotlibBackend(p)
    backend.process_series()
    backend.ax[0].spines['left'].set_position(('axes', 0))
    backend.ax[0].spines['bottom'].set_position(('axes', 0))
    plt.xscale('log')
    plt.grid(which='both')
    return p

def db(x):
    'compute dB value'
    return 20 * sym.log(sym.Abs(x), 10)
```

# Combinación de Modelos Frecuenciales

Es conveniente analizar sistemas elaborados como una representación de sistemas más sencillos. A continuación los principales tipos de combinación.

## Concatenación / Serie

Cuando dos sistemas LTI $H_1$ y $H_2$ están interconectados de manera que la salida de $H_1$ es la entrada de $H_2$. Esta es la conexión en **Serie** o **Concatenación de sistemas**.

Suponga que:
- La respuesta impulsional de $H_1$ es $h_1(t)$.
- La respuesta impulsional de $H_2$ es $h_2(t)$.
- La concatenación se excita con una señal $x(t)$.

La salida de $H_1$ es entonces $x(t)*h_1(t)$. La salida de la concatenación de sistemas es:

\begin{equation}
y(t) =  x(t) * h_1(t) * h_2(t)
\end{equation}

En el dominio de Laplace, la convolución anterior se transforma en:

\begin{equation}
Y(s) = \underbrace{H_1(s) \cdot H_2(s)}_{H(s)} \cdot X(s)
\end{equation}

Así, los dos sistemas concatenados tienen una equivalencia $H(s) = H_1(s) \cdot H_2(s)$.

Si se tienen $N$ sistemas conectados en serie, el modelo equivalente está dado por la multiplicación de las respectivas funciones de transferencia.

\begin{equation}
H(s) = \prod_{n=1}^{N} H_n(s)
\end{equation}

**Ejemplo**

Al concatenar filtros de bajo orden se obtiene un filtro de orden más elevado.

Suponga las siguientes funciones de transferencia para filtros de orden 2.

\begin{equation}
G(s) = \frac{1}{LC s^2 + RC s + 1}
\end{equation}

Si se concatenan $N$ filtros con acoples de impedancia, se obtiene:

\begin{equation}
H_N(s) = \left( \frac{1}{LC s^2 + RC s + 1} \right)^N
\end{equation}

A continuación los diagramas de Bode. Para valores $R$, $L$ y $C$.

```python
s = sym.symbols('s', complex=True)
w = sym.symbols('omega', real=True)
R = 1
L = 0.5
C = 0.4

N = sym.symbols('N', integer=True)

H0 = 1/(L*C*s**2 + R*C*s + 1)
HN = H0**N
HN

```

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

```python
H0w = H0.subs(s, sym.I * w)
HNw = HN.subs(s, sym.I * w)

plt.rcParams['figure.figsize'] = 6, 2

m1 = logx_plot(db(H0w), (w, 0.1, 100),
               xlabel='$\omega$', ylabel='$20 \log_{10} | H(j \omega) |$ in dB',
               line_color='b', show=False);
m2 = logx_plot(db(HNw.subs(N, 2)), (w, 0.1, 100),
               xlabel='$\omega$', ylabel='$20 \log_{10} | H(j \omega) |$ in dB',
               line_color='r', show=False);
m5 = logx_plot(db(HNw.subs(N, 5)), (w, 0.1, 100),
               xlabel='$\omega$', ylabel='$20 \log_{10} | H(j \omega) |$ in dB',
               line_color='g', show=False);
f1 = logx_plot(sym.arg(H0w), (w, 0.1, 100),
               xlabel='$\omega$', ylabel=r'$\varphi(j \omega)$',
               line_color='b', show=False);
f2 = logx_plot(sym.arg(HNw.subs(N, 2)), (w, 0.1, 100),
               xlabel='$\omega$', ylabel=r'$\varphi(j \omega)$',
               line_color='r', show=False);
f5 = logx_plot(sym.arg(HNw.subs(N, 5)), (w, 0.1, 100),
               xlabel='$\omega$', ylabel=r'$\varphi(j \omega)$',
               line_color='g', show=False);

m1.extend(m2)
m1.extend(m5)

f1.extend(f2)
f1.extend(f5)
m1.show()
```

```python
backend = MatplotlibBackend(m1)
backend.process_series()
backend.ax[0].spines['left'].set_position(('axes', 0))
backend.ax[0].spines['bottom'].set_position(('axes', 0))
plt.xscale('log')
plt.grid(which='both')

backend = MatplotlibBackend(f1)
backend.process_series()
backend.ax[0].spines['left'].set_position(('axes', 0))
backend.ax[0].spines['bottom'].set_position(('axes', 0))
plt.xscale('log')
plt.grid(which='both')
```

## Paralelo

Cuando dos sistemas LTI comparten la misma entrada y sus salidas se combinan, están conectados en paralelo.

Cuando dos sistemas LTI $H_1$ y $H_2$ están interconectados de manera que tienen la misma entrada y sus salidas se combinan, esta es la conexión en **Paralelo** .

Suponga que:
- La respuesta impulsional de $H_1$ es $h_1(t)$.
- La respuesta impulsional de $H_2$ es $h_2(t)$.
- La conexión paralela se excita con una señal $x(t)$.

La salida de $H_1$ es entonces $x(t)*h_1(t)$. La salida de $H_2$ es entonces $x(t)*h_2(t)$. La salida de la conexión paralela es:

\begin{equation}
y(t) = h_1(t) * x(t) + h_2(t) * x(t)
\end{equation}

En el dominio de Laplace, la combinación anterior se transforma en:

\begin{equation}
Y(s) = \underbrace{\left( H_1(s) + H_2(s)  \right)}_{H(s)} \cdot X(s)
\end{equation}

Así, los dos sistemas en paralelo tienen una equivalencia $H(s) = H_1(s) + H_2(s)$.

Si se tienen $N$ sistemas conectados en paralelo, el modelo equivalente está dado por la suma de las respectivas funciones de transferencia.

\begin{equation}
H(s) = \sum_{n=1}^{N} H_n(s)
\end{equation}

## Realimentación

En algunos casos, parte de la señal de salida de $H_1$ es devuelta a su entrada a través de otro sistema $H_2$. Esta conexión es llamada **realimentación**.

La salida $y(t)$ está dada por:

\begin{equation}
y(t) = (x(t) + y(t) * h_2(t)) * h_1(t)
\end{equation}

En el dominio de Laplace, la expresión se transforma en:

\begin{equation}
y(t) = (x(t) + y(t) * h_2(t)) * h_1(t)
\end{equation}

\begin{equation}
Y(s) = (X(s) + Y(s) \cdot H_2(s)) \cdot H_1(s)
\end{equation}

\begin{equation}
Y(s) = X(s) \cdot H_1(s) + Y(s) \cdot H_2(s) \cdot H_1(s)
\end{equation}

\begin{equation}
Y(s) -  Y(s) \cdot H_2(s) \cdot H_1(s) = X(s) \cdot H_1(s)
\end{equation}

\begin{equation}
Y(s) \cdot (1 -  H_2(s) \cdot H_1(s)) = X(s) \cdot H_1(s)
\end{equation}

\begin{equation}
Y(s) = \frac{H_1(s)}{1 - H_1(s) \cdot H_2(s)} \cdot X(s)
\end{equation}

La equivalencia es

\begin{equation}
H(s) = \frac{H_1(s)}{1 - H_1(s) \cdot H_2(s)}
\end{equation}

Si la realimentación es negativa, la equivalencia es:

\begin{equation}
H(s) = \frac{H_1(s)}{1 + H_1(s) \cdot H_2(s)}
\end{equation}

## Álgebra de bloques

Cuando hay varios subsistemas interactuando entre sí, se modelan por separado y se identifican las conexiones entre ellos. Para encontrar equivalencias deben reducirse el sistema a una sola expresión con las siguientes normas.

**Ejemplo**

## Ejercicio para entregar el 7 de Junio

Reducir el siguiente diagrama.  Realice el ejercicio en papel y suba fotografías como evidencia.

## Próxima sesión

Transformada Z


#  Moving averages

In many applications of machine learning the input data has *spatial* structure that can, and should, be leveraged when solving associated learning problems.  In this short post we discuss single-dimensional moving averages, which are widely used in machine learning as a pre-processing step for time-series problems.

You can skip around this document to particular subsections via the hyperlinks below.

-  [Denoising time series using a moving average](#moving-average-intro)
-  [Moving averages: mathematical details](#moving-average-details)
-  [Python implementation](#python-implementation)

```python
## This code cell will not be shown in the HTML version of this notebook
# imports from custom library for animations
import sys
sys.path.append('../../')
from demo_libraries import timeseries_library as tlib

# import standard libs
import numpy as np
import pandas as pd
from IPython.display import clear_output

# path to data
datapath = '../../datasets/plain_timeseries/'

# This is needed to compensate for matplotlib notebook's tendancy to blow up images when plotted inline
%matplotlib notebook
from matplotlib import rcParams
rcParams['figure.autolayout'] = True

%load_ext autoreload
%autoreload 2
```

## Denoising time series using a moving average

Suppose we have a *time series* like the one shown below (a snippet of historical stock prices for a major car manufacturer).  When analyzing such time series for trends it is quite common to first *smooth* them.  One way to do this is via a *moving average* - wherein we take a small window and slide it along the time series from its start to finish and average the values inside.  Taking the average inside of each little window tends to cancel out noisy values, resulting in a smoothed version of the original series that is easier to study.  Below we animate the process of building a moving average from left to right.  As the animation progresses and you will see the window in which each average is computed straddled on both sides by vertical blue bars, with the resulting moving average shown as an orange series.

```python
## This code cell will not be shown in the HTML version of this notebook
# load in data
csvname = datapath + 'ford_data.csv'
data = pd.read_csv(csvname)
x = np.array(data['Close'])    # date: 1980 to 2017

# moving average function
def moving_average(x,D):
    y = [v for v in x[:D]]
    for p in range(len(x) - D):
        # make next element
        b = np.sum(x[p:p+D])/float(D)
        y.append(b)
    return np.array(y)

# produce moving average time series
D = 15
y = moving_average(x,D)

# run animator
demo = tlib.time_series_animators.Visualizer()
demo.animate_moving_ave(x,y,D,savepath='videos/animation_1.mp4')
clear_output()
```

```python
## This code cell will not be shown in the HTML version of this notebook
from IPython.display import HTML
HTML("""

""")
```

Below we animate the process above more slowly and only on the first $50$ elements of the input series.

```python
## This code cell will not be shown in the HTML version of this notebook
# run animator
demo = tlib.time_series_animators.Visualizer()
demo.animate_moving_ave(x[:50],y[:50],D,savepath='videos/animation_2.mp4',fps=100)
clear_output()
```

```python
## This code cell will not be shown in the HTML version of this notebook
from IPython.display import HTML
HTML("""

""")
```

In these examples the length of the window - also called the "order" of the moving average and denoted by $D$ - was $D = 50$.  Note as you increase the order how the moving average gets smoother, but mirrors the structure of the true underlying input sequence less and less.  We illustrate this via the animation below.

Also notice how the delay of the moving average - how its values consistently trail those of the original series - increases with the order of the system and is an artifact of using a large history of equally weighted examples of the series as a predictor of its next values.

```python
## This code cell will not be shown in the HTML version of this notebook
# moving average function
def moving_average(x,D):
    y = [v for v in x[:D]]
    for t in range(len(x) - D):
        # make next element
        b = np.sum(x[t:t+D])/float(D)
        y.append(b)
    return np.array(y)

# load in data
csvname = datapath + 'ford_data.csv'
data = pd.read_csv(csvname)
x = np.array(data['Close'])    # date: 1980 to 2017

# run animator
demo = tlib.time_series_animators.Visualizer()
params = np.arange(1,51)
demo.animate_moving_average_range(x,moving_average,params,savepath='videos/animation_3.mp4',fps=1)
clear_output()
```

```python
## This code cell will not be shown in the HTML version of this notebook
from IPython.display import HTML
HTML("""

""")
```

## Moving averages: mathematical details

Time-series data is one of the most common types of one-dimensional data one typically deals with in machine learning.  A single dimensional time-series data set consisting of $P$ naturally ordered points takes the general form

\begin{equation}
\left(t_1,x_1\right),\, \left(t_2,x_2\right),\, \ldots,\, \left(t_P,x_P\right)
\end{equation}

where the inputs $t_1,\ldots,t_P$ are time-marks or time-stamps sorted in ascending order (that is, $t_1<t_2<\cdots<t_P$), with $x_1,\ldots,x_P$ denoting the corresponding output values respectively. Figure 1 shows a prototypical time-series data set where every two successive data points, i.e., $\left(t_p,x_p\right)$ and $\left(t_{p+1},x_{p+1}\right)$, are connected via a line segment. This - typically referred to as *linear interpolation* - is done merely to facilitate visualization of the time-series data by giving it a continuous appearance.

 Figure 1:  A prototypical time-series data with $N=6$ data points.

Time-series data, in many cases, is collected at equally-spaced points in time.  For example, stock exchanges around the world typically publish their data every few minutes (and in some cases every few seconds) whereas the federal spending as a percentage of GDP in the United States, again a time-series data, is published only once every year. Regardless of the temporal resolution and as long as the data is captured at equally-spaced points in time, we can drop the time-marks $t_1,\ldots,t_P$ and represent the data in Equation (1) simply as a one-dimensional sequence of length $P$

\begin{equation}
x_1,\, x_2,\, \ldots,\, x_P
\end{equation}

To construct the first elements in a moving average process for a generic time series we choose a window length or *order* $B$ that we will average over, and set the first $D$ elements of the moving average to the first $D$ elements of the time series as

\begin{equation}
h_p = x_p \,\,\,\,\,\, p=1,...,D.
\end{equation}

This is sometimes referred to as an *initial condition*.  Once we have $D$ elements we can compute the first real average, setting

\begin{equation}
h_{D+1} = \frac{x_1 + x_2 + \cdots + x_{D}}{D}.
\end{equation}

In general when reaching the $p^{th}$ element of the moving average series - where $p$ ranges from $p=D+1,...,P$ - we then compute the moving average value as

\begin{equation}
h_{p} = \frac{x_{p-1} + x_{p-2} + \cdots + x_{p-D}}{D}.
\end{equation}

The moving average is a simple example of a *dynamic system*, that is a function of naturally ordered (e.g., time series) data.

## Python implementation

Moving averages can be implemented in `Python` in a straightforward manner, as shown below.  Here the input `x` (to be smoothed) is loaded in as a contiguous list or array, and `D` is the desired order of the moving average `h` which is returned as a list.

```python
# a simple moving average function
def moving_average(x,D):
    # initialize moving average to input series until D elements are present
    h = [v for v in x[:D]]

    # loop over input series and compute moving averages
    for p in range(len(x) - D):
        # make next element as average of prior D inputs
        h.append(sum(x[p:p+D])/float(D))
    return h
```


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

# Spreading out

Welcome to the second lesson of the course module: "_Spreading out: parabolic PDEs."_ We're studying the heat equation in one spatial dimension:

\begin{equation}
\frac{\partial T}{\partial t} = \alpha \frac{\partial^2 T}{\partial x^2}
\end{equation}

where $\alpha$ is the thermal diffusivity and $T$ is the temperature.

In the previous lesson, we reviewed the numerical solution of the 1D diffusion equation with a forward-time, centered-space scheme: an _explicit_ scheme. What does that mean?

The solution for $T$ at timestep $t^{n+1}$ was calculated using different combinations of $T$ values from the *previous* timestep $t^n$.  We have complete knowledge of the parts that feed into the solution update at each spatial point.

*Implicit* methods work differently: we will use more data from the "future" in the update, including several values of $T$ at $t^{n+1}$. This will make the scheme more difficult to apply, but there are several reasons why it may be worth the effort.

In lesson 1, we discussed two disadvantages of explicit methods: (1) boundary effects drag behind by one time step; (2) stability requirements constrain the time-step to very small values. Both of these issues are resolved by implicit schemes.

## Implicit schemes

Let's move things around a bit and try combining the Euler time step with an evaluation of the spatial derivative on the *updated* solution at $t^{n+1}$. The discretized form of the equation is now as follows:

\begin{equation}\frac{T_{i}^{n+1}-T_{i}^{n}}{\Delta t}=\alpha\frac{T_{i+1}^{n+1}-2T_{i}^{n+1}+T_{i-1}^{n+1}}{\Delta x^2}\end{equation}

The stencil for this discretization doesn't look anything like the other stencils we've used until now. Check it out.

.
#### Figure 1. Stencil of the implicit central scheme.

From the previous time-step, we only know $T_i^{n}$, but what about $T_i^{n+1}$, $T_{i-1}^{n+1}$ and $T_{i+1}^{n+1}$?  What can we do?

No need to panic! Let's start by putting what we *do know* on the right-hand side of the equation and what we *don't know* on the left. We get:

\begin{equation} -T_{i-1}^{n+1} + \left( 2 + \frac{\Delta x^2}{\alpha\Delta t}\right) T_{i}^{n+1} - T_{i+1}^{n+1} = T_{i}^{n}\frac{\Delta x^2}{\alpha\Delta t} \end{equation}

It looks like there are a lot of unknowns and just one equation!

What does it look like with $i=1$?

\begin{equation}-T_{0}^{n+1} + \left( 2 + \frac{\Delta x^2}{\alpha\Delta t}\right) T_{1}^{n+1} - T_{2}^{n+1} = T_{1}^{n}\frac{\Delta x^2}{\alpha\Delta t} \end{equation}

and $i=2$?

\begin{equation}-T_{1}^{n+1} + \left( 2 + \frac{\Delta x^2}{\alpha\Delta t}\right) T_{2}^{n+1} - T_{3}^{n+1} = T_{2}^{n}\frac{\Delta x^2}{\alpha\Delta t} \end{equation}

What about $i=3$?

\begin{equation}-T_{2}^{n+1} + \left( 2 + \frac{\Delta x^2}{\alpha\Delta t}\right) T_{3}^{n+1} - T_{4}^{n+1} = T_{3}^{n}\frac{\Delta x^2}{\alpha\Delta t} \end{equation}

Can you see the common element across equations? Here's a little help:

$T_{i}^{n+1}$ also appears in the equation for $T_{i-1}^{n+1}$ and $T_{i+1}^{n+1}$. We might have enough equations if we apply this for all $i$-values at the same time, don't you think? In fact, this is a linear system of equations for the unknown values $T_{i}^{n+1}$ on the spatial grid.

### What about boundary conditions?

Let's look at the boundary points of the example from the previous lesson with a Dirichlet BC at $x=0$ and a Neumann BC at $x=1$, discretizing with $N$ mesh points.

The value $T_0^{n+1}$ is known at every time-step from the BC, so putting all unknown terms on the left-hand side of the equation and the known values on the right side yields the following for the $i=1$ equation:

\begin{equation}-T_{2}^{n+1} + \left( 2 + \frac{\Delta x^2}{\alpha\Delta t}\right) T_{1}^{n+1} = T_{1}^{n}\frac{\Delta x^2}{\alpha\Delta t} + T_{0}^{n+1} \end{equation}

That was easy!

On the other hand, for $i=N-2$, the equation reads

\begin{equation} -T_{N-3}^{n+1} + \left( 2 + \frac{\Delta x^2}{\alpha\Delta t}\right) T_{N-2}^{n+1} - T_{N-1}^{n+1} = T_{N-2}^{n}\frac{\Delta x^2}{\alpha\Delta t} \end{equation}

The discretized Neumann boundary condition on the right side of the rod is

\begin{equation}\frac{T^{n}_{N-1} - T^{n}_{N-2}}{\Delta x} = q.\end{equation}

But we can just as easily write that at time step $n+1$ (the boundary conditions apply at every time-step):

\begin{equation}\frac{T^{n+1}_{N-1} - T^{n+1}_{N-2}}{\Delta x} = q.\end{equation}

Inserting the Neumann boundary condition in the equation for $i=N-2$ yields

\begin{equation} - T_{N-3}^{n+1} + \left( 1 + \frac{\Delta x^2}{\alpha\Delta t}\right) T_{N-2}^{n+1} = T_{N-2}^{n}\frac{\Delta x^2}{\alpha\Delta t} + \Delta x q \end{equation}

Make sure you work this out with pen and paper: it's important to recognize where these terms come from!

Now we can write the linear system of equations in matrix form as follows:

$$[A][x] = [b]+[b]_{b.c.}$$

where the matrix of coefficients $[A]$ is a sparse matrix—most of the matrix elements are zero—with three non-zero diagonals. We write below the system expanded out, so you can see the structure of the matrix, with $\sigma=\frac{\alpha\Delta t}{\Delta x^2}$:

\begin{align}\left[ \begin{array}{cccccc}
 \left(2 + \frac{1}{\sigma}\right) & -1 & 0 & \cdots & & 0 \\
 -1 & \left(2 + \frac{1}{\sigma}\right) & -1 & 0 & \cdots & 0 \\
 0 & & \ddots& & & \vdots \\
 \vdots & & & & \left(2 + \frac{1}{\sigma}\right)& \\
 0 & \cdots & & & -1 & \left(1 + \frac{1}{\sigma}\right) \end{array} \right]
 \cdot
 \left[ \begin{array}{c}
 T_1^{n+1} \\ T_2^{n+1} \\ \vdots \\ \\ T_{N-2}^{n+1} \end{array} \right]
 =
 \left[ \begin{array}{c}
T_1^n \frac{1}{\sigma} \\ T_2^{n}\frac{1}{\sigma} \\ \vdots \\ \\ T_{N-2}^{n}\frac{1}{\sigma} \end{array} \right]
 +
 \begin{bmatrix}
  T_0^{n+1}\\
  0\\\\
  \vdots\\\\
  0\\
  q\Delta x
 \end{bmatrix}
 \end{align}

Notice that the Dirichlet boundary condition adds only a term to the right-hand side of the system.  The Neumann boundary condition both adds a term to the right-hand side and modifies the matrix $[A]$.

### Problem set up

We'll re-use the problem from lesson 1: we have a graphite rod, with [thermal diffusivity](http://en.wikipedia.org/wiki/Thermal_diffusivity) $\alpha=1.22\times10^{-3} {\rm m}^2/{\rm s}$, length $L=1{\rm m}$, and temperature held at $T=100{\rm C}$ on the left end, $x=0$, and $0{\rm C}$ everywhere else intially. We'll compute the evolution of temperature on the length of the rod.

Let's start like we did in the previous lesson: import your libraries and set up the discretization.

```python
import numpy
from matplotlib import pyplot
%matplotlib inline
from matplotlib import rcParams
rcParams['font.family'] = 'serif'
rcParams['font.size'] = 16
```

```python
L = 1.
nt = 100
nx = 51
alpha = 1.22e-3

q = 0.

dx = L/(nx-1)

qdx = q*dx

Ti = numpy.zeros(nx)
Ti[0] = 100
```

### Solving a linear system

We need to solve the linear system of equations written above to advance the solution in time. Luckily, we can rely on our friends from SciPy who have developed some nice linear solvers, so we don't need to write our own.

From `scipy.linalg`, let's import `solve`: a function to solve linear systems. Make sure to explore the documentation of [`scipy.linalg`](http://docs.scipy.org/doc/scipy-0.14.0/reference/linalg.html). We'll need to define our own custom functions to generate the coefficient matrix and the right-hand side of the linear system. You should carefully study the code below.

```python
from scipy.linalg import solve
```

```python
def generateMatrix(N, sigma):
    """ Computes the matrix for the diffusion equation with backward Euler
        Dirichlet condition at i=0, Neumann at i=-1

    Parameters:
    ----------
    T: array of float
        Temperature at current time step
    sigma: float
        alpha*dt/dx^2

    Returns:
    -------
    A: 2D numpy array of float
        Matrix for diffusion equation
    """

    # Setup the diagonal
    d = numpy.diag(numpy.ones(N-2)*(2+1./sigma))

    # Consider Neumann BC
    d[-1,-1] = 1+1./sigma

    # Setup upper diagonal
    ud = numpy.diag(numpy.ones(N-3)*-1, 1)

    # Setup lower diagonal
    ld = numpy.diag(numpy.ones(N-3)*-1, -1)

    A = d + ud + ld

    return A
```

```python
def generateRHS(T, sigma, qdx):
    """ Computes right-hand side of linear system for diffusion equation
        with backward Euler

    Parameters:
    ----------
    T: array of float
        Temperature at current time step
    sigma: float
        alpha*dt/dx^2
    qdx: float
        flux at right boundary * dx

    Returns:
    -------
    b: array of float
        Right-hand side of diffusion equation with backward Euler
    """

    b = T[1:-1]*1./sigma
    # Consider Dirichlet BC
    b[0] += T[0]
    # Consider Neumann BC
    b[-1] += qdx

    return b
```

Next, we'll define a function that steps in time using the implicit central-space scheme.  Remember that for an implicit method, a step in time is performed by solving the entire linear system. This is a fundamental difference between implicit and explicit methods, and implies a considerable computational cost.

```python
def implicit_ftcs(T, A, nt, sigma, qdx):
    """ Advances diffusion equation in time with implicit central scheme

    Parameters:
    ----------
    T: array of float
        initial temperature profile
    A: 2D array of float
        Matrix with discretized diffusion equation
    nt: int
        number of time steps
    sigma: float
        alpha*td/dx^2

    qdx: float
        flux at right boundary * dx
    Returns:
    -------
    T: array of floats
        temperature profile after nt time steps
    """

    for t in range(nt):
        Tn = T.copy()
        b = generateRHS(Tn, sigma, qdx)
        # Use numpy.linalg.solve
        T_interior = solve(A,b)
        T[1:-1] = T_interior
        # Enforce Neumann BC (Dirichlet is enforced automatically)
        T[-1] = T[-2] + qdx

    return T
```

We solve the linear system for every time step, but the $A$ matrix does not change. Thus, you can generate it only once and then use it as an input parameter to the time-stepping function. Let's try this out!

```python
sigma = 0.5
dt = sigma * dx*dx/alpha
nt = 1000

A = generateMatrix(nx, sigma)
```

It's always a good idea to check your matrix...

```python
print(A)
```

    [[ 4. -1.  0. ...,  0.  0.  0.]
     [-1.  4. -1. ...,  0.  0.  0.]
     [ 0. -1.  4. ...,  0.  0.  0.]
     ...,
     [ 0.  0.  0. ...,  4. -1.  0.]
     [ 0.  0.  0. ..., -1.  4. -1.]
     [ 0.  0.  0. ...,  0. -1.  3.]]

Looks good!

Now, we apply the time iteration with our very own function `implicit_ftcs()`.

```python
T = implicit_ftcs(Ti.copy(), A, nt, sigma, qdx)
```

Now plot the solution!

```python
pyplot.plot(numpy.linspace(0,1,nx), T, color='#003366', ls='-', lw=3);
```

Not too impressive, this looks just like the result from *explicit* forward in time, centered in space for $\alpha\frac{\Delta t}{\Delta x^2} = \frac{1}{2}$.

But try $\alpha\frac{\Delta t}{\Delta x^2} = 5$, which violates the stability condition of the *explicit* scheme:

```python
sigma = 5.0

A = generateMatrix(nx, sigma)

T = implicit_ftcs(T.copy(), A, nt, sigma, qdx)

pyplot.plot(numpy.linspace(0,1,nx), T, color='#003366', ls='-', lw=3);
```

**It didn't blow up!**

We were not able to use such a large time step with the explicit scheme. You can try out other values of `sigma` and you'll get a stable solution. In fact, this is an *unconditionally stable* scheme—the most valuable feature of implicit methods is that they give stable solutions without a constraint on the choice of time step.

Using the implicit scheme, we can always advance in time using larger time steps. But each time step requires the solution of a linear system, which is computationally expensive. This is the trade-off between explicit and implicit methods.
To experiment further, set different values of the Neumann boundary flux and see if the solution behaves as you expect.

##### A word of warning

Implicit methods allow you to use significantly larger time steps, because they are not subject to stability constraints. But that doesn't mean you can use just _any_ large time step! Remember that Euler's method is a first-order method, so the _accuracy_ gets worse as you increase the time step, in direct proportion. In fact, you can lose the ability to capture the correct physics if your time step is too large. Numerical stability does not imply accuracy!

##### Dig deeper

You see how matrix `A` is mostly full of zeros?  We call such a matrix *sparse*, and there are many ways to make more efficient calculations taking advantage of their particular structure. First of all, you can optimize the memory usage. Check out SciPy's [sparse-matrix storage formats](http://docs.scipy.org/doc/scipy-0.14.0/reference/sparse.html): you don't need too store $(N-2)^2$ elements! For example, a `coo_matrix` format stores only $3*N_\text{nonzero}$, where $N_\text{nonzero}$ is the number of non-zero elements in `A`. Make sure to explore this topic a little more. It's an important topic in numerical PDEs.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


```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
np.random.seed(19971107)
ns = (10 ** np.linspace(1, 5, 50)).astype(np.int64)
err = np.zeros(ns.size)
for i, n in enumerate(ns):
    y = binom.rvs(n=n, p=p)
    posterior = beta(a=a+y, b=b+n-y).pdf
    p_star = (a+y-1) / (a+b+n-2)

    neglogposterior = lambda p: -np.log(posterior(p))
    sigma2 = 1 / nd.Derivative(neglogposterior, n=2, step=1e-5)(p_star)

    normal = norm(loc = p_star, scale = np.sqrt(sigma2)).pdf
    Z_hat = posterior(p_star) / normal(p_star)
    err[i] = np.abs(Z_hat - 1)

fit = linreg().fit(np.log(ns).reshape([-1, 1]), np.log(err))
c, r = np.exp(fit.intercept_), -fit.coef_[0]
print('c = {:.4f}, r = {:.4f}'.format(c, r))

fig, ax = plt.subplots(figsize = (9, 5))
ax.loglog(ns, err)
ax.loglog(ns, np.exp(fit.predict(np.log(ns).reshape([-1, 1]))))
ax.set_xlabel('$\log(n)$')
ax.set_ylabel('$\log(|\hat{Z}-1|)$')
plt.show()
```

**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
np.random.seed(19971107)
samples = uniform.rvs(size=10000000)
hx = h(samples)

ns = np.arange(2, hx.size+1)
cummean_x = np.cumsum(hx)[1:] / ns
cummean_x2 = np.cumsum(hx**2)[1:] / ns
cumstd = np.sqrt((cummean_x2 - cummean_x**2) / (ns - 1))

upper = cummean_x + 1.96 * cumstd
lower = cummean_x - 1.96 * cumstd

fig, axs = plt.subplots(1, 2, figsize = (15, 5))

axs[0].plot([ns[0], ns[3000]], [mu, mu], label='true $\mu$')
axs[0].plot(ns[:3000], cummean_x[:3000], label='$\hat{mu}$')
axs[0].plot(ns[:3000], upper[:3000], label='C.I. upper bound')
axs[0].plot(ns[:3000], lower[:3000], label='C.I. lower bound')
axs[0].legend()

err = np.abs(cummean_x - mu)
fit = linreg().fit(np.log(ns).reshape([-1, 1]), np.log(err), sample_weight=1/ns)
c, r = np.exp(fit.intercept_), -fit.coef_[0]
print('c = {:.4f}, r = {:.4f}'.format(c, r))

axs[1].loglog(ns, err)
axs[1].loglog(ns, np.exp(fit.predict(np.log(ns).reshape([-1, 1]))))
axs[1].set_xlabel('$\log(n)$')
axs[1].set_ylabel('$\log(|\hat{\mu}-\mu|)$')

plt.show()
```

**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
np.random.seed(19971107)
unifs = uniform.rvs(size=3000)
samples = []
for unif in unifs:
    fun = lambda x: norm.cdf(x) - unif
    samples.append(brentq(fun, -100, 100))

xx = np.linspace(-4, 4, 1000)
fig, ax = plt.subplots(figsize = (9, 5))
ax.plot(xx, norm.pdf(xx), label='$N(0, 1)$')
ax.hist(samples, bins=50, density=True, histtype='step', label='samples')
ax.legend()
plt.show()
```

### 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
np.random.seed(19971107)
weight = lambda x: norm.pdf(x) / t(df=1).pdf(x)
C = weight(minimize(lambda x: -np.log(weight(x)), x0=10, tol=1e-10).x)
n = 3000

start1 = dt.now()
samples1 = []
while len(samples1) < n:
    new = t(df=1).rvs()
    if uniform.rvs() <= weight(new) / C:
        samples1.append(new)

end1 = dt.now()
time1 = (end1 - start1).seconds

start2 = dt.now()
m = np.ceil(C * n).astype(np.int64)
samples2 = t(df=1).rvs(size=m)
samples2 = samples2[uniform.rvs(size=m) <= weight(samples2) / C]
while samples2.size < n:
    new = t(df=1).rvs()
    if uniform.rvs() <= weight(new) / C:
        samples2 = np.append(samples2, new)

samples2 = samples2[:n]
end2 = dt.now()
time2 = (end2 - start2).seconds

xx = np.linspace(-4, 4, 1000)
fig, ax = plt.subplots(figsize = (9, 5))
ax.plot(xx, norm.pdf(xx), label='$N(0, 1)$')
ax.hist(samples1, bins=50, density=True, histtype='step', label='samples 1')
ax.hist(samples2, bins=50, density=True, histtype='step', label='samples 2')
ax.legend()
plt.show()

print('time 1: {}s; time 2: {}s'.format(time1, time2))
```

**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
np.random.seed(19971107)
ns = (10 ** np.linspace(1, 5, 50)).astype(np.int64)
err = np.zeros(ns.size)
samples0 = norm.rvs(size=100)
for i, n in enumerate(ns):
    y = binom.rvs(n=n, p=p)
    posterior = beta(a=a+y, b=b+n-y).pdf
    p_star = (a+y-1) / (a+b+n-2)

    neglogposterior = lambda p: -np.log(posterior(p))
    sigma2 = 1 / nd.Derivative(neglogposterior, n=2, step=1e-5)(p_star)

    proposal = norm(loc = p_star, scale = np.sqrt(sigma2)).pdf
    samples = np.sqrt(sigma2) * samples0 + p_star
    Z_hat = np.mean(posterior(samples) / proposal(samples))
    err[i] = np.abs(Z_hat - 1)

fit = linreg().fit(np.log(ns).reshape([-1, 1]), np.log(err))
c, r = np.exp(fit.intercept_), -fit.coef_[0]
print('c = {:.4f}, r = {:.4f}'.format(c, r))

fig, ax = plt.subplots(figsize = (9, 5))
ax.loglog(ns, err)
ax.loglog(ns, np.exp(fit.predict(np.log(ns).reshape([-1, 1]))))
ax.set_xlabel('$\log(n)$')
ax.set_ylabel('$\log(|\hat{Z}-1|)$')
plt.show()
```

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 (with answers).ipynb


# Problem Set 1
**Due: April 26, 2021** (in class; subject to change if COVID restrictions apply)

## Problem 1

Suppose you have n i.i.d. observations from the nonlinear regression
$$y_i=(x_i+\beta)^2 + \epsilon_i \quad \epsilon_i \vert x_i \sim \mathcal{N}(0,1)$$
1. Find the asymptotic distribution of the nonlinear least squares estimator of $\beta$.
2. Find the asymptotic distribution of the maximum likelihood estimator of $\beta$. Compare the two asymptotic distributions.

**Hint**: For NLS $m(x_i;\beta)=-[y_i-(x_i+\beta)^2]^2$. For MLE $m(x_i;\beta)=\log f(y\vert x;\beta)=\log(1)-\log(\sqrt{2\pi})-\frac{1}{2}[y-(x+\beta)^2]^2$

## Problem 2
**Hint**:
- Read Hayashi Chapter 7.3, Example 7,10
- Read Hayashi Chapter 1

Consider the linear regression model with normal errors, whose conditional density for observation $i$ is
$$\log f(y_i \vert \bf{x}_i ; \beta, \sigma^2) = - \frac{1}{2}\log (2\pi) - \frac{1}{2}\log (\sigma^2) - \frac{\big(y_i - \bf{x}_i'\beta\big)^2}{2\sigma^2}$$

Let $\big(\hat{\beta},\hat{\sigma}^2\big)$ be the unrestricted ML estimate of $\theta = \big(\beta,\sigma^2\big)$ and let $\big(\tilde{\beta},\tilde{\sigma}^2\big)$ be the restricted ML estimate subject to the constraint $\bf{R}\beta = c$ where $\bf{R}$ is an $r \times X$ matrix of known constraints. Assume that $\Theta = \mathbb{R}^K \times \mathbb{R}_++$ and that $\mathbb{E}[x_ix_i']$ is nonsingular. Also, let
$$\hat{\Sigma} = \begin{bmatrix}
\frac{1}{\hat{\sigma^2}}\frac{1}{n}\sum_{1}^{n}x_ix_i'&0\\
0&\frac{1}{2(\hat{\sigma}^2)^2}\\
\end{bmatrix}$$
$$\quad$$
$$\tilde{\Sigma} = \begin{bmatrix}
\frac{1}{\tilde{\sigma^2}}\frac{1}{n}\sum_{1}^{n}x_ix_i'&0\\
0&\frac{1}{2(\tilde{\sigma}^2)^2}\\
\end{bmatrix}$$
- (a) Verify that $\hat{\beta}$ minimizes the sum of squared residuals. So it is the OLS estimator. Verify that $\tilde{\beta}$ minimizes the sum of squared residuals subject to the constraint $\bf{R}\beta = c$. so it is the restricted least squares estimator. **Hint**: to set up a problem, please refer to Restrictions section from EE.pdf notes. You can replace the term $\sum (y_i - \bf{x}'_i \beta)^2$ with the sum of squaredresidual: i.e.,  $SSR(\beta) \equiv \sum (y_i - \bf{x}'_i \beta)^2$.

- (b) Let $Q_n(\theta) = \log f(y_i \vert \bf{x}_i ; \beta, \sigma^2)$. Show that
$$Q_n(\hat{\theta}) = -\frac{1}{2}\log (2\pi) -\frac{1}{2}-\frac{1}{2}\log \big(\frac{SSR_U}{n}\big)$$
$$Q_n(\tilde{\theta}) = -\frac{1}{2}\log (2\pi) -\frac{1}{2}-\frac{1}{2}\log \big(\frac{SSR_R}{n}\big)$$
where
$SSR_U \equiv \sum (y_i - \bf{x}'_i \hat{\beta})^2$ is the unrestricted sum of squared residuals and
$SSR_R \equiv \sum (y_i - \bf{x}'_i \tilde{\beta})^2$ is the restricted sum of squared residuals.

- (c) Verify that the $\hat{\Sigma}$ given here, although not the same as $-\frac{1}{n}\sum_{1}^{n}\bf{H}(\bf{w}_i;\hat{\theta})$, is consistent for $\mathbb{E}\big[\bf{w}_i;\theta_0\big]$. Verify that the $\tilde{\Sigma}$ given here, although not the same as $-\frac{1}{n}\sum_{1}^{n}\bf{H}(\bf{w}_i;\tilde{\theta})$, is consistent for $\mathbb{E}\big[\bf{w}_i;\theta_0\big]$.

- (d) Show that the Wald, LM, and LR statistics using $\hat{\Sigma}$ and $\tilde{\Sigma}$ given here can be written as
$$\begin{align}
W & = n \cdot \frac{(\bf{R}\hat{\beta}-c)'[\bf{R(X'X)^{-1}R'}]^{-1}(\bf{R}\hat{\beta}-c)}{SSR_U}\\
LM & =n \cdot \frac{(y-\bf{X}\tilde{\beta})'P(y-\bf{X}\tilde{\beta})}{SSR_R} \\
LR & =n \cdot \bigg[ \log \bigg( \frac{SSR_R}{n} \bigg)- \log \bigg(\frac{SSR_U}{n}\bigg) \bigg]
\end{align}$$
where $y$ is a vector with dimension $(n \times 1)$ and $\bf{X}$ is a matrix with dimension $(n \times K)$.

- (e) Show that the three statistics can also be written as
$$\begin{align}
W & = n \cdot \frac{SSR_R - SSR_U}{SSR_U}\\
LM & =n \cdot \frac{SSR_R - SSR_U}{SSR_R} \\
LR & =n \cdot \log \bigg( \frac{SSR_R}{SSR_U} \bigg)
\end{align}$$

**Hint**: Refer to Analytical Exercise 1,  Chapter 1 of Hayashi.
$$\begin{align}
SSR_R - SSR_U
& = (\hat{\beta}-\tilde{\beta})'(X'X)(\hat{\beta}-\tilde{\beta})\\
& = (R\hat{\beta}-c)'[R(X'X)^{-1}R'](R\hat{\beta}-c)\\
& = (y-X\tilde{\beta})'P()(y-X\tilde{\beta})
\end{align}$$
where $P = X(X'X)^{-1}X'$

- (f) Show that $W \geq LR \geq LM$. These inequality do not always hold in nonlinear regression models.


# Hypersolvers for Optimal Control - Pre-training

### Basic usage of the `Hypersolver` API

We showcase the `torchdyn` API for [hypersolvers](https://arxiv.org/pdf/2007.09601.pdf). Hypersolvers, or hybrid ODE solvers equipped with a neural network to approximate residuals, are seamlessly integrated into the `odeint` API. A major design of the API is exactly preserving a persistent state for the solver, which in the case of standard ODE solvers contains the Tableau. For hypersolvers, the state also contains their hypernetwork parameters.

```python
import sys; sys.path.append(2*'../') # go n dirs back
from src import *
```

```python
import time
import torch
import torch.nn as nn
import matplotlib.pyplot as plt

from torchdyn.core import NeuralODE
from torchdyn.datasets import *
from torchdyn.numerics import odeint, Euler, HyperEuler
```

## Optimal Control

We want to control an inverted pendulum and stabilize it in the upright position. The equations in Hamiltonian form describing an inverted pendulum with a torsional spring are as following:

\begin{equation}
    \begin{bmatrix} \dot{q}\\ \dot{p}\\ \end{bmatrix} =
    \begin{bmatrix}
    0& 1/m \\
    -k& -\beta/m\\
    \end{bmatrix}
    \begin{bmatrix} q\\ p\\ \end{bmatrix} -
    \begin{bmatrix}
    0\\
    mgl \sin{q}\\
    \end{bmatrix}+
    \begin{bmatrix}
    0\\
    1\\
    \end{bmatrix} u
\end{equation}

```python
class ControlledPendulum(nn.Module):
    """
    Inverted pendulum with torsional spring
    """
    def __init__(self, u, m=1., k=.5, l=1., qr=0., β=.01, g=9.81):
        super().__init__()
        self.u = u # controller (nn.Module)
        self.nfe = 0 # number of function evaluations
        self.cur_f = None # current function evaluation
        self.cur_u = None # current controller evaluation
        self.m, self.k, self.l, self.qr, self.β, self.g = m, k, l, qr, β, g # physics

    def forward(self, t, x):
        self.nfe += 1
        q, p = x[..., :1], x[..., 1:]
        self.cur_u = self.u(t, x)
        dq = p/self.m
        dp = -self.k*(q - self.qr) - self.m*self.g*self.l*torch.sin(q) \
            -self.β*p/self.m + self.cur_u
        self.cur_f = torch.cat([dq, dp], -1)
        return self.cur_f
```

In order to control the pendulum, we have to define a proper _integral cost function_ which will be our loss to be minimized during training. In a general form, it can be defined as:

\begin{equation}
        J = x^\top(t_f)\mathbf{P} x(t_f) + \int_{t_0}^{t_f} \left[ x^\top(t) \mathbf{Q} x(t) + u^\top(t) \mathbf{R} u(t) \right] dt
\end{equation}

where $ x = \begin{bmatrix} q\\ p\\ \end{bmatrix}$ is the state and $\mathbf{u}$ is the controller and matrices $\mathbf{P},~\mathbf{Q}, ~ \mathbf{R}$ are weights for controlling the performance.

```python
class IntegralCost(nn.Module):
    '''Integral cost function
    Args:
        x_star: torch.tensor, target position
        P: float, terminal cost weights
        Q: float, state weights
        R: float, controller regulator weights
    '''
    def __init__(self, x_star, P=0, Q=1, R=0):
        super().__init__()
        self.x_star = x_star
        self.P, self.Q, self.R, = P, Q, R

    def forward(self, x, u=torch.Tensor([0.])):
        """
        x: trajectory
        u: control input
        """
        cost = self.P*torch.norm(x[-1] - self.x_star, p=2, dim=-1).mean()
        cost += self.Q*torch.norm(x - self.x_star, p=2, dim=-1).mean()
        cost += self.R*torch.norm(u - 0, p=2).mean()
        return cost
```

```python
# Change device according to your configuration
# device = torch.device('cuda:1') if torch.cuda.is_available() else torch.device('cpu')
device = torch.device('cpu')
```

```python
# The controller is a simple MLP with one hidden layer with bounded output
class NeuralController(nn.Module):
    def __init__(self, model, u_min=-20, u_max=20):
        super().__init__()
        self.model = model
        self.u_min, self.u_max = u_min, u_max

    def forward(self, t, x):
        x = self.model(x)
        return torch.clamp(x, self.u_min, self.u_max)

model = nn.Sequential(nn.Linear(2, 32), nn.Tanh(), nn.Linear(32, 1)).to(device)
u = NeuralController(model)
for p in u.model[-1].parameters(): torch.nn.init.zeros_(p)

# Controlled system
sys = ControlledPendulum(u).to(device)
```

```python
from math import pi as π

# Loss function declaration
x_star = torch.Tensor([0., 0.]).to(device)
cost_func = IntegralCost(x_star)

# Time span
dt = 0.2
t0, tf = 0, 3 # initial and final time for controlling the system
steps = int((tf - t0)/dt) + 1 # so we have a time step of 0.2s
t_span = torch.linspace(t0, tf, steps).to(device)

# Initial distribution
x0 = π # limit of the state distribution (in rads and rads/second)
init_dist = torch.distributions.Uniform(torch.Tensor([-x0, -x0]), torch.Tensor([x0, x0]))
```

Now we have to address one question: can we speed up the simulation?

## Hypersolvers for Controlled Systems

By training the hypersolver on residuals between the ground-truth solution and the base solver (we will use Euler in this case) we can render the training more efficient while retaining solution accuracy.

The residuals can be defined by:
$$\begin{equation}
\begin{aligned}
    R \left(t_k,  x(t_k), x(t_{k+1}) \right) = \Phi(x(t_k), t_k, t_{k+1}) - x(t_k) - \epsilon f(x_k, u_k)
\end{aligned}
\end{equation}$$

where $\Phi$ is an accurate solver and $\epsilon$ is the time stepping.

The hypernet $g_w$ should have as input data important features for predicting future trajectories: we can use the current state $x$, its derivative $f$ and the control input $u$. The training will be carried out by minimizing a loss function like:

$$\begin{equation}
    \ell = \frac{1}{K} \sum_{k=0}^{K-1} \left|\left|{ R \left( t_k,  x(t_k), x(t_{k+1}) \right) - g_\omega\left(x(t_k), f(t_k), u(t_{k}) \right) }\right|\right|_2
\end{equation}$$

```python
# We consider the controller fixed during each solver step
class RandConstController(nn.Module):
    def __init__(self):
        super().__init__()
        self.u0 = torch.Tensor(1024, 1).uniform_(-10,10).to(device)

    def forward(self, t, x):
        return self.u0

# Save previously learned controller
u_no_hypersolver = sys.u
sys.u = RandConstController() # modify controller for training
```

```python
class VanillaHyperNet(nn.Module):
    """Simple hypernetwork for controlled systems
    Input: current x, f and u from the controlled system
    Output: p-th order residuals"""
    def __init__(self, net):
        super().__init__()
        self.net = net

    def forward(self, t, x):
        xfu = torch.cat([x, sys.cur_f, sys.cur_u], -1)
        return self.net(xfu)

net = nn.Sequential(nn.Linear(5, 32), nn.Softplus(), nn.Linear(32, 32), nn.Tanh(), nn.Linear(32, 2))
hypersolver = HyperEuler(VanillaHyperNet(net))
# model = nn.DataParallel(hypersolver, device_ids=[1]) # feel free to change here according to your setup and GPU available.
# model = model.to(device)
```

## Training the Hypernetwork

Training the hypernetwork $g_w$ requires us to know which set its input values are contained in.
If we want the pendulum with initial states in $\pm \pi$, we can sample the input states for the hypernetwork from a distribution which includes possible states which the system can visit (i.e., the hypernetwork should be able to predict outside of the initial boundaries in a certain range). For example, we can draw the states $x$ from a Uniform distribution:
$$x = \begin{bmatrix} q\\ p\\ \end{bmatrix} \sim Uniform(-2\pi, 2\pi) \times Uniform(-2\pi, 2\pi)$$
Similarly, the controller values can be sampled from a distribution including all the possible controller values, such as
$$u \sim Uniform(-20, 20)$$

```python
# Initial distribution
x0 = 2*π # limit of the state distribution (in rads and rads/second)
init_dist = torch.distributions.Uniform(torch.Tensor([-x0, -x0]), torch.Tensor([x0, x0]))

base_solver = Euler()
# Time span
t0, tf = 0, 3 # initial and final time for controlling the system
steps = int((tf - t0)/dt) + 1 # so we have a time step of 0.2s
t_span = torch.linspace(t0, tf, steps).to(device)
dt = (t_span[1] - t_span[0]).detach().cpu().item()
```

```python
opt = torch.optim.Adam(hypersolver.parameters(), lr=3e-4)
loss_func = nn.MSELoss()
epochs = 300000
bs = 2048
# hypernet = model.module.hyperne
hypernet = hypersolver.hypernet
span = torch.linspace(0, dt, 2)
losses = []

for i in range(epochs):
    # Sample random intial states and controllers
    x0 = init_dist.sample((bs,)).to(device)
    sys.u.u0 = torch.Tensor(bs, 1).uniform_(-5, 5).to(device)

     # Compute residuals
    _, sol_gt = odeint(sys, x0, span, solver='tsit5', atol=1e-5, rtol=1e-5)[-1]
    sol = x0 + sys(0., x0)*dt
    residuals = (sol_gt - sol) / dt**2
    residuals_hypersolver = hypernet(0., x0)
    loss = loss_func(residuals, residuals_hypersolver)

    # Optimization step
    loss.backward(); opt.step(); opt.zero_grad()
    print(f'Step: {i}, Residual loss: {loss:.3f}', end='\r')
    losses.append(loss.detach().cpu().item())
```

    Step: 299999, Residual loss: 0.001

```python
plt.plot(losses)
plt.yscale('log')
```

```python
torch.save(model, 'saved_models/hs_torchdyn.pt')
```

```python
# Sample and plot some trajectories
x0 = π # limit of the state distribution (in rads and rads/second)
init_dist = torch.distributions.Uniform(torch.Tensor([-x0, -x0]), torch.Tensor([x0, x0]))
x0 = init_dist.sample((10000,)).to(device)
sys = ControlledPendulum(RandConstController()).to(device)
sys.u.u0 = torch.Tensor(10000, 1).uniform_(-3,3).to(device)
t_span = torch.linspace(0, 40, 401).to(device)

_, traj_euler = odeint(sys, x0, t_span, solver='euler')
_, traj_hyper = odeint(sys, x0, t_span, solver=hypersolver)
_, traj_mp = odeint(sys, x0, t_span, solver='midpoint')
_, traj_rk4 = odeint(sys, x0, t_span, solver='rk4')
_, traj_gt = odeint(sys, x0, t_span, solver='tsit5', atol=1e-5, rtol=1e-5)
traj_euler = traj_euler.detach().cpu(); traj_hyper = traj_hyper.detach().cpu(); traj_gt = traj_gt.detach().cpu()
traj_mp = traj_mp.detach().cpu(); traj_rk4 = traj_rk4.detach().cpu()
t_span = t_span.cpu()

fig, axs = plt.subplots(2, 5, figsize=(20,6))

for i in range(5):
    for j in range(2):
        axs[j, i].plot(t_span, traj_euler[:,i,j], 'r')
        axs[j, i].plot(t_span, traj_hyper[:,i,j], 'orange')
        axs[j, i].plot(t_span, traj_mp[:,i,j], 'green')
        axs[j, i].plot(t_span, traj_rk4[:,i,j], 'purple')
        axs[j, i].plot(t_span, traj_gt[:,i,j], 'b-.')
        axs[j, i].legend(['Euler', 'HyperEuler', 'Midpoint', 'RK4', 'Ground Truth'])
        axs[j, i].label_outer()
    axs[0, i].set_ylabel(r'Positions $q$'); axs[1, i].set_ylabel(r'Momenta $p$')

fig.suptitle('Uncontrolled Trajectories')
```

```python
# Error analysis
def smape(yhat, y):
    return torch.abs(yhat - y) / (torch.abs(yhat) + torch.abs(y)) / 2

err_euler = smape(traj_gt, traj_euler).detach().cpu();  err_hyper = smape(traj_gt, traj_hyper).detach().cpu()
err_mp, err_rk4 = smape(traj_gt, traj_mp).detach().cpu(), smape(traj_gt, traj_rk4).detach().cpu()
mean_euler, std_euler = err_euler.mean(1), err_euler.std(1)
mean_hyper, std_hyper = err_hyper.mean(1), err_hyper.std(1)
mean_mp, std_mp = err_mp.mean(1), err_mp.std(1)
mean_rk4, std_rk4 = err_rk4.mean(1), err_rk4.std(1)

fig = plt.figure(figsize=(16,5))
ax = fig.add_subplot(1,2,1)
ax.plot(t_span, mean_euler[:,0], 'r', label='Euler')
ax.plot(t_span, mean_hyper[:,0], 'orange', label='HyperEuler')
ax.plot(t_span, mean_mp[:,0], 'green', label='Midpoint')
ax.plot(t_span, mean_rk4[:,0], 'purple', label='RK4')

ax.fill_between(t_span, mean_euler[:,0]-std_euler[:,0], mean_euler[:,0]+std_euler[:,0], alpha=.2, color='r')
ax.fill_between(t_span, mean_hyper[:,0]-std_hyper[:,0], mean_hyper[:,0]+std_hyper[:,0], alpha=.2, color='orange')
ax.fill_between(t_span, mean_mp[:,0]-std_mp[:,0], mean_mp[:,0]+std_mp[:,0], alpha=.2, color='green')
ax.fill_between(t_span, mean_rk4[:,0]-std_rk4[:,0], mean_rk4[:,0]+std_rk4[:,0], alpha=.2, color='purple')

ax.legend()
ax.set_title('Positions')

ax = fig.add_subplot(1,2,2)
ax.plot(t_span, mean_euler[:,1], 'r', label='Euler')
ax.plot(t_span, mean_hyper[:,1], 'orange', label='HyperEuler')
ax.plot(t_span, mean_mp[:,1], 'green', label='Midpoint')
ax.plot(t_span, mean_rk4[:,1], 'purple', label='RK4')

ax.fill_between(t_span, mean_euler[:,1]-std_euler[:,1], mean_euler[:,1]+std_euler[:,1], alpha=.2, color='r')
ax.fill_between(t_span, mean_hyper[:,1]-std_hyper[:,1], mean_hyper[:,1]+std_hyper[:,1], alpha=.2, color='orange')
ax.fill_between(t_span, mean_mp[:,1]-std_mp[:,1], mean_mp[:,1]+std_mp[:,1], alpha=.2, color='green')
ax.fill_between(t_span, mean_rk4[:,1]-std_rk4[:,1], mean_rk4[:,1]+std_rk4[:,1], alpha=.2, color='purple')
ax.legend()
ax.set_title('Momenta')
fig.suptitle('SMAPE Propagation')
```

Nice! We managed obtain a more accurate solver with respect to the Euler baseline. Now, let's see how this performs with optimal control.


# *Circuitos Elétricos I - Semana 10*

### Problema 1

(Problema 7.19 - Nilsson) Para o circuito abaixo, pede-se:

a) Determine a tensão $v_0(t)$ sobre o indutor de $48\;mH$ para $t\geq0$.\
b) Determine a corrente $i_0(t)$ sobre o indutor de $48\;mH$ para $t\geq0$.\
c) Determine a energia consumida pelo resistor de $2.5\;k\Omega$ no intervalo $0\leq t \leq\infty$.

Link para a simulação do circuito: https://tinyurl.com/yj69udn8

```python
# valores das indutâncias
L1 = 20e-3
L2 = 80e-3
L3 = 48e-3

# valores iniciais das correntes
i1_0 = 5e-3
i2_0 = 5e-3
i3_0 = 0
```

```python
# indutância equivalente
Leq1 = (L2*L3)/(L2+L3)
Leq  = L1 + Leq1

print('Leq = ', Leq/1e-3, ' mH')
```

    Leq =  50.0  mH

```python
R = 2.5e3

# constante de tempo
τ = Leq/R

print('τ = ', τ, ' s')
```

    τ =  2e-05  s

```python
import sympy as sp

iL_inf = 0
iL_0   = i1_0

# define as variável tempo
t = sp.symbols('t')

# define i(t)
iL = iL_inf + (iL_0 - iL_inf)*sp.exp(-t/τ)

print('Corrente no indutor equivalente:')
print('iL(t) = ', iL/1e-3 , ' mA')
```

    Corrente no indutor equivalente:
    iL(t) =  5.0*exp(-50000.0*t)  mA

```python
# calcula v0
v0 = Leq1*sp.diff(iL,t)

print('v0(t) = ', v0 , ' V')
```

    v0(t) =  -7.5*exp(-50000.0*t)  V

```python
# correntes nos indutores em função da tensão aplicada aos terminais
i1 = iL
i2 = (1/L2)*sp.integrate(v0, (t, 0, t)) + i2_0
i3 = (1/L3)*sp.integrate(v0, (t, 0, t)) + i3_0

print('Correntes nos indutores:')
print('i1(t) = ', i1/1e-3 , ' mA')
print('i2(t) = ', i2/1e-3 , ' mA')
print('i3(t) = ', i3/1e-3 , ' mA')
```

    Correntes nos indutores:
    i1(t) =  5.0*exp(-50000.0*t)  mA
    i2(t) =  3.125 + 1.875*exp(-50000.0*t)  mA
    i3(t) =  -3.125 + 3.125*exp(-50000.0*t)  mA

```python
# calculando os valores de energia em t=0
E1_0 = (1/2)*L1*(i1.evalf(subs={t:0}))**2
E2_0 = (1/2)*L2*(i2.evalf(subs={t:0}))**2
E3_0 = (1/2)*L3*(i3.evalf(subs={t:0}))**2

print('Energia inicial armazenada nos indutores:')
print('E1(0) = %.2f μJ' %(E1_0/1e-6))
print('E2(0) = %.2f μJ' %(E2_0/1e-6))
print('E3(0) = %.2f μJ' %(E3_0/1e-6))
```

    Energia inicial armazenada nos indutores:
    E1(0) = 0.25 μJ
    E2(0) = 1.00 μJ
    E3(0) = 0.00 μJ

```python
# calculando os valores de energia em t =oo
E1_inf = (1/2)*L1*(i1.evalf(subs={t:100}))**2
E2_inf = (1/2)*L2*(i2.evalf(subs={t:100}))**2
E3_inf = (1/2)*L3*(i3.evalf(subs={t:100}))**2

print('Energia final armazenada nos indutores:')
print('E1(oo) = %.2f μJ' %(E1_inf/1e-6))
print('E2(oo) = %.2f μJ' %(E2_inf/1e-6))
print('E3(oo) = %.2f μJ' %(E3_inf/1e-6))
```

    Energia final armazenada nos indutores:
    E1(oo) = 0.00 μJ
    E2(oo) = 0.39 μJ
    E3(oo) = 0.23 μJ

```python
# calculando a variação de energia nos indutores

ΔE = (E1_inf-E1_0) + (E2_inf-E2_0) + (E3_inf-E3_0)

print('Variação da energia armazenada nos indutores:')
print('ΔE = %.2f μJ' %(ΔE/1e-6))
```

    Variação da energia armazenada nos indutores:
    ΔE = -0.63 μJ

```python
# define tensão sobre o resistor vR(t)
vR = R*i1

# potência consumida pelo resistor
p = vR*i1

# energia consumida pelo resistor
E = sp.integrate(p, (t, 0, sp.oo))
print('Energia consumida pelo resistor:')
print('E = %.2f μJ' %(E/1e-6))
```

    Energia consumida pelo resistor:
    E = 0.63 μJ

Jupyter notebooks/Circuitos Eletricos I - Semana 10.ipynb


Traçar um esboço do gráfico e obter uma equação da parábola que satisfaça as condições dadas.

15. Focos: $F(0,-\frac{1}{4})$; diretriz $d: 4y -1 = 0$

Arrumando a equação da diretriz

$d: y = \frac{1}{4}$

Se $p$ é a distância entre o foco e a diretriz, temos que  $p = -\frac{1}{2}$

$F = \frac{p}{2}$

$-\frac{1}{4} = \frac{p}{2}$

$-\frac{1}{2} = p$

Montando a equação

$x^2 = 2py$

$x^2 = 2 \cdot (-\frac{1}{2}) \cdot y$

$x^2 = -y$

Gráfico da função

```python
from sympy import *
from sympy.plotting import plot_implicit
x, y = symbols("x y")
plot_implicit(Eq((x-0)**2, -1*(y+0)), (x,-10,10), (y,-10,10),
title=u'Gráfico da parábola', xlabel='x', ylabel='y');
```

Problemas Propostos. Pag. 172 - 175/15.ipynb


# Euler Equation with CentPy in 1D

### Import packages

```
# Install the centpy package
!pip install centpy
```

    Requirement already satisfied: centpy in /usr/local/lib/python3.6/dist-packages (0.1)
    Requirement already satisfied: numpy in /usr/local/lib/python3.6/dist-packages (from centpy) (1.18.5)

```
# Import numpy and centpy for the solution
import numpy as np
import centpy
```

```
# Imports functions from matplotlib and setup for the animation
import matplotlib.pyplot as plt
from matplotlib import animation
from IPython.display import HTML
```

### Equation

We solve the Euler equations in 1D

\begin{equation}
\partial_t
\begin{bmatrix} \rho \\ \rho v \\ E \end{bmatrix}
+
\partial_x
\begin{bmatrix} \rho v \\ \rho v^2 +p \\ v (E+p) \end{bmatrix}
= 0
\end{equation}

with the equation of state

\begin{equation}
p = (\gamma-1) \left(E-\frac{1}{2} \rho v^2\right), \qquad \gamma=1.4
\end{equation}

on the domain $(x,t)\in([0,1]\times[0,0.1])$ with initial data for a *Sod shock tube*:

\begin{equation}
(\rho, v, p)_{t=0} =
\begin{cases}
(1,0,1) & \text{if} & 0<x\leq0.5 \\
(0.125, 0, 0.1) & \text{if} & 0.5<x<1
\end{cases}
\end{equation}

and Dirichlet boundary data set by initial data on each boundary. The solution is computed using 400 cells and CFL number 0.75.

```
pars = centpy.Pars1d(x_init=0.0, x_final=1.0, t_final=0.3, dt_out=0.005, J=400, cfl=0.75, scheme="sd2")
pars.gamma = 1.4
```

```
# Euler equation
class Euler1d(centpy.Equation1d):
    def initial_data(self):
        u = np.zeros((self.J + 4, 3))
        midpoint = int(self.J / 2) + 2

        left_v = [1, 0, 1.0 / (self.gamma - 1.0)]
        right_v = [0.125, 0.0, 0.1 / (self.gamma - 1.0)]

        # Left side
        u[:midpoint, :] = left_v
        # Right side
        u[midpoint:, :] = right_v

        return u

    def boundary_conditions(self, u):
        left_v = [1, 0, 1.0 / (self.gamma - 1.0)]
        right_v = [0.125, 0.0, 0.1 / (self.gamma - 1.0)]
        # Left side
        u[0] = left_v
        u[1] = left_v
        # Right side
        u[-1] = right_v
        u[-2] = right_v

    def flux_x(self, u):
        f = np.zeros_like(u)
        rho = u[:, 0]
        u_x = u[:, 1] / rho
        E = u[:, 2]
        p = (self.gamma - 1.0) * (E - 0.5 * rho * u_x ** 2)

        f[:, 0] = rho * u_x
        f[:, 1] = rho * u_x ** 2 + p
        f[:, 2] = u_x * (E + p)

        return f

    def spectral_radius_x(self, u):
        rho = u[:, 0]
        u_x = u[:, 1] / rho
        p = (self.gamma - 1.0) * (u[:, 2] - 0.5 * rho * u_x ** 2)
        return np.abs(u_x) + np.sqrt(self.gamma * p / rho)
```

### Solution

```
eqn = Euler1d(pars)
soln = centpy.Solver1d(eqn)
soln.solve()
```

### Animation

```
# Animation

# First set up the figure, the axis, and the plot element we want to animate
fig = plt.figure(figsize=(10,8))
ax1=fig.add_subplot(1,2,1)
ax2=fig.add_subplot(2,2,2)
ax3=fig.add_subplot(2,2,4)

# Set the labels
ax1.set_xlabel('x')
ax1.set_ylabel(r'$\rho$')
ax2.set_ylabel(r'$v_x$')
ax3.set_xlabel('x')
ax3.set_ylabel(r'$p$')

# Axis limits and lines
line_u=[]
for ax in [ax1, ax2, ax3]:
  ax.set_xlim(0, 1.05)
  ax.set_ylim(0.1, 1.05)
  line_u.append(ax.plot([], [], linewidth=1, color='b', marker='o', markersize=2)[0])

plt.subplots_adjust(bottom=0.1, right=0.8, top=0.9)

# animation function.  This is called sequentially
j0 = slice(2,-2)
def animate(i):
    rho=soln.u_n[i,j0,0]
    u_x = soln.u_n[i,j0,1]/soln.u_n[i,j0,0]
    p = (soln.gamma-1.)*(soln.u_n[i,j0,2]-0.5*rho*u_x**2)
    line_u[0].set_data(soln.x[j0], rho)
    line_u[1].set_data(soln.x[j0], u_x)
    line_u[2].set_data(soln.x[j0], p)

plt.close()
anim = animation.FuncAnimation(fig, animate, frames=soln.Nt, interval=100, blit=False);
HTML(anim.to_html5_video())

```


## Define belly regime for N' and N'' (increasing with depth)

$C(z)=C_0 + C'(z-z_0) + C''(z-z_0)^2$, where $N_0$ is the concentration at $z_0$. I will start with $z_0=H_s$, so $C_0$ is a reference concentration at shelf break depth.

```python
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
import sympy as sym
import seaborn as sns
sym.init_printing() # enable fancy printing
```

    /Users/Karina/anaconda3/lib/python3.5/site-packages/matplotlib/__init__.py:872: UserWarning: axes.color_cycle is deprecated and replaced with axes.prop_cycle; please use the latter.
      warnings.warn(self.msg_depr % (key, alt_key))

```python
# Set appearance options seaborn
sns.set_style('white')
sns.set_context('notebook')
```

```python
C1,C2,z,Co,Hs,tau,Z = sym.symbols('C1,C2,z,Co,Hs,tau, Z')
func = Co + C1*(z-Hs) + C2*((z-Hs)**2)

```

```python
func
```

### What should be the value of $\hat{C''}$ to get maxima, minima or inflectio points within Hd?

i.e. Profiles with 'bellies' (max, mins or inflection points)

There are bellies wherever

$d\hat{C}/dz=\hat{C'}+2\hat{C''}(z-Hs)=0$. (1)

I want these points (I'll call them $z_{belly}$) to be within my profile ($0\le z \le Hd$), so I have the condition:

  $0 \le z_{belly} \le 400$ (2),

but solving (1) for $z_{belly}$ gives,

$z_{belly}=\frac{-\hat{C'}}{2\hat{C''}}+Hs$ (3).

The inequalities (2) and (3) give:

(4) $\frac{\hat{C'}}{2Hs}\lt {\hat{C''}}$ and (5) $\frac{\hat{C'}}{2(Hs-Hd)}\gt {\hat{C''}}$,

for a given $ 0\le\hat{C'}$ (since I want increasing profiles)

The region that satisfies conditions (4) and (5) is the triangle plotted below. Also, these hold for the dimensional parameters $C'$ and $C''$, where $  0 \le C'$

```python
p2 = sym.plot_implicit(sym.Or(C1/(2*(147.5-400)) > C2,C1/(2*147.5) < C2),(C1,0,0.5),(C2,-0.005,0.005),
                      title='C2 values as a function of C1 values')

```

Let's see if it works...

```python
func = 0 + C1*(z-Hs) + C2*((z-Hs)**2)
hand = sym.plot(func.subs({C1:0.00005,Hs:147.5,C2:0.0005/(2*147.5)}),
               func.subs({C1:0.003,Hs:147.5,C2:0.03/(2*147.5)}),
               func.subs({C1:0.005,Hs:147.5,C2:-0.005/(2*147.5)}),
               func.subs({C1:0.01,Hs:147.5,C2:-0.0067/(2*147.5)}),(z, 0, 400),
               xlabel='Depth (m)',
               ylabel='Concentration (nd)',
               title='Using the upper bound of C2',
               show=False)
hand[1].line_color='r'
hand[2].line_color='g'
hand[3].line_color='purple'

hand.show()
```

## $\tau_v$ as a function of N' and N''

$$\frac{\tau}{Z}=\frac{\delta^2_vN}{\delta_vN}=Z\frac{\delta^2_vN}{\delta_vN}\rvert_{z=Hs}=\frac{2N''}{N'}$$

We have some bounds for N' and N'': we imposed $0\le N'$ and $N'/2Hs\gt N''$ and $N''\lt N'/(2(Hs-400))$ to have a profile with a max, min or inflection point above 400 m.

Using these bounds, we can now bound the values of \tau_v/Z:

$$\frac{\tau}{Z}=\frac{2N''}{N'} \gt \frac{2N'}{2HsN'} = \frac{1}{Hs}$$

and

$$\frac{\tau}{Z}=\frac{2N''}{N'} \lt \frac{2N'}{2N'(Hs-Hd)} = \frac{1}{(Hs-Hd)}$$

These bounds do not depend on N' or N'', only on Hs.

So,

$$ \frac{1}{Hs-Hd} \gt \frac{\tau}{Z} \gt \frac{1}{Hs}$$

Hs=0 and Hd are asymptotes, but that is fine. If Hs = 0 it means you don't even have a shelf and if Hs=400 m you probbaly care about not having bellies deeper than that, so 400 m wouldn't even be an asymptote in the first place.

```python
p2 = sym.plot_implicit(sym.Or(1/Hs < tau, 1/(Hs-400)> tau), (Hs, 0, 400),(tau,-0.1,0.1),
                      title='Tau/Z as a function of Hs',
                      ylabel='Tau/Z')

```

In the idealized bathymetry Hs=147.5 m, so the bounds are: $-0.004  \gt \frac{\tau}{Z} or  \frac{\tau}{Z}\gt 0.007$, units are m$^{-1}$. If I say that Z can be between 0 and 100 m then the values of $\tau_v$ can be:

```python
p2 = sym.plot_implicit(sym.Or(Z/147.5 < tau, Z/(147.5-400)> tau), (Z, 0, 160),(tau,-0.8,1.5),
                      title='Tau as a function of Z',
                      ylabel='Tau')

```

### Regime definition

Now we have all the elements to define a range of values for $N'$, $N''$ and $\tau_v$:

(1) Linear profiles that increase with depth:

$$0 \le N'$$.

(2) A maximum, minimum or inflection point above 400 m given an $N'$:

$$\frac{\hat{N'}}{2Hs}\lt {\hat{N''}} \,and \,  \frac{\hat{N'}}{2(Hs-400)}\gt {\hat{N''}}$$.

Using these bounds, the regime of values of $\tau/Z$ is:

$$ \frac{1}{Hs-400} \gt \frac{\tau}{Z}$$, or  $$\frac{\tau}{Z} \gt \frac{1}{Hs}$$.

For our value of Hs,

$$ -0.004 \gt \frac{\tau}{Z}$$, or  $$\frac{\tau}{Z} \gt 0.007$$.

```python
Hs = 147.5
No = 0

Nprime = np.array([0.1, 0.05,0.02,0.01,0.005,0.001])
z = np.linspace(0,400,50)

labels = ['C1=%1.1e'% Nprime[0],'C1=%1.1e'% Nprime[1],'C1=%1.1e'% Nprime[2],
          'C1=%1.1e'% Nprime[3],'C1=%1.1e'% Nprime[4],'C1=%1.1e'% Nprime[5]]
colors = ['blue','green','purple','red','orange','pink']
fig,ax = plt.subplots(1,1,figsize=(6,8))

for N1,lab,cc in zip(Nprime,labels,colors):
    N2min = (N1/(2*(Hs-400))-0.001)
    N2max = (N1/(2*Hs))+0.001

    Nmin = No + N1*(z-Hs) + N2min*(z-Hs)**2
    Nmax = No + N1*(z-Hs) + N2max*(z-Hs)**2

    ax.plot(Nmin,z, '--', color=cc)
    ax.plot(Nmax,z,'-', color=cc, label=lab)

    Tau_max = 2*N2max/(N1)
    Tau_min = 2*N2min/(N1)

    print('N1=%1.4f, N2min=%1.3e, Tau/Z=%1.3e \n N1=%1.4f, N2max=%1.3e, Tau/Z=%1.3e \n '
          %(N1,N2min,Tau_min,N1,N2max,Tau_max))

ax.set_xlabel('Concentration ($\mu$M)')
ax.set_ylabel('Depth (m)')
ax.invert_yaxis()
ax.axvline(0, color='k')
ax.legend(loc=0)
```

The linear profile I have used for all runs has:

$C'=(2.2\mu M-45.3\mu M)/(0m-1200m)=0.359 \mu M m^{-1}$

so, ${C'}=0.3 \mu M m^{-1}$

```python
Hs = 147.5
No = 7.49
Nprime = np.array([0.3/No])
z = np.linspace(0,400,50)

labels = ['C1=0.047']
colors = ['blue','green','purple','red','orange','pink']
fig,ax = plt.subplots(1,1,figsize=(6,8))

for N1,lab,cc in zip(Nprime,labels,colors):
    N2min =(N1/(2*(Hs-400))) - 0.001
    N2max = (N1/(2*Hs))+0.001
    N2mean = (N2max-N2min)/2

    Nmean = No + N1*(z-Hs)
    Nmax = No + N1*(z-Hs) + N2max*(z-Hs)**2
    Nmin = No + N1*(z-Hs) + N2min*(z-Hs)**2

    ax.plot(Nmean,z,'-', color=cc, label='lin profile')
    ax.plot(Nmax,z,':', color=cc, label='max curv')
    ax.plot(Nmin,z,'--', color=cc, label='min curv')

    Tau_mean = 2*N2mean/(N1)

    print('N1=%1.4f, N2mean=%1.3e, Tau/Z=%1.3e \n'
          %(N1,N2min,Tau_mean))

ax.set_xlabel('Concentration ($\mu$M)')
ax.set_ylabel('Depth (m)')
ax.invert_yaxis()
ax.axvline(0, color='k')
ax.legend(loc=0)
```

```python

```


```python
%load_ext sympyprinting
%matplotlib inline
import matplotlib.pyplot as plt
import sympy
from IPython.display import display
sympy.init_printing(use_unicode=False, wrap_line=False, no_global=True)
import scipy.constants
import numpy as np
```

    /home/ash/anaconda2/envs/python3/lib/python3.6/site-packages/IPython/extensions/sympyprinting.py:31: UserWarning: The sympyprinting extension has moved to `sympy`, use `from sympy import init_printing; init_printing()`
      warnings.warn("The sympyprinting extension has moved to `sympy`, "

# Equation of motion - SDE to be solved

### $\ddot{q}(t) + \Gamma_0\dot{q}(t) + \Omega_0^2 q(t) -  \dfrac{1}{m} F(t) = 0 $

#### where q = x, y or z

Where $F(t) = \mathcal{F}_{fluct}(t) + F_{feedback}(t)$

Taken from page 46 of 'Dynamics of optically levitated nanoparticles in high vacuum' - Thesis by Jan Gieseler

Using $\mathcal{F}_{fluct}(t) = \sqrt{2m \Gamma_0 k_B T_0}\dfrac{dW(t)}{dt}$

and $F_{feedback}(t) = \Omega_0 \eta q^2 \dot{q}$

Taken from page 49 of 'Dynamics of optically levitated nanoparticles in high vacuum' - Thesis by Jan Gieseler

we get the following SDE:

$\dfrac{d^2q(t)}{dt^2} + (\Gamma_0 - \Omega_0 \eta q(t)^2)\dfrac{dq(t)}{dt} + \Omega_0^2 q(t) - \sqrt{\dfrac{2\Gamma_0 k_B T_0}{m}} \dfrac{dW(t)}{dt} = 0$

split into 2 first order ODE/SDE s

letting $v = \dfrac{dq}{dt}$

$\dfrac{dv(t)}{dt} + (\Gamma_0 - \Omega_0 \eta q(t)^2)v + \Omega_0^2 q(t) - \sqrt{\dfrac{2\Gamma_0 k_B T_0}{m}} \dfrac{dW(t)}{dt} = 0$

therefore

$\dfrac{dv(t)}{dt} = -(\Gamma_0 - \Omega_0 \eta q(t)^2)v - \Omega_0^2 q(t) + \sqrt{\dfrac{2\Gamma_0 k_B T_0}{m}} \dfrac{dW(t)}{dt} $

$v = \dfrac{dq}{dt}$ therefore $dq = v~dt$

\begin{align}
dq&=v\,dt\\
dv&=[-(\Gamma_0-\Omega_0 \eta q(t)^2)v(t) - \Omega_0^2 q(t)]\,dt + \sqrt{\frac{2\Gamma_0 k_B T_0}m}\,dW
\end{align}

### Apply Milstein Method to solve

Consider the autonomous Itō stochastic differential equation

${\mathrm  {d}}X_{t}=a(X_{t})\,{\mathrm  {d}}t+b(X_{t})\,{\mathrm  {d}}W_{t}$

Taking $X_t = q_t$ for the 1st equation above (i.e. $dq = v~dt$) we get:

$$ a(q_t) = v $$
$$ b(q_t) = 0 $$

Taking $X_t = v_t$ for the 2nd equation above (i.e. $dv = ...$) we get:

$$a(v_t) = -(\Gamma_0-\Omega_0\eta q(t)^2)v - \Omega_0^2 q(t)$$
$$b(v_t) = \sqrt{\dfrac{2\Gamma_0 k_B T_0}m}$$

${\displaystyle b'(v_{t})=0}$ therefore the diffusion term does not depend on ${\displaystyle v_{t}}$ , the Milstein's method in this case is therefore equivalent to the Euler–Maruyama method.

We then construct these functions in python:

```python
def a_q(t, v, q):
    return v

def a_v(t, v, q):
    return -(Gamma0 - Omega0*eta*q**2)*v - Omega0**2*q

def b_v(t, v, q):
    return np.sqrt(2*Gamma0*k_b*T_0/m)

```

Using values obtained from fitting to data from a real particle we set the following constant values describing the system. Cooling has been assumed to be off by setting $\eta = 0$.

```python
Gamma0 = 4000 # radians/second
Omega0 = 75e3*2*np.pi # radians/second
eta = 0.5e7
T_0 = 300 # K
k_b = scipy.constants.Boltzmann # J/K
m = 3.1e-19 # KG
```

partition the interval [0, T] into N equal subintervals of width $\Delta t>0$:
$ 0=\tau _{0}<\tau _{1}<\dots <\tau _{N}=T{\text{ with }}\tau _{n}:=n\Delta t{\text{ and }}\Delta t={\frac {T}{N}}$

```python
dt = 1e-10
tArray = np.arange(0, 100e-6, dt)
```

```python
print("{} Hz".format(1/dt))
```

    10000000000.0 Hz

set $Y_{0}=x_{0}$

```python
q0 = 0
v0 = 0
q = np.zeros_like(tArray)
v = np.zeros_like(tArray)
q[0] = q0
v[0] = v0
```

Generate independent and identically distributed normal random variables with expected value 0 and variance dt

```python
np.random.seed(88)

dwArray = np.random.normal(0, np.sqrt(dt), len(tArray)) # independent and identically distributed normal random variables with expected value 0 and variance dt
```

Apply Milstein's method (Euler Maruyama if $b'(Y_{n}) = 0$ as is the case here):

recursively define $Y_{n}$ for $ 1\leq n\leq N $ by

$ Y_{{n+1}}=Y_{n}+a(Y_{n})\Delta t+b(Y_{n})\Delta W_{n}+{\frac  {1}{2}}b(Y_{n})b'(Y_{n})\left((\Delta W_{n})^{2}-\Delta t\right)$

Perform this for the 2 first order differential equations:

```python
#%%timeit

for n, t in enumerate(tArray[:-1]):
    dw = dwArray[n]
    v[n+1] = v[n] + a_v(t, v[n], q[n])*dt + b_v(t, v[n], q[n])*dw + 0
    q[n+1] = q[n] + a_q(t, v[n], q[n])*dt + 0

```

We now have an array of positions, $v$, and velocities $p$ with time $t$.

```python
plt.plot(tArray*1e6, v)
plt.xlabel("t (us)")
plt.ylabel("v")
```

```python
plt.plot(tArray*1e6, q)
plt.xlabel("t (us)")
plt.ylabel("q")
```

## Alternative multiple stage Runge-Kutta method

The previously used Euler-Maruyama method is a simple generalization of the Euler method for ordinary differential equations to stochastic differential equations. It is a relatively simple, unsophisticated method that requires small timesteps relative to the timescale of the dynamics to work correctly.

A more sophisticated alternative method we can use is a derivative-free version of Milsteins method as a two-stage kind-of Runge-Kutta method, documented in [wikipedia](https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_method_%28SDE%29) or from the original paper deriving the method on [arxiv](https://arxiv.org/pdf/1210.0933.pdf).

```python
q0 = 0
v0 = 0
X = np.zeros([len(tArray), 2])
X[0, 0] = q0
X[0, 1] = v0
```

```python
def a(t, X):
    q, v = X
    return np.array([v, -(Gamma0 - Omega0*eta*q**2)*v - Omega0**2*q])

def b(t, X):
    q, v = X
    return np.array([0, np.sqrt(2*Gamma0*k_b*T_0/m)])
```

```python
len(tArray)
```

```python
#%%timeit

S = np.random.choice([-1, 1], len(tArray)) # randomly choose -1 or 1 with 50% chance
for n, t in enumerate(tArray[:-1]):
    dw = dwArray[n]
    K1 = a(t, X[n])*dt + b(t, X[n])*(dw - S[n]*np.sqrt(dt))
    Xh = X[n] + K1
    K2 = a(t, Xh)*dt + b(t, Xh)*(dw + S[n]*np.sqrt(dt))
    X[n+1] = X[n] + 0.5 * (K1+K2)

```

```python
q = X[:, 0]
v = X[:, 1]
```

```python
plt.plot(tArray*1e6, v)
plt.xlabel("t (us)")
plt.ylabel("v")
```

```python
plt.plot(tArray*1e6, q)
plt.xlabel("t (us)")
plt.ylabel("q")
```

The form of $F_{feedback}(t)$ is still questionable

On page 49 of 'Dynamics of optically levitated nanoparticles in high vacuum' - Thesis by Jan Gieseler he uses the form: $F_{feedback}(t) = \Omega_0 \eta q^2 \dot{q}$

On page 2 of 'Parametric feeedback cooling of levitated optomechancs in a parabolic mirror trap' Paper by Jamie and Muddassar they use the form: $F_{feedback}(t) = \dfrac{\Omega_0 \eta q^2 \dot{q}}{q_0^2}$ where $q_0$ is the amplitude of the motion: $q(t) = q_0(sin(\omega_0t)$

However it always shows up as a term $\delta \Gamma$ like so:

$\dfrac{d^2q(t)}{dt^2} + (\Gamma_0 + \delta \Gamma)\dfrac{dq(t)}{dt} + \Omega_0^2 q(t) - \sqrt{\dfrac{2\Gamma_0 k_B T_0}{m}} \dfrac{dW(t)}{dt} = 0$

By fitting to data we extract the following 3 parameters:

1) $A = \gamma^2 \dfrac{k_B T_0}{\pi m}\Gamma_0 $

Where:

- $\gamma$ is the conversion factor between Volts and nanometres. This parameterises the amount of light/ number of photons collected from the nanoparticle. With unchanged allignment and the same particle this should remain constant with changes in pressure.
- $m$ is the mass of the particle, a constant
- $T_0$ is the temperature of the environment
- $\Gamma_0$ the damping due to the environment only

2) $\Omega_0$ - the natural frequency at this trapping power

3) $\Gamma$ - the total damping on the system including environment and feedback etc...

By taking a reference save with no cooling we have $\Gamma = \Gamma_0$ and therefore we can extract $A' = \gamma^2 \dfrac{k_B T_0}{\pi m}$. Since $A'$ should be constant with pressure we can therefore extract $\Gamma_0$ at any pressure (if we have a reference save and therefore a value of $A'$) and therefore can extract $\delta \Gamma$, the damping due to cooling, we can then plug this into our SDE instead in order to include cooling in the SDE model.

For any dataset at any pressure we can do:

$\Gamma_0 = \dfrac{A}{A'}$

And then $\delta \Gamma = \Gamma - \Gamma_0$

Using this form and the same derivation as above we arrive at the following form of the 2 1st order differential equations:

\begin{align}
dq&=v\,dt\\
dv&=[-(\Gamma_0 + \delta \Gamma)v(t) - \Omega_0^2 v(t)]\,dt + \sqrt{\frac{2\Gamma_0 k_B T_0}m}\,dW
\end{align}

```python
def a_q(t, v, q):
    return v

def a_v(t, v, q):
    return -(Gamma0 + deltaGamma)*v - Omega0**2*q

def b_v(t, v, q):
    return np.sqrt(2*Gamma0*k_b*T_0/m)

```

values below are taken from a ~1e-2 mbar cooled save

```python
Gamma0 = 15 # radians/second
deltaGamma = 2200
Omega0 = 75e3*2*np.pi # radians/second
eta = 0.5e7
T_0 = 300 # K
k_b = scipy.constants.Boltzmann # J/K
m = 3.1e-19 # KG
```

```python
dt = 1e-10
tArray = np.arange(0, 100e-6, dt)
```

```python
q0 = 0
v0 = 0
q = np.zeros_like(tArray)
v = np.zeros_like(tArray)
q[0] = q0
v[0] = v0
```

```python
np.random.seed(88)

dwArray = np.random.normal(0, np.sqrt(dt), len(tArray)) # independent and identically distributed normal random variables with expected value 0 and variance dt
```

```python
for n, t in enumerate(tArray[:-1]):
    dw = dwArray[n]
    v[n+1] = v[n] + a_v(t, v[n], q[n])*dt + b_v(t, v[n], q[n])*dw + 0
    q[n+1] = q[n] + a_q(t, v[n], q[n])*dt + 0

```

```python
plt.plot(tArray*1e6, v)
plt.xlabel("t (us)")
plt.ylabel("v")
```

```python
plt.plot(tArray*1e6, q)
plt.xlabel("t (us)")
plt.ylabel("q")
```

```python

```

```python

```


# Enfriamiento en placas metalicas

- 1.1 El objetivo de este proyecto es **intentar conocer cual es el tiempo de enfriamiento, principalmente en las placas metalicas, con el uso de ecuaciones diferneciales** .

- 1.2 utlizando la ley del enfriamiento de Newton, para saber en cuanto tiempo se enfriaria una placa de metal, adotando su uso para la ingenieria civil.

- # 1.3 Modelo

El modelo establece que la tasa de pérdida de calor de un cuerpo es proporcional a la diferencia de temperatura entre el cuerpo y sus alrededores con la ecuacion:

\begin{equation}
\frac{dT(t)}{dt}  = -k(T-Tm)
\end{equation}

Donde **T** es la tempretura del objeto, **Tm** es la temperatura del medio ambiente, y **k** es una constante de proporcionalidad.

- Para resolver esta ecuacion primero separamos las variables

\begin{equation}
\frac{dT(t)}{T-Tm}  = -k(dt)
\end{equation}

- Despues pasamos a intrgrar cada uno de sus lados

\begin{equation}
\int(\frac{dT(t)}{T-Tm})  = -k\int(dt)
\end{equation}

- Con lo cual obtenemos que la solucion de esta ecuacion diferencial es:

\begin{equation}
\ln({T-Tm})  = -kt+c
\end{equation}

- Y despejando

\begin{equation}
\ {T}  = Tm+Ce^{-kt}
\end{equation}

# 1.4 Simulacion

**Enfriamieto**

Tenemos una placa metalica, la cual  despues de soldarla para unir 2 piezas, se encuentra a una tempratura de 200 grados centigrados, el ambiente cuenta con una tempretura de 23 grados centigrados, y despues de 20 minutos esta a una tempratura de 100 grados queremos saber cuanto (tiempo en minutos) tardara eta placa en enfriarse para seguir trabajando, (25 grados seria la temperatura optima).

```python
import sympy as sym
sym.init_printing(use_latex='mathjax')
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
from scipy.integrate import odeint
```

```python
sym.var('t', real=True, positive=True)
T = sym.Function('T')
def func(T,k,t,x0,Tm):
    T1 = T[0]
    return np.array(-k*(T-Tm))
x0 = [200]
#asumiendo que en el minuto 20 habia una tempreatura de 100 grados
t = np.linspace(0,20)
Tm=23
k = 0.041617
T = odeint(func, x0, t, args=(T,k,Tm))
T1 = T[:,0]
```

```python
plt.figure(figsize=(8,6))
plt.plot(t, T1, 'r', lw=2, label='Temperatura')
plt.grid()
plt.legend(loc='best')
plt.xlabel('t [minutosx10]')
plt.ylabel('grados')
plt.show()

```

Como podemos observar en la grafica las condiciones mencionadas se cumplen, en t=20 la temperatura es igual a 100 grados, y podemos observar que alrededor del momento T=26 la tempreatura ya es optima para seguir trabajando.

# Ejemplo adicional (calentamiento)

Supongamos que se aplico nitrogeno liquido en una estructura, en ese momento la estructura se encuentra a una tempratura de -100 grados centigrados,en el minuto 20 la estructura tiene una temperatura de -60 grados, la teperatura ambiente es de 25 grados, ¿cuanto tiempo tardara la estructura en estar a una tempratura optima?

```python
sym.var('t', real=True, positive=True)
T = sym.Function('T')
def func(T,k,t,x0,Tm):
    T1 = T[0]
    return np.array(-k*(T-Tm))
x0 = [-100]
#asumiendo que en el minuto 20 habia una tempreatura de -60 grados
t = np.linspace(0,20)
Tm=25
k = -5.6961
T = odeint(func, x0, t, args=(T,k,Tm))
T1 = T[:,0]
```

```python
plt.figure(figsize=(8,6))
plt.plot(t, T1, 'r', lw=2, label='Temperatura')
plt.grid()
plt.legend(loc='best')
plt.xlabel('t [minutosx10]')
plt.ylabel('grados')
plt.show()
```

Como podemos ver esta estructura tendra una tempratura optima para trabajar aproximadamente al tiempo t=26.

# Bibliografia

Clase9_IntroED Archivo
Clase12_OsciladorArmonico Archivo
Clase13_OsciladorAmortiguado Archivo
http://www.sc.ehu.es/sbweb/fisica/estadistica/otros/enfriamiento/enfriamiento.htm
https://es.slideshare.net/cemepn/ley-de-enfriamiento-de-newton-16840945

```python

```