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

## Convolution

Convolution is often employed to obtain the degree of similarities between analyzed signal and prototype functions.
It's major advantage is to use known atoms to investigate unknown phenomena.
It is an important concept for Fourier transform, Wavelet transform etc.

The convolution of two functions $f(t)$ and $g(t)$ is:
\begin{equation}
h(t) = f(t)* g(t)= \int_{-\infty}^{\infty}f(\tau)g(t-\tau)d\tau
\end{equation}

For discrete time series, the convolution is a sum instead of an integral.
\begin{equation}
h(n) = \sum_{m=-\infty}^{\infty}f(m)g(n-m)
\end{equation}

The most common fast convolution algorithms use fast Fourier transform (FFT) algorithms via the circular convolution theorem.

```python
def conv(f, g, same=True):
    """
    discrete convolution:
        i: [0, len(f)+len(g)-1)
        j: [max(0, i-len(g)+1), min(i, len(f)-1)]
    """
    len_f = len(f)
    len_g = len(g)
    len_h = len_f + len_g - 1
    h = np.zeros(len_h)
    for i in range(len_h):
        for j in range(max(0, i-len_g+1), min(i, len_f-1)+1):
            h[i] += f[j]*g[i-j]
    if same:
        l = (len(g)-1)//2
        h = h[l:len(h)-(len(g)-l-1)]
    return h
```

```python
def gaussian(t, sigma, mu):
    return np.e ** (-0.5*((t-mu)/sigma)**2)

def Plot(x,y,z):
    # plot the morlet atom
    plt.figure(figsize=(10,5))
    ax = plt.gca()
    ax.grid(color='#b7b7b7', linestyle='-', linewidth=0.5, alpha=0.5)
    plt.plot(t,np.real(x), color='#333333', linestyle=':', alpha=0.5)
    plt.plot(t,np.real(y), color='#333333', linestyle=':', alpha=0.5)
    plt.plot(t,np.real(z), color='#121212', linestyle='-')
    plt.plot(t,np.imag(z), color='#121212', linestyle='-.')

# sampling rate
Fs = 100
# sampling interval
Ts = 1.0/Fs
# time vector
t = np.arange(0, 10, Ts)
# frequency of the atom
w = 2*np.pi*1
x=np.e**(-1j*w*t)
# construct the morlet atom
y=gaussian(t, 1, 5)
morlet=x*y
Plot(x,y,morlet)

```

### Morlet wavelet (standard version)
\begin{equation}
\psi(t) = \pi^{-1/4}e^{jwt}e^{-t^2/2}
\end{equation}

### Daughter wavelet atoms
\begin{equation}
\psi_{a,b}(t) = \frac1{\sqrt{a}}\psi\left(\frac{t - b}{a}\right)
\end{equation}

```python
def morlet(N, s=1.0, w=6.0):
    """
    Complex Morlet wavelet.

    :param N: (int) Length of the wavelet
    :param s: (float) Scaling factor. Default is 1.0
    :param w: (float) Omega0. Default is 5.0
    :return: morlet: (ndarray) Morlet wavelet
    """
    t = np.linspace(-1/s*2*np.pi, 1/s*2*np.pi, N)
    return np.pi**(-0.25)*np.exp(1j*w*t)*np.exp(-0.5*(t**2))

def fourier(x):
    """
    Fourier transform.

    :param x: (ndarray) input signal
    :return: X: (ndarray) one side frequency of input signal
    """
    n = len(x)         # length pf the signal
    X = np.fft.fft(x)  # fast fourier transform
    X /= n             # normalization
    X = X[range(n//2)] # one side frequency range
    return abs(X)

def Plot(morlet_atoms, scales, morlet_freqs, freq):
    alphas = [0.4, 0.7, 1]
    plt.figure(figsize=(10,5))
    plt.subplot(2,1,1)
    ax = plt.gca()
    ax.grid(color='#b7b7b7', linestyle='-', linewidth=0.5, alpha=0.5)
    for i in range(len(morlet_atoms)):
        plt.plot(np.real(morlet_atoms[i]), color="#333333", alpha=alphas[i], label=str(scales[i]))
    plt.legend(ncol=4, loc='upper right', fontsize='small')
    plt.subplot(2,1,2)
    ax = plt.gca()
    ax.grid(color='#b7b7b7', linestyle='-', linewidth=0.5, alpha=0.5)
    for i in range(len(morlet_atoms)):
        plt.plot(freq, morlet_freqs[i], color="#333333", alpha=alphas[i])

fs = 1
dt=1/fs
n = 256
T = n * dt
freq = np.arange(n)/T    # two side frequencies
freq = freq[range(n//2)] # one side frequencies

scales = [0.5, 1, 2]
morlet_atoms = [1/np.sqrt(s) * morlet(n, s) for s in scales]
morlet_freqs = [fourier(x) for x in morlet_atoms]
Plot(morlet_atoms, scales, morlet_freqs, freq)
```

### Continuous wavelet transform
\begin{equation}
W_f(a,b) = \frac1{\sqrt{a}}\int_{-\infty}^{+\infty}f(t)\psi^*\left(\frac{t - b}{a}\right)dt
\end{equation}
It is possible to calculate the wavelet transform using convolution in time space, it is considerably faster to do the calculations in Fourier space.

```python
t = np.linspace(-1, 1, 256, endpoint=False)
data  = 2*np.sin(2 * np.pi * 1 * t) + np.sin(2 * np.pi * 5 * t)*(t>0)
scales = np.arange(1, 21)

wavelet_spectrum = np.zeros([len(scales), len(data)], dtype=np.complex)
for ind, scale in enumerate(scales):
    wavelets = morlet(min(10 * scale, len(data)), scale)
    wavelet_spectrum[ind, :] = conv(dasta, wavelets)

wavelet_power_spectrum = abs(wavelet_spectrum)**2

fig = plt.figure(figsize=(10, 5))
fig.subplots_adjust(left=0.09, bottom=0.09, right=0.95, top=0.95,
                    hspace=0.05, wspace=0.05)
plt.subplot(2,1,1)
ax = plt.gca()
ax.grid(color='#b7b7b7', linestyle='-', linewidth=0.5, alpha=0.5)
plt.setp(ax.get_xticklabels(), visible=False)
plt.plot(data, '#333333')
plt.subplot(2,1,2)
plt.imshow(wavelet_spectrum.real, cmap='PRGn', aspect='auto',
           vmax=abs(wavelet_spectrum.real).max(), vmin=-abs(wavelet_spectrum.real).max())
plt.show()

```


#
Potencias y radicales

Con la biblioteca Sympy cargada, Python puede trabajar con radicales y potencias y aplicar algunas de sus propiedades para simplificar el resultado. La función **simplify** es una de las más útiles en este contexto. Sin embargo su potencia es limitada y no "sabe" simplificar ciertas expresiones.

```python
from sympy import *
init_printing()
a = symbols('a')
n = symbols('n')
m = symbols('m')
```

###
Simplifica las siguientes potencias:

* $a^{-n}$

* $ a^{-6}$

* $a^n \cdot a^m $

* $\displaystyle \frac{a^n}{a^m}$

* $ \left(a^n\right)^m$

* $\left( a^5\right)^7$

```python
simplify((a**7)**5)
```

```python

```

```python

```

```python

```

```python

```

Sympy también conoce y trabaja con las propiedades de los radicales. Para escribir **una raíz $n$-ésima debemos elevar el número a _Rational(1,n)_**, pues si lo elevamos simplemente a $1/n$ será Python y no Sympy el que haga los cálculos y estos no se realizarán  de modo exacto.

###
Simplfica los siguientes radicales:

* $\sqrt{80}$

* $\sqrt[5]{224}$

* $ 2^{16/6}$

* $\sqrt[12]{2^9}$

* $\sqrt[3]{2^{11}}$

```python
80**(Rational(1,2))
```

```python
224**Rational(1,5)
```

```python
2**Rational(6,16)
```

```python
2**Rational(9,12)
```

###
Realiza la siguientes multiplicación de radicales:

* $\sqrt[6]{2^5} \cdot \sqrt{2^7} \cdot \sqrt[3]{2^2}$

```python
2**Rational(5,6) * sqrt(2**7) *2**Rational(2,3)
```

En principio Sympy siempre da los radicales simplificados. En el caso de la racionalización ésta se realiza muchas veces por defecto. Otras veces es necesario utilizar el comando **simplify** para "forzar" a Sympy.

###
Realiza las siguientes operaciones, simplificando el resultado:

* $ \displaystyle \frac{3}{\sqrt[3]{2^7}}$

* $ \displaystyle \frac{5}{2\sqrt{3}-2}$

* $ 23\sqrt{125} + 3\sqrt{20}-2\sqrt{45}$

* $\sqrt[3]{2^5 \cdot \sqrt{2}}$

```python
3/(2**Rational(7,3))
```

```python
simplify(5/(2*sqrt(3)-2))
```

```python
23*sqrt(125) +3*sqrt(20) -2*sqrt(45)
```

```python
simplify((2**5 * sqrt(2))**Rational(1,3))
```

```python

```

notebooks/04.- Potencias y radicales.ipynb


# Nonlinear Equations and bisection

A large number of  nonlinear equations obviously *have* solutions, but the solutions cannot always be found in closed form. For example,

\begin{equation}
  f(x) = e^x + x - 2 = 0.
\end{equation}

```
%matplotlib inline
```

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

x = np.linspace(0.0, 1.0)
def fn(x):
    return np.exp(x) + x - 2.0
f = fn(x)

plt.figure()
plt.plot(x, f, label = r'$f(x) = e^x + x - 2$')
plt.xlabel('$x$')
plt.ylabel('$f(x)$')
plt.legend(loc = 2)
plt.show()
```

Numerical methods can give accurate, approximate solutions.

Write the problem as

\begin{equation}
    f(x) = 0.
\end{equation}

Goal: given $f$, find $x$.

Assume: $f$ real, continuous. Sometimes restrict to $f$ differentiable.

May consider equivalent problem

\begin{equation}
    g(x) = x.
\end{equation}

Same problem, different (geometric) interpretation.

Assume that evaluating $f$ or $g$ is \emph{expensive}. Want to minimize the number of evaluations.

Given $f: x \in \mathbb{R} \to \mathbb{R}$, continuous, find $x$ such that

\begin{equation}
    f(x) = 0.
\end{equation}

Simple and robust method: **bisection**.

1. Assume we have found (somehow!) two points, $x^{(L)}$ and $x^{(R)}$ that bracket root.
2. Check halfway point $x^{(M)}$. Either $f(x^{(M)})=0$ (problem solved), or $x^{(M)}$ and one of $x^{(L,R)}$ bracket the root.
3. Repeat to converge.

```
xL = 0.1
xR = 0.9
xM = 0.5 * (xL + xR)

plt.figure()
plt.plot(x, f)
plt.plot([xL], [fn(xL)], 'bo'); plt.plot([xL, xL], [0, fn(xL)], 'b--')
plt.plot([xR], [fn(xR)], 'bo'); plt.plot([xR, xR], [0, fn(xR)], 'b--')
plt.plot([xM], [fn(xM)], 'go'); plt.plot([xM, xM], [0, fn(xM)], 'g--')
plt.axhline(color = 'black', linestyle = '--')
plt.xlabel('$x$')
plt.ylabel('$f(x)$')
plt.show()
```

Lectures/05 - Nonlinear equations and bisection.ipynb


```python
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
import ipywidgets as widgets
import hublib.ui as ui
mpl.rc('animation', html='jshtml')
```

    <IPython.core.display.Javascript object>

# 1D Heat Transfer in a Copper Bar

A thin copper bar of thickness $L=5\,\mathrm{mm}$ is heated by an electric current resulting in a volumetric heat generation rate of $\dot{q}=5\cdot10^5\,\mathrm{W/m^3}$. The copper bar is in contact with a heat sink at temperature $T_0=50^\circ$C on the lower surface. The upper surface is exposed to an ambient temperature of $T_\infty=20^\circ\mathrm{C}$ with a convection heat transfer coefficient of $h=50\,\mathrm{W/(m^2K)}$. The thermophysical properties of pure copper are $k=401\ \mathrm{W/(m\cdot K)}$, $c_p=385\ \mathrm{J/(kg\cdot K)}$, and $\rho=8993\ \mathrm{kg/m^3}$.

Let us collect the known values in variables:

```python
rho = 8993
cp = 385
k = 401
h = 50
L = 5e-3
T0 = 50
Tinf = 20
q = 5e5
```

## 1D Heat Equation in Cartesian Coordinates

The heat transfer in the copper bar is described by the 1D heat equation

\begin{equation}
\rho c_p \frac{\partial T}{\partial t} = k \frac{\partial^2 T}{\partial x^2} + \dot{q} .
\end{equation}

Before we introduce the finite-difference formulation, we are going to obtain the steady-state temperature profile in the copper bar.

### Steady-state solution

The steady-state temperature profile is the solution of the ordinary differential equation
\begin{equation}
k \frac{d^2 T}{d x^2} + \dot{q} .
\end{equation}
subject to the boundary conditions. The general solution $T(x)$ is given by the superposition of the homogeneous solution $T_h(x)=C_1x+C_2$ and the particular solution $T_p(x) = -\frac{\dot{q}}{2k} x^2$, i.e.,
\begin{equation}
T(x) = -\frac{\dot{q}}{2k} x^2 + C_1 x + C_2 .
\end{equation}
The heat flux in the copper bar is given by Fourier's law
\begin{equation}
q''(x) = -k \frac{dT}{dx} = \dot{q} x - k C_1 .
\end{equation}
We choose the coordinate system such that the surface in contact with the heat sink is located at $x=0$ and the surface exposed to ambient air is located at $x=L$. The boundary conditions are then $T(0)=T_0$ and $q''(L)=h(T(L)-T_\infty)$ (Newton's law of cooling). We thus have to solve the equation system
\begin{align}
T(0) &= T_0 = C_2 \\
q''(L) &= h\left[T(L)-T_\infty\right] = h \left[ -\frac{\dot{q}}{2k} L^2 + C_1 L + C_2 -T_\infty \right] = \dot{q} L - k C_1 ,
\end{align}
which leads to

\begin{align}
C_1 &= \frac{\frac{\dot{q}h}{2k}L^2+\dot{q}L-h(T_0-T_\infty)}{k+hL} \\
C_2 &= T_0 .
\end{align}
Let us plot this solution:

```python
def steady_state(x):
    C2 = T0
    C1 = (q*L+h*q/(2*k)*L**2-h*(C2-Tinf))/(k+h*L)
    T = -q/(2*k)*x**2 + C1*x + C2
    return T

xs = np.linspace(0,L,20)
plt.plot(xs,steady_state(xs),'r--',lw=2,label='steady-state solution')
plt.xlabel(r'$x\ \mathrm{[mm]}$')
plt.ylabel(r'$T\ \mathrm{[^\circ C]}$')
plt.xlim(0,L)
plt.ticklabel_format(useOffset=False)
plt.legend()
plt.show()
```

### Finite Difference Formulation

The transient solution requires to solve the partial differential equation, and we have not yet discussed how to do that analytically. Here, we are going to look at a numerical solution. The basic idea is to replace the derivates with respect to space and time by finite differences. We can obtain the finite difference expression by using a Taylor expansion:

To estimate the second spatial derivative the temperature $T$, we expand the temperature at $T(x+\Delta x)$ and $T(x-\Delta x)$ around $T(x)$
\begin{equation}
\begin{split}
T(x+\Delta x) &= T(x) + \Delta x \frac{\partial T}{\partial x} + \frac{(\Delta x)^2}{2} \frac{\partial^2 T}{\partial x^2} + \dots \\
T(x-\Delta x) &= T(x) - \Delta x \frac{\partial T}{\partial x} + \frac{(\Delta x)^2}{2} \frac{\partial^2 T}{\partial x^2} - \dots
\end{split}
\end{equation}
By subtracting the two equations, we obtain an approximate expression for the first derivative
\begin{equation}
\frac{\partial T}{\partial x} \approx \frac{T(x+\Delta x)-T(x-\Delta x)}{2\Delta x}
\end{equation}
By summing the two equations, we obtain an approximate expression for the second derivative
\begin{equation}
\frac{\partial^2 T}{\partial x^2} \approx \frac{T(x+\Delta x) - 2 T(x) + T(x-\Delta x)}{(\Delta x)^2} .
\end{equation}
The time derivative of $T$ can be approximated by a so called _forward Euler time step_
\begin{equation}
T(t+\Delta t) = T(t) + \Delta t \frac{\partial T}{\partial t}
\end{equation}

We now plug the finite differences into the original PDE to obtain an algebraic integration scheme for the transient heat equation:
\begin{equation}
T(x,t+\Delta t) = T(x,t) + \Delta t \frac{k}{\rho c_p} \left( \frac{T(x+\Delta x,t) - 2 T(x,t) + T(x-\Delta x,t)}{(\Delta x)^2} + \frac{\dot{q}}{k} \right)
\end{equation}

### Discretization

This equation gives an update rule for a set of discrete points on the $x$ axis, evenly spaced by a distance $\Delta x$. Successive applications of the update rule yield the solution at discrete time steps with increment $\Delta t$. It is worth noting that the forward Euler step is conditionaly stable. This means that the time step $\Delta t$ needs to be sufficiently small such that $\alpha \Delta t/(\Delta x)^2<0.5$. Let us introduce the discrete points and space and time steps, assuming that we want to integrate until `time`.

```python
time = 0.25

dx = 2.5e-4
dt = 1.0e-4

xs = np.arange(0,L+dx,dx)
ts = np.arange(0,time+dt,dt)
```

### Boundary conditions

Looking at the update scheme, we see that the new value for each point depends on its two neighbors. However, at the boundaries of the domain, only one neighbor exists and it seems at first that the update rule cannot be applied. This can be resolved by considering the boundary conditions.

The boundary conditions for the copper bar are
\begin{equation}
\begin{split}
T(0,t) &= T_o  \\
- k \left. \frac{\partial T}{\partial x}\right|_{x=L} &= h \left[ T(L,t) - T_\infty \right]
\end{split}
\end{equation}

Thus, at the lower boundary condition we can simply set the temperature to the fixed value $T_o$. The convection boundary condition can be approximated by
\begin{equation}
-k \frac{T(L+\Delta x) -T(L-\Delta x)}{2\Delta x} = h \left[ T(L,t) - T_\infty \right] ,
\end{equation}
which can be written as
\begin{equation}
T(L+\Delta x) = T(L-\Delta x) - 2\Delta x\frac{h}{k} \left[ T(L,t) - T_\infty \right] .
%T(L,t) = \left( 1 + \Delta x \frac{h}{k} \right)^{-1} \left[T(L-\Delta x,t) + \Delta x \frac{h}{k} T_\infty \right]  .
\end{equation}

Therefore, the Euler step at this boundary becomes
\begin{equation}
T(L,t+\Delta t) %= T(L,t) + \Delta t \frac{k}{\rho c_p} \left( \frac{T(L+\Delta x,t) - 2 T(L,t) + T(L-\Delta x,t)}{(\Delta x)^2} + \frac{\dot{q}}{k} \right) \\
= T(L,t) + \Delta t \frac{k}{\rho c_p} \left( \frac{ 2 T(L-\Delta x) - 2 T(L,t) - 2\Delta x\frac{h}{k}\left[ T(L,t)-T_\infty\right]}{(\Delta x)^2} + \frac{\dot{q}}{k} \right) .
\end{equation}

To facilitate modifying the boundary conditions later, let us define them as functions:

```python
def left_boundary(T):
    return T0 # Dirichlet boundary condition

def right_boundary(T):
    return T[-1] + dt*k/(rho*cp)*(2*(T[-2]-T[-1]-dx*h/k*(T[-1]-Tinf))/dx**2+q/k) # Neumann boundary condition
```

We can now define a function that computes the transient solution:

```python
def transient():
    steps = len(ts)-1
    Ts = np.zeros((len(ts),len(xs)))
    Ts[0] = [T0]*len(xs) # initial condition
    T = np.copy(Ts[0])
    for t in range(len(ts)-1):
        T[0] = left_boundary(T)
        T[-1] = right_boundary(T)
        T[1:-1] += dt*k/(rho*cp) * ( (T[2:] - 2*T[1:-1] + T[:-2]) / (dx**2) + q/k ) # forward Euler step
        Ts[t+1] = T
    return Ts
```

Let us plot the solution at selected times along with the steady-state solution:

```python
Ts = transient()
for t in [0,100,1000]:
    plt.plot(xs,Ts[t],'.:',label='finite difference t=%.1f'%ts[t]+' s')
plt.plot(xs,steady_state(xs),'r--',lw=2,label='steady state solution')
plt.xlabel(r'$x\ \mathrm{[mm]}$')
plt.ylabel(r'$T\ \mathrm{[^\circ C]}$')
plt.ticklabel_format(useOffset=False)
plt.legend()
plt.show()
```

### Effect of materials properties on the steady and transient solution

One observation we can make is that the temperature difference within the copper bar is rather small. To see the effects of the parameters a little better, we are going to change the setup of the problem a little. Instead of a constant temperature at the left boundary, we will now consider the left surface of the copper bar to be insulated, i.e., $q''(0)=0$. This will of course change the steady-state solution.

#### Exercise: Obtain the integration constants $C_1$ and $C_2$ for this new setup.

```python
def left_boundary(T):
    T = T[0] + dt*k/(rho*cp)*(2*(T[1]-T[0])/dx**2+q/k)
    return T

def steady_state(x):
    C2 = Tinf + q*L/h + q/(2*k)*L**2
    C1 = 0
    T = -q/(2*k)*x**2 + C1*x + C2
    return T
```

In the widget below, you can explore the effect of the materials properties on the steady-state solution and the transient behavior. _Note that it takes some time to update the animation every time you change a value._

```python
def animate1d(T,frames=100,interval=50):
    step = len(T)//frames
    T = np.vstack([T[0],T[1::step]])

    def init():
        profile.set_data(xs,T[0])
        time_text.set_text('t={0:.4f} s'.format(0))
        bifo_text.set_text(r'$\tau$'+'={0:.1e} s\nBi={1:.1e}\nFo={2:.1e}'.format(tau,Bi,0))
        return (profile,time_text,bifo_text)

    def animate(i):
        Fo = k/(rho*cp)*i*dt*step/L**2
        profile.set_data(xs,T[i])
        time_text.set_text('t={0:.4f} s'.format(i*dt*step))
        bifo_text.set_text(r'$\tau$'+'={0:.1e} s\nBi={1:.1e}\nFo={2:.1e}'.format(tau,Bi,Fo))
        return (profile,time_text,bifo_text)

    tau = rho*cp*L/h
    Bi = h*L/k
    Fo = k/(rho*cp)*t/L**2
    Tst = steady_state(xs)
    Tmax = max(80,np.max(steady_state(xs)))
    Tmin = min(Tinf,np.min(steady_state(xs)))

    fig, ax = plt.subplots()
    ax.plot(xs,Tst,'r--',lw=2,label='steady-state solution')
    profile, = ax.plot(xs,Ts[0],'r',label='transient solution')
    ax.set_ylim(Tmin-(Tmax-Tmin)/10,Tmax+(Tmax-Tmin)/10)
    ax.ticklabel_format(useOffset=False)
    ax.set_title('Temperature Profile')
    ax.set_xlabel(r'$x\ \mathrm{[mm]}$')
    ax.set_ylabel(r'$T\ \mathrm{[^\circ C]}$')
    ax.legend(loc=3)
    time_text = ax.text(0.7, 0.9, 't=0 s', transform=ax.transAxes, color='k', fontsize=14)
    bifo_text = ax.text(0.7, 0.5,r'$\tau$'+'={0:.1e} s\nBi={1:.2f}\nFo={2:.2f}'.format(tau,Bi,Fo), transform=ax.transAxes, fontsize=14)

    ani = mpl.animation.FuncAnimation(fig, animate, init_func=init,
                                  frames=len(T), interval=interval, blit=True)
    plt.close()
    return ani
```

```python
def animate_transient(name='', value=''):
    global time, ts, k, cp, h, q

    # set parameters
    time = t_widget.value # observation time
    k = k_widget.value # thermal conductivity
    cp = cp_widget.value # heat capacity
    h = h_widget.value # convection coefficient
    q = q_widget.value # heat generation
    ts = np.arange(0,time+dt,dt)
    Ts = transient()

    ani = animate1d(Ts,frames=40,interval=100)
    with out_widget:
        out_widget.clear_output(wait=True)
        display(ani)
    return ani

t_widget = ui.Number(
    name = 'time',
    desc = 'total observation time',
    value = 0.5,
    units = 's',
    cb = animate_transient
)
dt_widget = ui.Number(
    name = 'dt',
    desc = 'time step',
    value = 2.5e-4,
    units = 's',
    cb = animate_transient
)
q_widget = ui.Number(
    name = 'q',
    desc = 'heat generation',
    value = 5e5,
    units = 'W/m^3',
    cb = animate_transient
)
k_widget = ui.Number(
    name = 'k',
    desc = 'thermal conductivity',
    value = 401,
    units = 'W/(K m)',
    cb = animate_transient
)
cp_widget = ui.Number(
    name = 'cp',
    desc = 'specific heat capacity',
    value = 385,
    units = 'J/(kg K)',
    cb = animate_transient
)
h_widget = ui.Number(
    name = 'h',
    desc = 'convection coefficient',
    value = 20,
    units = 'W/(K m^2)',
    cb = animate_transient
)

out_widget = widgets.Output(layout=widgets.Layout())

display(ui.Form([out_widget,t_widget,dt_widget,k_widget,cp_widget,h_widget,q_widget]))
ani = animate_transient() # this call ensures that the animation is shown in the widget
```

    Group(children=(Output(), Number(children=(HTML(value='<div data-toggle="popover" title="total observation tim…

The widget above displays two important dimensionless numbers, the Biot number $Bi$ and the Fourier number $Fo$. They are defined as
\begin{align}
Bi &= \frac{hL}{k} \\
Fo &= \frac{\alpha t}{L^2} ,
\end{align}
where $\alpha=\frac{k}{\rho c_p}$ is the _thermal diffusivity_. You can observe how $Bi$ varies with the materials properties. Note that $Fo$ increases with time - in fact, it can be considered a dimensionless time. Later in the course, we will learn how these dimensionless numbers can be used to obtain useful approximations for transient heat transfer solutions.

## Exercises

1. Obtain the steady-state temperature profile in the copper bar.
2. Implement the forward Euler finite difference scheme discussed in class. For the given parameters, a resolution $\Delta x=2.5\cdot10^{-4}\ \mathrm{mm}$ and a time step $\Delta t=10^{-4}\ \mathrm{s}$ will yield a stable scheme. Plot the solution at three different times in _one axis system_ to demonstrate that it converges to the steady-state solution. Plot the steady-state solution in the _same axis system_ as well. _Hint:_ The transient profile should be close to the steady-state solution after $\approx 1\ \mathrm{s}$.
3. What do you expect when the heat generation is set to zero? Check if your Matlab code produces the expected behavior.
4. Switch the heat generation back on and assume that the surface at $x=0$ is now insulated. Copy your Matlab code into a new script and update it to the new boundary condition. Plot the solution at three different times and also the steady-state solution in _one axis system_. _Hint:_ The time it takes to approach the steady-state solution will be much longer. Observe the solution at least up to $100\ \mathrm{s}$. The insulating boundary condition at $x=0$ can be formulated as
\begin{equation*}
T(0,t+1) = T(0,t) + \frac{2k}{\rho c_p} \frac{\Delta t}{\Delta x^2} \left[ T(1,t)-T(0,t) \right] + \frac{\dot{q}}{\rho c_p} \Delta t
\end{equation*}
5. Reduce the thermal conductivity to $k=0.4\ \mathrm{W/(m\cdot K)}$. What do you observe? Plot the solution at three different times and the steady-state solution in _one axis system_.
6. Switch off the heat generation again. What is the new steady-state solution? What do you observe? Plot the solution at three different times and the steady-state solution in _one axis system_.
7. Increase the convection coefficient to $h=5000\ \mathrm{W/(m^2\cdot K)}$. What do you observe? Plot the solution at three different times and the steady-state solution in _one axis system_.
8. Summarize your findings.

    1. What is the effect of the thermal conductivity on the shape of the temperature profile?
    2. What is the effect of the convection coefficient on the shape of the temperature profile?
    3. What is the effect of the thermal conductivity on the time it takes to reach the steady-state solution?
    4. What is the effect of the convection coefficient on the time it takes to reach the steady-state solution?

The general behavior of the transient solution can be characterized by the Biot number $Bi=\frac{hL}{k}$ and the thermal time constant $\tau=\frac{\rho c_p L}{h}$. Based on these quantities:

1. Formulate a hypothesis how the thickness $L$ of the copper bar will affect the temperature profile and the time it takes to reach the steady-state solution.
2. Formulate a hypothesis how the thermal heat capacity $\rho c_p$ of the copper bar will affect the temperature profile and the time it takes to reach the steady-state solution.


# Section 7.3 $\quad$ Diagonalization of Symmetric Matrices

In this section, we consider diagonalization of symmetric matrices since they are easier to handle and they arise in many applications.

## Two Properties of Symmetric Matrices

- All eigenvalues of a symmetric matrix are

- Eigenvectors belonging to distinct eigenvalues are

### Example 1

Find eigenvalues and eigenvectors of the matrix
\begin{equation*}
  A = \left[
        \begin{array}{rrr}
          0 & 0 & -2\\
          0 & -2 & 0\\
          -2& 0 & -3 \\
        \end{array}
      \right]
\end{equation*}

```python
from sympy import *

A = Matrix([[0, 0, -2], [0, -2, 0], [-2, 0, -3]]);

A.eigenvects()
```

    [(-4, 1, [Matrix([
       [1/2],
       [  0],
       [  1]])]), (-2, 1, [Matrix([
       [0],
       [1],
       [0]])]), (1, 1, [Matrix([
       [-2],
       [ 0],
       [ 1]])])]

## Orthogonal Matrix

Eigenvectors of a symmetric matrix is orthogonal; hence,

>**Definition** A real square matrix $A$ is called $\underline{\hspace{1.5in}}$, if

### Example 2

Let $A$ be the matrix defined in Example 1. Find an orthogonal matrix $P$ such that $D=P^{-1}AP$.

```python
from sympy import *

A = Matrix([[0, 0, -2], [0, -2, 0], [-2, 0, -3]]);
B = A.diagonalize()[0];

B.QRdecomposition()[0]
```

    Matrix([
    [  sqrt(5)/5, 0, -2*sqrt(5)/5],
    [          0, 1,            0],
    [2*sqrt(5)/5, 0,    sqrt(5)/5]])

## Several Results Involving Orthogonal Matrices

1. $A$ is orthogonal if and only if

- If $A$ is an orthogonal matrix, $det(A) = $

- If $A$ is a symmetric matrix,

### Example 3

Find an orthogonal matrix $P$ such that such that $D=P^{-1}AP$ where
\begin{equation*}
  A = \left[
        \begin{array}{rrr}
          0 & 2 & 2\\
          2 & 0 & 2\\
          2 & 2 & 0 \\
        \end{array}
      \right]
\end{equation*}

```python
from sympy import *

A = Matrix([[0, 2, 2], [2, 0, 2], [2, 2, 0]]);
B = A.diagonalize()[0];

B.QRdecomposition()[0]
```

    Matrix([
    [-sqrt(2)/2, -sqrt(6)/6, sqrt(3)/3],
    [ sqrt(2)/2, -sqrt(6)/6, sqrt(3)/3],
    [         0,  sqrt(6)/3, sqrt(3)/3]])


# Lecture 3

In this lecture we move onto non-homogeneous second-order ordinary differentials. We want to solve equations of the form

$$
a \frac{d^{2}y}{dx^{2}} + b \frac{dy}{dx} + c y = f(x)
$$

where $a$, $b$ and $c$ are real constants. We've seem previously that for the case $b^{2} \ne 4ac$ and
$f(x) = 0$, the general solution to the above equation is

$$
y = A_{1} e^{\lambda_{1}} + A_{2}e^{\lambda_{2}}
$$

where $\lambda$ are the roots to the characteristic equation $a\lambda^{2} + b \lambda + c = 0$. For the case that $b^{2} = 4ac$ (repeated roots), the general solution is of the form

$$
y =(A_{1} + A_{2} x) e^{\lambda x}
$$

Recall that the constants $A_{1}$ and $A_{2}$ are determined from the boundary conditions.

### Initialising SymPy

To solve equations using SymPy, we again need to import SymPy, and we;ll call `init_printing()` to get nicely typeset equations:

```
from sympy import *

# This initialises pretty printing
init_printing()
from IPython.display import display

# This command makes plots appear inside the browser window
%matplotlib inline
```

## Mass-spring-damper system

The differential equation that governs a single degree-of-freedom mass-spring-damper system, with a forcing term $f(t)$, is

$$
m \frac{d^{2}y}{dt^{2}} + \lambda \frac{dy}{dt} + ky = f(t)
$$

To solve this problem using SymPy, we first define the symbols $t$ (time), $m$ (mass), $\lambda$ (damper coefficient) and $k$ (spring stiffness), and the function $y$ (displacement):

```
t, m, lmbda, k = symbols("t m lambda k")
y = Function("y")
f = Function("f")
```

Next, we define the differential equation, and print it to the screen:

```
eqn = Eq(m*Derivative(y(t), t, t) + lmbda*Derivative(y(t), t) + k*y(t), f(t))
display(eqn)
```

## Classifying the ODE

Checking the order of the ODE:

```
print("This order of the ODE is: {}".format(ode_order(eqn, y(t))))
```

    This order of the ODE is: 2

As expected, it is second order. Now classifying the ODE:

```
print("Properties of the ODE are: {}".format(classify_ode(eqn)))
```

    Properties of the ODE are: ('nth_linear_constant_coeff_variation_of_parameters', 'nth_linear_constant_coeff_variation_of_parameters_Integral')

## Solving the ODE

We can now try to solve the ODE symbolically, but without defining $f(t)$ the result will be umanageble. Sympy will return a solution but it will too complicated to interpret.

We will therefore specify some of the parameters in the equation:

```
m0, lmbda0, k0 = 4, 1, 16
eqn1 = eqn.subs(m, m0).subs(k, k0).subs(lmbda, lmbda0)

dsolve(eqn1)
```

The solution is looking more manageable, but we still haven't specified $f(t)$. Setting $f(t) = 2$ and then solving,

```
eqn1 = eqn1.subs(f(t), 2)
print("ODE with f(t) = 2")
display(eqn1)

ys = dsolve(eqn1, y(t))
print("Solution to ODE:")
display(ys)
```

The solution is now looking tractable. If we set $C_{1} = C_{2} = 1$, we can plot the solution.

```
ys = ys.subs('C1', 1).subs('C2', 1)
plot(ys.args[1], (t, 0.0, 20.0), xlabel="time", ylabel="displacement")
```

We can see the that the solution is a decaying sinusoidal function.

### Exercise:

Try changing the values for $m$, $\lambda$ and $k$ to investigate their influence in the response. You can try negative values, but only positive values are physically possible.

### Exercise:

Try changing $f(t)$ to explore the response for different forcing functions.

### Example: resonance

An undamped system ($\lambda = 0$) changes character when the forcing term is sinusoidal at the natural frequency of the system. The corresponds to the case of the forcing term having the same form as the solution to the homogeneous problem.

The natural frequency $\omega_{n} = \sqrt{k/m}$. Setting $f(t) = \sin( \omega x)$

```
# Define natural frequency
omega = Symbol("omega")
omega = sqrt(k/m)

# Set f(t) = sin(\omega t)
eqn1 = eqn.subs(f(t), sin(omega*t))
display(eqn1)
```

We now set values for $m$, $\lambda$ and $k$ (we'll set $\lambda = 0$ which corresponds to an undamped system):

```
# Set numerical values for m, lambda and k
m0, lmbda0, k0 = 4, 0, 1
eqn1 = eqn1.subs(m, m0).subs(k, k0).subs(lmbda, lmbda0)

# Solve ODE
display(eqn1)
y = dsolve(eqn1)
display(y)
```

Note the presence of $t$ in front of the cosine. This implies that the solution will grow linearly in time. We can see this by plotting the solution:

```
y = y.subs('C1', 1).subs('C2', 1)
plot(y.args[1], (t, 0.0, 100.0), xlabel="time", ylabel="displacement")
```

We observe that the solution grows in time - this is a feature of resonance.

### Example: Interactive control for the natural frequency of the mass-spring-damper system

####NOTE: This will not run on the online IPython Noteboook Viewer. You would need IPython 2.3.1 and run this cell locally for the interactive to work.

The equation for the mass-spring-damper system above can be rewritten in terms of damping $\zeta$ and natural frequency $\omega_{n}$ as belowed:

$$ \frac{d^{2}y}{dt^{2}} + 2\zeta\omega_{n}\frac{dy}{dt} + \omega_{n}^2y = 0$$

which gives rise to three different solutions, whether the system is under-damped, critically damped or over-damped.

The values of $\omega_{n}$ and $\zeta$ are defined as below, from the original mass-spring system:

$$
\omega_{n} = \sqrt{\frac{k}{m}}
$$
$$
\zeta = \frac{\lambda }{2 \sqrt{mk}}
$$

The interactive widget belows provides the visualisation between the changes of $m, k, \lambda$ on the final results.

```
from sympy import *

# Import the interactive function from IPython widgets
from IPython.html.widgets import interact
from IPython.html import widgets

# This initialises pretty printing
init_printing()
from IPython.display import display
%matplotlib inline

# Declare modules nump
from numpy import *
from matplotlib.pyplot import *

# Definie time limits and step. This gives a time limit from 0 to 500 with a step of 5.
t = np.linspace(0,10,500)

# Define the initial conditions velocity = 0, position = 1
y0 = [1.0, 0.0]

# Set up the function that plots the repsonse based on slider changes
# These values of m, k and lambda here are in SI units. m in kg, k in N/m and lambda in Ns/m

def plot_response(m = 5, k = 4, lmbda = 0.0):

    # Calculate and print out values of omega_n and zeta
    omega_n = sqrt(k/m)
    zeta = lmbda/(2*sqrt(m*k))
    print "Natural frequency is",omega_n
    print "Damping ratio is",zeta

    # Define natural and damped frequency
    omega_d = omega_n*sqrt(1-zeta**2)

    # Define y(t) based on different values of zeta

    if zeta < 1: # Under-damped
        A = y0[0];
        B = (1/omega_d)*(zeta*omega_n*y0[0] + y0[1])
        y = exp(-zeta*omega_n*t)*(A*cos(omega_d*t) + B*sin(omega_d*t))
    elif zeta == 1: # Critically damped
        A = y0[0]
        B = y0[1] + omega_n*y0[0]
        y = (A + B*t)*exp(-omega_n*t)
    elif zeta > 1: # Over-damped. root_p and root_n are the positive and negative solutions of the characteristic equations.
        delta = (2*zeta*omega_n)**2 - 4*(omega_n**2)
        root_p = (0.5)*(-2*zeta*omega_n + sqrt(delta))
        root_n = (0.5)*(-2*zeta*omega_n - sqrt(delta))
        A = y0[0] + (root_p*y0[0] - y0[1])/(root_n - root_p)
        B = (-1)*(root_p*y0[0] - y0[1])/(root_n - root_p)
        y = A*exp(root_p*t) + B*exp(root_n*t)

    # Draw an label the figure window
    ax = gca()
    ax.grid(True,linestyle=':',color='0.5')
    xlabel('Time (s)',fontsize=20,labelpad=5)
    ylabel('Position (m)',fontsize=20,labelpad=10)
    ylim(-1.2,1.2)
    xlim(0,10)

    # Normalised value of y
    y = y/y[0]

    # Plot the function on the figure
    plot(t,y,color="red",linewidth=1.5)

# Call the slider interaction
interact(plot_response, m=(0,20, 0.1), k = (0,10,0.1), lmbda = (0,10,0.1))
```

```

```


## GraphSLAM Derivation

The belief function associated with a collection of measurements with uncertainties is expressed as a product of probabilities. Assuming a gaussian uncertainty associated with both the movements and the landmark sensing measurements, this becomes a product of gaussian functions; each exponent contains the constraint imposed by a measurement or movement. For instance, moving 10 units to the right from $x_1$ to $x_2$ produces a constraint $x_2-x_1-10=0$, where "10" has an uncertainty $\sigma$ to it. Then the gaussian is written as $\exp(-(x_2-x_1-10)^2/2\sigma^2)$, where sigma is now the half-width of the gaussian. The values of $x_1$ and $x_2$ that yield a constraint value of 0 maximizes the gaussian and therefore the belief function.

Say that $N$ constraints (measurements or movements) have been collected, then the belief function can be expressed as

\begin{equation}
\prod_{i=0}^N \exp(-\frac{(\text{constraint}_i)^2}{2\sigma^2})
\end{equation}

We want to use calculus to determine the combination of constraint values that maximizes the belief, but taking the derivative of a product of many gaussians with many unknowns produces a mess. Fortunately we can use the logarithm function, since it is monotonic, to simplify the belief function. The same values maximize the belief function and its logarithm. The logarithm converts the product to a sum:

\begin{equation}
-\log(\text{belief}) = \sum_{i=0}^N(\text{constraint}_i)^2/2\sigma^2
\end{equation}

Let the uncertainties differ between the move function and the landmark sensing function, such that the uncertainties are $\sigma_{mo}$ and $\sigma_{ls}$. Further, to simplify the appearance of the equations, let weight $w_{mo} = 1/2\sigma_{mo}^2$, and similarly for weight $w_{ls}$. Setting the partials with respect to all unknowns (i.e., the gradient) to zero and solving determines the extrema of the belief function, i.e., the values of the unknowns that maximize it. One ends up with a system of N linear equations in N unknowns that can be solved with linear algebra, where 'N' is the number of moves plus the number of landmark measurements.

For example, in a 1D world, say we have three robot poses, and one landmark whose distance is measured at each pose. This yields six constraint equations (the initial pose may or may not have an uncertainty associated with it depending on how it was determined; here we associate one):

\begin{equation}
x_0 - A = 0, x_1 - x_0 - B = 0, x_2 - x_1 - C = 0
\end{equation}
\begin{equation}
L_0 - x_0 - D = 0, L_0 - x_1 - E = 0, L_0 - x_2 - F = 0
\end{equation}

Plugging these into the $-\log(\text{belief})$ equation looks like this:

\begin{equation}
-\log(\text{belief}) = w_{mo}(x_0 - A)^2 + w_{mo}(x_1 - x_0 - B)^2 + w_{mo}( x_2 - x_1 - C)^2 + \\
                         w_{ls}(L_0 - x_0 - D)^2 + w_{ls}(L_0 - x_1 - E)^2 + w_{ls}(L_0 - x_2 - F)^2
\end{equation}

Applying the partial derivatives with respect to $x_0$, $x_1$, $x_2$, and $L_0$, and then setting them to zero, yields a system of four equations (2's are already canceled):

\begin{align*}
{\partial}/{\partial x_0} &\to & w_{mo}(x_0 - A) - w_{mo}(x_1 - x_0 - B) - w_{ls}(L_0 - x_0 - D) = 0 \\
{\partial}/{\partial x_1} &\to & w_{mo}(x_1 - x_0 - B) - w_{mo}(x_2 - x_1 - C) - w_{ls}(L_0 - x_1 - E) = 0 \\
{\partial}/{\partial x_2} &\to & w_{mo}(x_2 - x_1 - C) - w_{ls}(L_0 - x_2 - F) = 0 \\
{\partial}/{\partial L_0} &\to & w_{ls}(3L_0 - (x_0+x_1+x_2+D+E+F)) = 0
\end{align*}

Collecting coefficients for each term yields a system of equations that can be expressed as a matrix equation:

\begin{equation}
\large \Omega \mu = \xi \implies \mu = \Omega^{-1} \xi.
\end{equation}

These equations provide the basis for the Graph SLAM algorithm; $\mu$ is the vector of unknowns we wish to solve for. None of the derivation above needs to be reproduced in code, but only provides the basis for the algorithm. At this point all that is needed is to construct $\Omega$ and $\xi$, invert $\Omega$, and perform the matrix multiplication. Almost too easy!

For our example,

$
\quad
{\large\Omega} = \begin{bmatrix}
2w_{mo}+w_{ls} & -w_{mo} & 0 & -w_{ls} \\
-w_{mo} & 2w_{mo} + w_{ls} & -w_{mo} & -w_{ls} \\
0 & -w_{mo} & w_{mo} + w_{ls} & -w_{ls} \\
-w_{ls} & -w_{ls} & -w_{ls} & 3w_{ls}
\end{bmatrix}\quad
{\large\mu} = \begin{bmatrix} x_0 \\ x_1 \\ x_2 \\ L_0
\end{bmatrix}\quad
$
and
$
\quad
{\large\xi} =
\begin{bmatrix}
w_{mo}(A-B) - w_{ls}D \\
w_{mo}(B-C) - w_{ls}E \\
w_{mo}C - w_{ls}F \\
w_{ls}(D+E+F)
\end{bmatrix}
$

Note that $\Omega$ is symmetric. You may also notice other patterns. $\Omega$ and $\xi$ can be constructed together by processing one constraint at a time. How may be determined by simply noticing how each constraint modifies $\Omega$ and $\xi$.

A move constraint like $x_i - x_{i-1} - X$ adds weight $w_{mo}$ to both $\Omega[i,i]$ and $\Omega[i-1,i-1]$, $-w_{mo}$ to $\Omega[i-1,i]$ and $\Omega[i,i-1]$, $(w_{mo}X)$ to $\xi[i]$, and $(-w_{mo}X)$ to $\xi[i-1]$ where $X$ is the distance of the move.

A landmark measurement constraint like $L_i - x_j - Y$, where $Y$ is the measured distance to the landmark, adds weight $w_{ls}$ to $\Omega[j,j]$ and $\Omega[i+N,i+N]$, $-w_{ls}$ to $\Omega[j,i+N]$ and $\Omega[i+N,j]$, $(w_{ls}Y)$ to $\xi[i+N]$ and $-w_{ls}Y$ to $\xi[j]$, assuming the landmark positions are located in $\Omega$ and $\xi$ after $N$ measured robot poses ($N=3$ in the example).

## Extension to 2D

The $y$ coordinate is considered independent from the $x$ coordinate. Each constraint equation in 1D becomes two constraint equations in 2D, so $\Omega$ and $\xi$ double in size. It doesn't matter where in the two matrices the $y$ constraints are put, as long as it's consistent. Alternatively, one can use the same $\Omega$ matrix for both $x$ and $y$ but separate $\xi$ vectors, e.g., $\xi_x$ and $\xi_y$.

## Online SLAM

If a robot operates over an extended period of time, adding poses and landmarks to its $\Omega$ and $\xi$ matrices all the while, these structures can get to be large enough to exceed memory limitations. At the same time, larger and larger matrices take more time to invert and multiply, so the computation time will get longer. These are not viable conditions for online operation. Fortunately it turns out that one can simply keep only the latest pose information, with no loss of accuracy, as long as one properly accounts for the prior information that has accumulated. The method is explained in [this video](https://www.youtube.com/watch?v=jaeNlxhQL1I). An additional bonus is that the implementation becomes a bit more of a proper challenge.