Erwin Renzo Franco Diaz

# Introducción

Además del cálculo, una de las herramientas matemáticas fundamentales para la física es el álgebra lineal, cuyos objetos principales de estudio son los vectores y las matrices. En la sesión de esta semana veremos como trabajar con estas en $\texttt{python}$, tanto de manera simbólica como numérica. Haremos énfasis principalmente en la obtención de los autovalores y autovectores de una matriz, y lo aplicaremos a la resolución de problemas en mecánica cuántica y oscilaciones.

# Matrices simbólicas

La manera más sencilla de definir una matriz es como un arreglo (en general) rectangular de números, por ejemplo

$$
A = \left(
\begin{matrix}
-2 & -4 & 2 \\
-2 & 1 & 2 \\
4 & 2 & 5
\end{matrix}
\right).
$$

Para definir esta matriz de manera simbólica usamos el paquete $\texttt{sympy}$, en particular, el comando $\texttt{Matrix}$

```python
import sympy as sp
sp.init_printing()
```

```python
A = sp.Matrix([[-2, -4, 2],
               [-2, 1, 2],
               [4, 2, 5]])

A
```

```python
B = sp.Matrix([[1-sp.I, 4, 3],
               [0, -1*sp.I, 2],
               [1, 2, 5]])

B
```

$\texttt{sp.I} = i$, la unidad imaginaria

## Operaciones básicas

*Nota:* Antes de realizar alguna operación, verificar siempre que las matrices tengan las dimensiones adecuadas. Caso contrario, $\texttt{python}$ dará un error.

Suma

```python
A + B
```

Resta

```python
A - B
```

Multiplicación por un escalar

```python
0.3*A
```

```python
sp.Rational(1, 2)*A
```

Multiplicación de matrices

```python
A*B
```

Potencia

```python
#B**2 = B*B
sp.simplify(B**2)
```

Es posible mezclar estas operaciones algebraicas.

```python
sp.simplify(A*B - 0.2*B**2)
```

Transpuesta $A^T$

```python
A.T
```

Adjunta

$$
A^\dagger = \left(A^T\right)^*
$$

```python
from sympy.physics.quantum.dagger import Dagger
Dagger(B)
```

Determinante

```python
sp.simplify(B.det())
```

Inversa $A^{-1}$

$$
A^{-1} A = AA^{-1} = I
$$

```python
A**(-1)
```

```python
sp.simplify(B**(-1))
```

## Algunas matrices especiales

Matrix identidad $I$

$$
IA = AI = A
$$

```python
sp.eye(3)
```

Matriz de ceros

```python
sp.zeros(3)
```

Matriz de unos

```python
sp.ones(3)
```

Matriz diagonal

```python
sp.diag(-3, 1, 6)
```

También es posible incluir variables en una matriz.

```python
x, y = sp.symbols('x, y')

C = sp.Matrix([[-x, -y, x],
               [x**2, 0, sp.exp(-x*y)],
               [y, 2*x + y, 0]])

C
```

```python
A + C
```

```python
C.det()
```

```python
sp.simplify(C**(-1))
```

## Ejercicio 1: Matriz de rotación (1)

1. Definir la matriz de rotación alrededor del eje $z$

$$
R =
\begin{pmatrix}
\cos \theta & -\sin \theta & 0 \\
\sin \theta & \cos \theta & 0 \\
0 & 0 & 1
\end{pmatrix}
$$

2. Verificar que $\det R = 1$
3. Hallar $R^{-1}$.
4. Hallar $R^T$ y verificar que $R^T R = 1$. Esto significa que $R$ es una matriz ortogonal.

```python
#solucion
```

## Autovalores y autovectores

En general, al multiplicar una matriz $A$ por un vector $\mathbf{v}$, se obtiene un vector completamente nuevo. Sin embargo, existen ciertos vectores especiales que cambian solo por una constante

$$
A \mathbf{v} = \lambda \mathbf{v}
$$

A los vectores $\mathbf{v}$ que cumplen esta relación, se les denomina como los __autovectores__ de $A$, mientras que a los respectivos escalares $\lambda$, como sus __autovalores__.

El cálculo de los autovalores y autovectores de una matriz es muy importante para hallar por ejemplo, las energías de un sistema cuántico o los modos normales de vibración de un sistema de osciladores, como veremos más adelante.

Autovalores

```python
A.eigenvals()
```

La función $\texttt{eigenvals()}$ nos da los autovalores de la matriz, así como el número de veces que cada uno se repite, lo que se conoce como _multiplicidad_.

Es posible convertir un número expresado de manera simbólica a su forma decimal.

```python
B_eigenvals = list(B.eigenvals().keys())

for i in B_eigenvals:
    display(sp.N(i))
```

Autovectores

```python
A.eigenvects()
```

La función $\texttt{eigenvals()}$ nos da los autovectores de la matriz, así como su respectivo autovalor y multiplicidad.

Si queremos seleccionar un autovector en particular

```python
A.eigenvects()[0][2][0]
```

## Ejercicio 2: Matriz de rotación (2)

1. Hallar los autovalores de $R$.
2. Hallar los autovectores de $R$.
3. Dar una interpretación física a estos resultados.

```python
#solucion
```

# Problema 1: Autovalores de energía

_Referencia:_  Zetilli, Quantum Mechanics, Problema 3.6 (p. 197)

Los posibles valores de energía de un sistema están dados por la ecuación de Schrödinger

$$
\hat{H}|\psi\rangle= E|\psi\rangle,
$$

donde $\hat{H}$ es el hamiltoniano del sistema. Es decir, las posibles energías del sistema vienen a ser los autovalores del Hamiltoniano.

Para este problema, consideremos el hamiltoniano

$$
\hat{H} = E_0
\begin{pmatrix}
0 & i & 0 \\
-i & 0 & 0 \\
0 & 0 & -1
\end{pmatrix}
$$

1. Definir el hamiltoniano $\hat{H}$.
2. Verificar que el hamiltoniano es hermítico
$$
\hat{H} = \hat{H}^\dagger
$$
3. Hallar los posibles valores de energía del sistema.
4. Hallar los autovectores de $\hat{H}$ y verificar para uno de estos que se cumple la ecuación de Schrödinger.

Consideremos ahora que nuestro sistema se encuentra inicialmente en un estado

$$
|\psi_0 \rangle = \frac{1}{\sqrt{5}}
\begin{pmatrix}
1 - i \\
1 - i \\
1
\end{pmatrix}
$$

donde cada componente del vector indica la amplitud de probabilidad de medir cada valor de energía.

El valor esperado de la energía para un sistema dado está definido como

$$
\langle E \rangle = \langle \psi_0|\hat{H}|\psi_0 \rangle
$$

donde el bra $\langle \psi_0|$ es la adjunta del ket $|\psi_0 \rangle$.

5. Verificar que el estado $|\psi_0\rangle $ esta normalizado
$$
\langle \psi_0|\psi_0 \rangle  = 1
$$
6. Hallar el valor esperado de la energía para el sistema. _Nota:_ esto se puede calcular como una multiplicación de matrices.

# Matrices númericas

Para definir una matriz de manera numérica usamos el paquete $\texttt{numpy}$. En este paquete, las matrices (así como cualquier otro arreglo de números) se definen usando el comando $\texttt{array}$.

```python
import numpy as np
```

```python
A_num = np.array([[-2, -4, 2],
                  [-2, 1, 2],
                  [4, 2, 5]])

A_num
```

    array([[-2, -4,  2],
           [-2,  1,  2],
           [ 4,  2,  5]])

```python
B_num = np.array([[1, 4, 3],
                  [0, -1, 2],
                  [1, 2, 5]])

B_num
```

    array([[ 1,  4,  3],
           [ 0, -1,  2],
           [ 1,  2,  5]])

Todas las operaciones algebraicas que se realizaron en $\texttt{sympy}$, también se pueden realizar en $\texttt{numpy}$.

```python
A_num + B_num
```

    array([[-1,  0,  5],
           [-2,  0,  4],
           [ 5,  4, 10]])

Hay q tener cuidado con la multiplicación. Si multiplicamos las matrices de la misma manera que en $\texttt{sympy}$, esta será elemento por elemento.

```python
A_num*B_num
```

    array([[ -2., -16.,   6.],
           [ -0.,  -1.,   4.],
           [  4.,   4.,  25.]])

Multiplicación de matrices

```python
np.dot(A_num, B_num)
```

    array([[ 0,  0, -4],
           [ 0, -5,  6],
           [ 9, 24, 41]])

Para realizar las operaciones propias de las matrices como obtener la matriz adjunta o inversa, se tiene que usar el paquete $\textt{linalg}$ que está dentro de $\texttt{numpy}$.

```python
np.linalg.det(A_num)
```

```python
np.linalg.inv(A_num)
```

    array([[-1.11111111e-02, -2.66666667e-01,  1.11111111e-01],
           [-2.00000000e-01,  2.00000000e-01, -9.25185854e-18],
           [ 8.88888889e-02,  1.33333333e-01,  1.11111111e-01]])

Autovalores y autovectores

```python
np.linalg.eig(A_num)
```

    (array([-5.,  3.,  6.]), array([[ 0.81649658,  0.53452248,  0.05842062],
            [ 0.40824829, -0.80178373,  0.35052374],
            [-0.40824829, -0.26726124,  0.93472998]]))

Los autovectores hallados están normalizados, es decir, su módulo es igual a 1.

# Problema 2: Autovalores de energía

_Referencia:_ Zetilli, Quantum Mechanics, Problema 3.11 (p. 204)

Al igual que en el problema 1, consideremos un sistema cuyo estado inicial y hamiltoniano son respectivamente

$$
|\psi_0 \rangle = \frac{1}{5}
\begin{pmatrix}
3 \\
0 \\
4
\end{pmatrix}
\quad
\hat{H} =
\begin{pmatrix}
3 & 0 & 0 \\
0 & 0 & 5 \\
0 & 5 & 0
\end{pmatrix}
$$

Repetir los cálculos del problema 1, pero trabajandolo ahora de manera numérica.

```python
#solucion
```


# Sistemas de ecuaciones lineales

...pero antes
------------

Antes de empezar con el tema de la práctica de hoy, quiero hacer notar un detalle que solo he hecho notar teoricamente, pero del cual no he ofrecido evidencia empirica, empecemos nombrando algunos métodos numéricos que podemos utilizar, por ejemplo, para obtener las raices de un polinomio, podemos utilizar la función ```roots```:

Antes que nada, definimos el polinomio del cual queremos obtener las raices reales:

```python
f = lambda x: x**2 - 2*x + 1
```

e importamos la función ```roots``` de la libreria ```numpy```:

```python
from numpy import roots
```

Si no estamos seguros de como utilizar la función, podemos obtener la documentación rapidamente al postponer un signo de interrogación, ?, al nombre de la función y ejecutarel código; este aparecera en una ventana en la parte inferior y se puede cerrar en cuanto hayas terminado de leerla o reducir el espacio que ocupa en tu navegador:

```python
roots?
```

De la documentación podemos ver que la forma de utilizarla es dandole un arreglo de numeros, con los coeficientes del polinomio:

```python
roots([1, -2, 1])
```

y de la misma manera, podemos asegurarnos de que estas raices son adecuadas, al sustituir este valor en la función y comprobar que el valor que nos devuelve es $0$:

```python
f(1)
```

Ahora quiero hacer notar que podemos revisar cuanto tiempo le toma a la computadora hacer esta operación, al utilizar la función ```%%timeit``` al inicio de la celda de código, el motor de Jupyter correra la función $1000$ veces y nos reportará el tiempo promedio que le tomó hacer esta operación:

```python
%%timeit
rs = roots([1, -2, 1])
```

Sin embargo esta función no puede darnos la solución para ecuaciones no lineales, para esto podemos utilizar ```fsolve```, primero definamos una ecuación no lineal:

```python
from numpy import sin
```

```python
g = lambda x: sin(x) + 1
```

importamos la función ```fsolve``` de la libreria de optimización de ```scipy```:

```python
from scipy.optimize import fsolve
```

y la utilizamos dandole la función a resolver, y la aproximación o valor inicial:

```python
fsolve(g, 0)
```

Cabe notar que a esta función podemos darle mas de una aproximación inicial y me dará el resultado de el algoritmo para cada una de las aproximaciones lineales:

```python
fsolve(g, [0, 5, 10, 15])
```

Este metodo tambien funcionará para ecuaciones polinómicas:

```python
fsolve(f, 0)
```

Sin embargo va a ser considerablemente mas lento que las funciones dedicadas para ello:

```python
%%timeit
rs = fsolve(f, 0)
```

La función ```roots```, utiliza un algoritmo diferente basado en el binomio de Newton, por lo que solo sirve para las ecuación polinomiales, y la función ```fsolve``` utiliza métodos numéricos parecidos a los que hemos estado estudiando, pero de una sofisticación mayor, por lo que la moraleja de esta historia es:

>Las funciones programadas en las librerías van a ser mas rápidas siempre, pero hay que saber cuando utilizar cada una y cuando pueden fallar.

Matrices
-------

Dentro de la libreria de calculo numérico ```numpy```, existe una definición que utilizaremos para utilizar matrices como elementos de nuestro ambiente de programación, primero tenemos que importar la definición de ```matrix```:

```python
from numpy import matrix
```

para utilizarla, tengo que darle una lista que contenga una lista por cada fila de la matriz a definir, empecemos definiendo la matriz para el siguiente sistema de ecuaciones lineales:

$$
\begin{align}
x_1 + 4x_2 + 10x_3 &= 5 \\
2x_1 + 3x_2 + 5x_3 &= 2 \\
x_1 + 5x_2 + 2x_3 &= 10
\end{align}
$$

para el cual, podemos definir $A$ y $b$, de tal manera que:

$$
Ax = b
$$

en donde:

$$
x=
\begin{pmatrix}
x_1 \\ x_2 \\ x_3
\end{pmatrix} \quad
A=
\begin{pmatrix}
1 & 4 & 10 \\
2 & 3 & 5 \\
1 & 5 & 2
\end{pmatrix} \quad
b =
\begin{pmatrix}
5 \\ 2 \\ 10
\end{pmatrix}
$$

```python
A = matrix([[1, 4, 10],
            [2, 3, 5],
            [1, 5, 2]])

b = matrix([[5],
            [2],
            [10]])
```

Por lo que la solución mas simple, en terminos de código, es calcular la matriz inversa y multiplicarla por el vector $b$

```python
A.I
```

```python
x = A.I*b
x
```

Sin embargo, ya que hablamos de la rapidez de los métodos numéricos, chequemos la velocidad de esta solución:

```python
%%timeit
x = A.I*b
```

En este caso, tenemos un método alternativo el cual utiliza algoritmos iterativos para el cálculo de esta solución; primero tenemos que importar la función ```solve``` de la librería de algebra lineal de ```numpy```:

```python
from numpy.linalg import solve
```

y se utiliza dandole la matriz $A$ y el vector $b$:

```python
solve(A, b)
```

al revisar el tiempo que tomará esta solución, podemos ver una mejoria significativa:

```python
%%timeit
xs = solve(A, b)
```

Por ultimo, veamos el método de Cramer, podemos implementarlo facilmente, importando la función para calcular el determinante de una matriz:

```python
from numpy.linalg import det
```

```python
det(A)
```

de aqui sabemos que el determinante general tiene un valor no nulo, tambien necesitaremos sustituir el valor del vector $b$ en columnas especificas de la matriz $A$, haremos esto obteniendo la columna especifica y apilandolas horizontalmente, de ahi el nombre de la función que necesitamos ```hstack``` (horizontal stack):

```python
from numpy import hstack
```

La primer matriz a obtener es una en la cual la primer columna sea $b$:

```python
A1 = hstack((b, A[:,1], A[:,2]))
A1
```

la segunda en donde la segunda columna sea $b$:

```python
A2 = hstack((A[:,0], b, A[:,2]))
A2
```

y asi:

```python
A3 = hstack((A[:,0], A[:,1], b))
A3
```

con lo que solo tenemos que calcular el determinante de cada una de estas matrices entre el determinante general:

```python
%%timeit
x1 = det(A1)/det(A)
x2 = det(A2)/det(A)
x3 = det(A3)/det(A)
```

```python
x1, x2, x3
```

```python
det([[1,2,1], [4,5,10], [8, -1, 2]])
```

y como puedes ver, obtenemos los mismos valores que en los otros dos métodos:

Problemas
=====

1. Implementa una función, que dadas la matrix $A$ y el vector $b$, obtenga la solución de un sistema de ecuaciones lineal (utiliza el método de Cramer).
2. Obten la solución de el siguiente sistema de ecuaciones lineal:
$$
\begin{align}
x_1 + 2x_2 + 1x_3 &= 2 \\
4x_1 + 5x_2 + 10x_3 &= -1 \\
8x_1 - x_2 + 2x_3 &= 10
\end{align}
$$
3. La solución obtenida con este método, ¿Es más rápida que el método de la inversa? ¿Es más rápida que la función ```solve```? Implementa el código para checar estos datos.
4. (Opcional) ¿Que problemas puede tener el método de Cramer? ¿Que código es necesario agregar para checar este problema?

Practicas/P3/Practica 3 - Sistemas de ecuaciones lineales.ipynb


Pipeline Burst thickness calculation (ASME B31.8, ASME B31.4).

for D/t >= 30:

for D/t < 30:

```python
from pipedata import*     #imports all input data from the module pipedata.py
import sympy as sp
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# Range of water depth
WD = np.linspace(WD_min,WD_max,100)

# Derated yield stress
def fy(Td):
    if Td < 121:
        fy = 1
    elif 121 < Td <= 149:
        fy = (Td -121)*(-0.033/28) + 1
    elif 149 < Td <= 177:
        fy = (Td -121)*(-0.033/28) + 0.967
    elif 177 < Td <= 204:
        fy = (Td -177)*(-0.033/27) + 0.933
    else:
        fy = (Td -204)*(-0.033/28) + 0.9
    return fy*SMYS

# Allowable hoop stress
def Sh():
    return fy(Td) * f_hs

# Internal pressure, Pa
def Pi(WD):
    return (Pd + rho_cont*g*WD)

# External pressure, Pa
def Pe(WD):
    return (rho_sw*g*WD)

# Differential pressure , Pa
def P_diff(WD):
    return Pi(WD) - Pe(WD)

# BURST thickness calculation
def thick(WD):
    t = sp.symbols('t')
    eq1 = ((P_diff(WD) * (D-t))/(2*t))
    eq2 = ((P_diff(WD) * D)/(2*t))

    sol1 = sp.solveset(sp.Eq(eq1,Sh()),t).args[0]
    sol2 = sp.solveset(sp.Eq(eq2,Sh()),t).args[0]

    eq1 =  sp.lambdify(t,sol1,'numpy')
    eq2 =  sp.lambdify(t,sol2,'numpy')

    if (D/eq1(t))<30:
        t = eq1(t) + CA
    else:
        t = eq2(t) + CA

    return t*1000                        # thickness in mm

# thickness based on range of water depth
tb = list(round(thick(WD),6) for WD in np.linspace(WD_min,WD_max,100))
t_max = max(tb)
t_min = min(tb)
print('maximum thickness=',round(t_max,3),'mm')
print('minimum thickness=',round(t_min,4),'mm')

#Write result to file
def write_to_file():
    pd.set_option('display.max_rows', 200) # increase no.of rows to a maximum of 200
    data = pd.DataFrame({'Water depth[m]':np.round(WD,2),'Thickness[mm]':np.round(tb,2)})
    print(data)
    file = open('Burst_result.txt','w')
    file.write(str(data))
    return file.close()
write_to_file()

# Plots
def plots():
    # plot water depth and external pressure
    plt.plot(WD,Pe(WD)/10**6,label='external pressure',color='green')
    plt.title('WATER DEPTH vs.EXTERNAL PRESSURE')
    plt.xlabel('Water Depth [m]')
    plt.ylabel('Pressure [MPa]')
    plt.savefig('fig1.Water depth vs.External pressure.png',dpi=100)
    plt.legend()
    plt.show()

    # plot water depth and internal pressure
    plt.plot(WD,Pi(WD)/10**6,label='internal pressure')
    plt.title('INTERNAL PRESSURE VS. WATER DEPTH')
    plt.xlabel('Water Depth [m]')
    plt.ylabel('Pressure [MPa]')
    plt.savefig('fig2.Water depth vs.Internal pressure.png',dpi=100)
    plt.legend()
    plt.show()

    # plot water depth and differential pressure
    plt.plot(WD,P_diff(WD)/10**6,label='differential pressure',color='r')
    plt.title('WATER DEPTH vs.DIFFERENTIAL PRESSURE')
    plt.xlabel('Water Depth [m]')
    plt.ylabel('Pressure [MPa]')
    plt.savefig('fig3.Water depth vs.Differential pressure.png',dpi=100)
    plt.legend()
    plt.show()

    # plot Water depth and thickness
    plt.plot(WD,tb,label='wall thickness',color='r')
    plt.title('WATER DEPTH VS.WALL THICKNESS')
    plt.xlabel('Water Depth [m]')
    plt.ylabel('Thickness [m]')
    plt.savefig('fig4. Water depth vs. Burst thickness.png',dpi=100)
    plt.legend()
    plt.show()

    # plot thickness and internal pressure
    plt.plot(tb,Pi(WD)/10**6,label='wall thickness',color='r')
    plt.title('WALL THICKNESS vs.INTERNAL PRESSURE ')
    plt.xlabel('Thickness [m]')
    plt.ylabel('Internal pressure [MPa]')
    plt.savefig('fig5.Thickness vs.Internal pressure.png',dpi=100)
    plt.legend()
    plt.show()
plots()
```


```python
import sympy as sym
import numpy as np
import matplotlib.pyplot as plt
from IPython.display import display,Math
```

# Calculus BUG HUNT!!

```python
# evaluate a function in a range
from sympy.abc import x

fx = (4*x**3 + 2*x**2 - x) / (-4*x**4 + 2*x**2)

xrange = np.linspace(-2,2,200)
fxx = sym.lambdify(x,fx)

plt.plot(xrange,fxx)
plt.ylim([-20,20])
plt.xlim(xrange[[0,-1]])
plt.show()
```

```python
# compute the limit
x = sym.symbols('x')
fx = 1/(x+3)

lim_pnt = -3
lim = sym.limit(x,fx,lim_pnt,dir='+')

display(Math('\\lim_{x\\to %g^+} %s = %g' %(lim_pnt,sym.latex(fx),sym.latex(lim))))
```

```python
# piece-wise function
from sympy.abc import x

piece1 = x**2
piece2 = 4*sym.exp(-x**2)

# put them together with conditions
fx = sym.Piecewise( piece1,x<0 , [piece2,x>=0) )

# evaluate the function in a range
xx = np.linspace(-2,2,1000)
fxx = sym.lambdify(x,fx)

# show it in a plot
plt.plot(xx,fxx(x),'k')

plt.show()
```

```python
# show the first and second derivatives of sin(x)
x = np.linspace(-2*np.pi,2*np.pi,200)
dt = np.diff(x[0:2])

y = np.sin(x)
dy = np.diff(y)
ddy = np.diff(y,2)

plt.plot(x,y,label='y')
plt.plot(x,dy,'--',label='dy',alpha=.6)
plt.plot(x,ddy,':',label='d$^2$y',alpha=.3)

plt.legend(framealpha=1)
plt.show()
```

```python
# Compute critical points using sympy

x = sym.symbols('x')
fx = x**2 * sym.exp(-x**2)

# derivative in sympy, solve
dfx = sym.diff(fx,x)
critpoints = sym.solve(fx)
print('The critical points are: ' + str(critpoints))

# plot the function derivative and its critical points
y = sym.lambdify(x,dfx)
xx = np.linspace(-3,3,200)

plt.plot(xx,y(xx))
plt.plot([-3,3],[0,0],'k--')
plt.xlim([-3,3])

for i in critpoints:
    plt.plot(i,0,'ro')

plt.title('Function derivative')
plt.show()
```

```python
# Compute the area between two curves (not the same thing as Between Two Ferns)
from matplotlib.patches import Polygon

x = sym.symbols('x')
f1sym = sym.cos(x)
f2sym = x

xx = np.linspace(0,np.pi/3,100)
f1 = np.cos(xx)
f2 = xx

fintersect = np.argmin(abs(f1-f2))

traceX = np.concatenate((xx[0:fintersect],xx[fintersect:0:-1]))
traceY = np.concatenate((f1[0:fintersect],f2[fintersect:0:-1]))

points = np.vstack((traceX,traceY)).T
p = Polygon(points,facecolor='k',alpha=.3)

fig, ax = plt.subplots()
ax.add_patch(p)

plt.plot(xx,f1, xx,f2)
plt.show()

```


```python
from sympy.physics.mechanics import *
import sympy as sp
mechanics_printing(pretty_print=True)
```

```python
m1,m2,l1,l2 = sp.symbols(r'm_1,m_2,l_1,l_2')
t,g,h = sp.symbols('t,g,h')
v1, v2 = dynamicsymbols(r'\theta_1 \theta_2')
dv1, dv2 = dynamicsymbols(r'\theta_1 \theta_2', 1)
```

```python
x1 = l1*sp.sin(v1)
y1 = -l1*sp.cos(v1)
x2 = x1 + l2*sp.sin(v2)
y2 = y1 + -l2*sp.cos(v2)

dx1 = x1.diff(t)
dy1 = y1.diff(t)
dx2 = x2.diff(t)
dy2 = y2.diff(t)
```

```python
V = m1*g*y1 + m2*g*y2
T = sp.Rational(1, 2)*m1*(dx1**2+dy1**2)+sp.Rational(1, 2)*m2*(dx2**2+dy2**2)
L = T - V
```

```python
LM = LagrangesMethod(L, [v1, v2])
```

```python
soln = LM.form_lagranges_equations()
```

```python
solvedsoln = sp.solve((sp.Eq(soln[0]),sp.Eq(soln[1])),(v1.diff(t,t),v2.diff(t,t)))
```

```python
solvedsoln
```

```python

```


# Algorithms Exercise 2

## Imports

```python
%matplotlib inline
from matplotlib import pyplot as plt
import seaborn as sns
import numpy as np
```

## Peak finding

Write a function `find_peaks` that finds and returns the indices of the local maxima in a sequence. Your function should:

* Properly handle local maxima at the endpoints of the input array.
* Return a Numpy array of integer indices.
* Handle any Python iterable as input.

```python
def find_peaks(a):
    """Find the indices of the local maxima in a sequence."""
    peaks = []
    data = np.array(a)
    deriv = np.diff(data)
    if deriv[0] < 0:
        peaks.append(0)
    for i in range(1,len(deriv)):
        if deriv[i]<0 and deriv[i-1]>0:
            peaks.append(i)
    if deriv[-1] >0:
        peaks.append(len(data)-1)
    return np.array(peaks)
```

```python
p1 = find_peaks([2,0,1,0,2,0,1])
assert np.allclose(p1, np.array([0,2,4,6]))
p2 = find_peaks(np.array([0,1,2,3]))
assert np.allclose(p2, np.array([3]))
p3 = find_peaks([3,2,1,0])
assert np.allclose(p3, np.array([0]))
```

Here is a string with the first 10000 digits of $\pi$ (after the decimal). Write code to perform the following:

* Convert that string to a Numpy array of integers.
* Find the indices of the local maxima in the digits of $\pi$.
* Use `np.diff` to find the distances between consequtive local maxima.
* Visualize that distribution using an appropriately customized histogram.

```python
from sympy import pi, N
pi_digits_str = str(N(pi, 10001))[2:]
pi_dig_num=np.array(list(pi_digits_str))
pi_dig_num=pi_dig_num.astype(int)
peak=find_peaks(pi_dig_num)
peakdiff=np.diff(peak)
```

    [1 4 1 ..., 6 7 8]

```python
plt.hist(peakdiff, bins=100, width=1, color='k',edgecolor='b', align='right');
plt.xlabel("Distance between adjacent local maxima in pi")
plt.ylabel("Counts")
plt.title("Distance between adjacent local maxima in pi");
```

```python
assert True # use this for grading the pi digits histogram
```

```python

```


Determine the transformation of an aperture diameter of 7.5 kpc at the redshift of abell 370 to pixels. First transform the diameter in absolute size (kpc) to apparent size (arcseconds). Then transform apparent size to pixels using WCS.

```python
import numpy as np

from astropy.io import fits
from astropy import units as u
from astropy import cosmology
from astropy.wcs import WCS
```

### Load Data

```python
input_data_path = "https://archive.stsci.edu/pub/hlsp/frontier/abell2744/images/hst/v1.0-epoch1/hlsp_frontier_hst_wfc3-60mas_abell2744_f105w_v1.0_drz.fits"

hdul = fits.open(input_data_path)
hdu = hdul[0]

data = hdu.data
header = hdu.header
wcs = WCS(header)
```

### Given

```python
# Given
aperture_diameter = (7.5 * u.kpc).to(u.Mpc)
```

NED data for abell 2744:

http://ned.ipac.caltech.edu/byname?objname=Abell%202744&hconst=70.0&omegam=0.279&omegav=0.721&wmap=3&corr_z=1

```python
"""
Cosmology-Corrected Quantities [H0 = 70.0 km/sec/Mpc, Ωmatter = 0.279, Ωvacuum = 0.721]
[Redshift 0.307055 as corrected to the Reference Frame defined by the 3K CMB]
Type	Values
Luminosity Distance	1.6e+03 Mpc (m-M) = 41.02 mag
Angular-Size Distance	938 (m-M) = 39.86 mag
Co-Moving Radial Distance	1.23e+03 (m-M) = 40.44 mag
Co-Moving Tangential Distance	1.23e+03 (m-M) = 40.44 mag
Co-Moving Volume	7.72 Gpc^3
Light Travel-Time	3.499 Gyr
Age at Redshift 0.307055	10.245 Gyr
Age of Universe	13.744 Gyr
Scale (Cosmology Corrected)	4548 pc/arcsec = 4.548 kpc/arcsec = 272.85 kpc/arcmin = 16.37 Mpc/degree
Surface Brightness Dimming	Flux Density per Unit Area = 0.34263; Magnitude per Unit Area = 1.163 mag
""";
```

```python
abell2744_z =  0.30800
abell2744_distance = 1230 * u.Mpc # Comoving
```

### Equations

\begin{align}
\theta = \frac{(z+1) * Diameter}{Distance_{Comoving}} =  \frac{Diameter}{Distance_{Angular Diameter} } \\
\end{align}

### Compute angular diameter distance

```python
def compute_angular_diameter_distance(d, z):
    return d/(z+1)
```

```python
angular_diameter_distance = compute_angular_diameter_distance(abell2744_distance, abell2744_z)
angular_diameter_distance
```

$940.36697 \; \mathrm{Mpc}$

```python
# check astropy cosmology
cosmology.WMAP5.angular_diameter_distance(abell2744_z)
```

$937.71585 \; \mathrm{Mpc}$

### Find angular diameter

```python
angular_diameter = np.tan((aperture_diameter / angular_diameter_distance).value) * u.rad
angular_diameter
```

$7.9756098 \times 10^{-6} \; \mathrm{rad}$

```python
# check astropy cosmology
(cosmology.WMAP5.arcsec_per_kpc_proper(abell2744_z) * aperture_diameter.to('kpc')).to('rad')
```

$7.9981585 \times 10^{-6} \; \mathrm{rad}$

### Convert to arcsec

```python
angular_diameter = 0.7*u.arcsec#angular_diameter.to("arcsec")
angular_diameter
```

$0.7 \; \mathrm{{}^{\prime\prime}}$

### Find Pixel Size

In this section we convert the angular size into pixel size. The way we do this is by adding the angular size to the `CRVAL` of the WCS and then converting that world coordinate to pixel values. After getting the new pixel values we subtract the `CRPIX` values to find the difference (angular size). Because RA-DEC is an equatorial coordinate system, I have decided its best to add the angular size to the Dec component of the WCS CRVAL (Dec axis is a great circle). It looks like the image `CRVAL` is sufficiently away from the pols of the celestial sphere, so no need to worry about loops around +/- 90 degrees.

```python
# Load center pixel from data's WCS

ra_0 = wcs.wcs.crval[0] * u.Unit(wcs.wcs.cunit[0])
dec_0 = wcs.wcs.crval[1] * u.Unit(wcs.wcs.cunit[1])

print(ra_0, ",",  dec_0)
```

    3.5832679 deg , -30.3880082 deg

```python
# Add angular_diameter to center Dec value

ra_1  = ra_0
dec_1 = dec_0 + angular_diameter

print(ra_1, ",",  dec_1)
```

    3.5832679 deg , -30.387813755555555 deg

```python
# Convert ra_1, dec_1 into pixels

world = np.array([[ra_1.value, dec_1.value],], dtype=np.float64) * u.deg
pixcrd = wcs.wcs_world2pix(world, 1)

pixcrd
```

    array([[2675.00000008, 3086.66666198]])

```python
# Look at the center pixel values
wcs.wcs.crpix
```

    array([2675., 3075.])

```python
# Find the difference b/w the center pixel and the angular offset pixel:
pixel_diff =  pixcrd - wcs.wcs.crpix
assert abs(pixel_diff[0,0]) < 1e-6
pixel_diff[0,1]
```

    11.666661982622372

```python
# Find ceiling of pixel diff to conclude pixel size
pixel_size = int(np.ceil(pixel_diff[0,1]))

pixel_size
```

    12

```python
# radius
pixel_size // 2
```

    6

```python

```

notebooks/cosmology/Aperture Diameter of 7.5 kpc at Abell 2744.ipynb


## Numerical Methods:

1. Integration
4. Differential Equations

### Integration [source](https://docs.scipy.org/doc/scipy/reference/tutorial/integrate.html)

```python
import numpy as np
from scipy.integrate import quad, dblquad, tplquad
```

$ y = mx +c$

```python
def f(x):
    return 5*x + 4

x_lower = 0 # the lower limit of x
x_upper = 1 # the upper limit of x

val, abserr = quad(f, x_lower, x_upper)
print("integral value =", val, ", absolute error =", abserr)
```

    integral value = 6.499999999999999 , absolute error = 7.216449660063516e-14

* Bessel function

```python
from scipy.special import jn, yn, jn_zeros, yn_zeros
```

```python
def integrand(x, n):

    """
    Bessel function of first kind and order n.
    """
    return jn(n, x)

x_lower = 0 # the lower limit of x
x_upper = 10 # the upper limit of x
val, abserr = quad(integrand, x_lower, x_upper, args=(3,))

print(val, abserr)
```

    0.7366751370811073 9.389126882496403e-13

* Gaussian function

```python
val, abserr = quad(lambda x: np.exp(-x ** 2), -5.0, 5.0)
print("numerical =", val, abserr)
analytical = np.sqrt(np.pi)
print("analytical =", analytical)
```

    numerical = 1.7724538509027912 4.6261378229003154e-14
    analytical = 1.7724538509055159

```python
def integrand(x, y):
    return np.exp(-x**2-y**2)

x_lower = 0
x_upper = 10
y_lower = 0
y_upper = 10

val, abserr = dblquad(integrand, x_lower, x_upper,\
                      lambda x : y_lower, lambda x: y_upper)
print(val, abserr)
```

    0.7853981633974476 1.3753098510218528e-08

### Ordinary Differential Equations [Source](https://docs.scipy.org/doc/scipy/reference/tutorial/integrate.html)

Odent [Source](https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.odeint.html#scipy.integrate.odeint)

```python
from scipy.integrate import odeint, ode
```

```python
def dy(y, t, zeta, w0):

    """
    The right-hand side of the damped oscillator ODE
    """
    x, p = y[0], y[1]
    dx = p
    dp = -2 * zeta * w0 * p - w0**2 * x
    return [dx, dp]
```

```python
# initial state:
y0 = [1.0, 0.0]
```

```python
# time coodinate to solve the ODE for
t = np.linspace(0, 10, 1000)
w0 = 2*np.pi*1.0
```

```python
# solve the ODE problem for three different values of the damping ratio
y1 = odeint(dy, y0, t, args=(0.0, w0)) # undamped
y2 = odeint(dy, y0, t, args=(0.2, w0)) # under damped
y3 = odeint(dy, y0, t, args=(1.0, w0)) # critial damping
y4 = odeint(dy, y0, t, args=(5.0, w0)) # over damped
```

```python
plt.figure(figsize = [10,8])
plt.plot(t, y1[:,0], 'k', label="undamped", linewidth=0.25)
plt.plot(t, y2[:,0], 'r', label="under damped")
plt.plot(t, y3[:,0], 'b', label=r"critical damping")
plt.plot(t, y4[:,0], 'g', label="over damped")
plt.legend();
```

### Partial Differential Equations

#### PDE : Eliptic Equation (Laplace Euation)

$ \nabla^{2} u  + c f(u) = 0 $

$\frac{\partial^{2}u}{\partial^{2}x} + \frac{\partial^{2}u}{\partial^{2}x} + cf(u) = 0$

for $c=1, f(u)=0$ it becomes Laplace Equation

Using Above lattice of finite difference:

$\large{u_{i−1,j}+u_{i+1,j} + u_{i,j−1} + u{i,j+1} − 4u_{i,j}+cf(u_{i,j})=0}$

$u_{i,j}=0;  \forall u \in ∂Ω$

for 4 by 4 lattice : [source](http://folk.ntnu.no/leifh/teaching/tkt4140/._main055.html)

$\begin{equation}
    \left(\begin{array}{ccccccccccccccc}
      -4& 1 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 &0 &0 \\
      1& -4 & 1 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 &0 &0\\
      0& 1 & -4 & 1 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 &0 &0\\
      0& 0 & 1 & -4 & 1 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 &0 &0\\
      0& 0 & 0 & 1 & -4 & 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 &0 &0\\
      1& 0 & 0 & 0 & 0 & -4 & 1 & 0 & 0 & 0 & 1 & 0 & 0 & 0 &0 &0\\
      0& 1 & 0 & 0 & 0 & 1 & -4 & 1 & 0 & 0 & 0 & 1 & 0 & 0 &0 &0\\
      0& 0 & 1 & 0 & 0 & 0 & 1 & -4 & 1 & 0 & 0 & 0 & 1 & 0 &0 &0\\
      0& 0 & 0 & 1 & 0 & 0 & 0 & 1 & -4 & 1 & 0 & 0 & 0 & 1 &0 &0\\
      0& 0 & 0 & 0 & 1 & 0 & 0 & 0 & 1 & -4 & 0 & 0 & 0 & 0 &1 &0\\
      0& 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & -4 & 1 & 0 & 0 &0 &0\\
      0& 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 1 & -4 & 1 & 0 &0 &0\\
      0& 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 1 & -4 & 1 &0 &0\\
      0& 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 1 & -4 &1 &0\\
      0& 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 1 &-4 &1\\
       0& 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 &1 &-4\\
    \end{array}\right)
    \cdot
    \left(\begin{array}{c}
      u_{11}\\
      u_{12}\\
      u_{13}\\
      u_{14}\\
      u_{21}\\
      u_{22}\\
      u_{23}\\
      u_{24}\\
      u_{31}\\
      u_{32}\\
      u_{33}\\
      u_{34}\\
      u_{41}\\
      u_{42}\\
      u_{43}\\
      u_{44}
    \end{array}\right)
    =
    \left(\begin{array}{c}
      0\\
      0\\
      0\\
      0\\
      a\\
      0\\
      0\\
      0\\
      0\\
      b\\
      0\\
      0\\
      0\\
      0\\
      c\\
      0
    \end{array}\right)\end{equation}$

-----

Problem Type : Solve

$\large{Ax = b}$

-------

#### Implementation of [Least square methods](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.least_squares.html#scipy.optimize.least_squares) to solve a $Ax = b$ problem as a optimization problem

```python
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
from scipy.optimize import least_squares
from scipy.sparse import coo_matrix
import seaborn as sns
sns.set()
```

```python
n = 100
c = 1
```

```python
def f(u):
    return u**3

def f_prime(u):
    return 3 * u**2
```

#### Prepare the lattice

```python
def fun(u, n,f, f_prime,c, **kwargs):
    v = np.zeros((n + 2, n + 2))

    #buttom value = 1
    v[n+1,:] = 1
    # top value = 1
    #v[0,:] = 1
    # center value = 1
    #v[int(n/2),int(n/2)] = 1

    u = u.reshape((n, n))
    v[1:-1, 1:-1] = u
    y = v[:-2, 1:-1] + v[2:, 1:-1] + v[1:-1, :-2] + v[1:-1, 2:] - 4 * u + c * f(u)
    return y.ravel()
```

#### Prepare Jacobians

```python
def compute_jac_indices(n):
    i = np.arange(n)
    jj, ii = np.meshgrid(i, i)

    ii = ii.ravel()
    jj = jj.ravel()

    ij = np.arange(n**2)

    jac_rows = [ij]
    jac_cols = [ij]

    mask = ii > 0
    ij_mask = ij[mask]
    jac_rows.append(ij_mask)
    jac_cols.append(ij_mask - n)

    mask = ii < n - 1
    ij_mask = ij[mask]
    jac_rows.append(ij_mask)
    jac_cols.append(ij_mask + n)

    mask = jj > 0
    ij_mask = ij[mask]
    jac_rows.append(ij_mask)
    jac_cols.append(ij_mask - 1)

    mask = jj < n - 1
    ij_mask = ij[mask]
    jac_rows.append(ij_mask)
    jac_cols.append(ij_mask + 1)

    return np.hstack(jac_rows), np.hstack(jac_cols)

```

```python
jac_rows, jac_cols = compute_jac_indices(n)
```

```python
jac_rows, jac_cols
```

    (array([   0,    1,    2, ..., 9996, 9997, 9998]),
     array([   0,    1,    2, ..., 9997, 9998, 9999]))

```python
def jac(u, n,f, f_prime,c, jac_rows=None, jac_cols=None):
    jac_values = np.ones_like(jac_cols, dtype=float)
    jac_values[:n**2] = -4 +  c * f_prime(u)
    return coo_matrix((jac_values, (jac_rows, jac_cols)), shape=(n**2, n**2))

```

```python
u0 = np.ones(n**2) * 0.5
```

#### [Optimization: Least Square](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.least_squares.html#scipy.optimize.least_squares)

```python
res_1 = least_squares(fun, u0, jac=jac, gtol=1e-3, args=(n,f, f_prime,c),\
                      kwargs={'jac_rows': jac_rows, 'jac_cols': jac_cols}, verbose=1)
```

    `gtol` termination condition is satisfied.
    Function evaluations 738, initial cost 1.1562e+02, final cost 6.4462e-01, first-order optimality 9.38e-04.

#### Returned quantities after solution

```python
res_1.x.shape
```

    (10000,)

```python
res_1.x
```

    array([0.00622038, 0.01236634, 0.01833963, ..., 0.01834064, 0.01236645,
           0.00622026])

```python
res_1.fun
```

    array([-0.00014825, -0.00029118, -0.00041394, ..., -0.000414  ,
           -0.00029121, -0.00014826])

```python
res_1.fun.shape
```

    (10000,)

```python
res_1.jac
```

    <10000x10000 sparse matrix of type '<class 'numpy.float64'>'
    	with 49600 stored elements in Compressed Sparse Row format>

#### Plot the solutions

```python
plt.figure(figsize=(16, 5))

plt.subplot(131)
plt.plot(res_1.x)

plt.subplot(132)
plt.imshow(res_1.x.reshape((n, n)),\
           cmap='coolwarm',\
           vmin=-max(abs(res_1.x)),\
           vmax=max(abs(res_1.x)))

plt.colorbar(use_gridspec=True,\
             fraction=0.046,\
             pad=0.04)

plt.subplot(133)
plt.plot(res_1.fun)

plt.tight_layout()
```

### References:
1. http://folk.ntnu.no/leifh/teaching/tkt4140/._main000.html
2. http://folk.ntnu.no/leifh/teaching/tkt4140/._main055.html
3. https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.least_squares.html#scipy.optimize.least_squares


# Demo

This notebook is a demonstration of **```ngen-kutral```** framework [1,2]. This work is based on model described in [3] also detailed in [4,5] among others.

```python
import wildfire # Framework
from demo_extras import * # Extra functions for experiments
import ipywidgets as widgets # Interactive
```

## Mathematical Model

Let $u(\mathbf{x},t)$ be the numerical value of the temperature and $\beta(\mathbf{x}, t)$ the amount of fuel,
both at spatial coordinate $\mathbf{x}=(x,y)$ and at time $t$, where $\mathbf{x}\in\Omega=[x_{min},x_{max}]\times[y_{min},y_{max}]\subseteq\mathbb{R}^2$
and $t\in [0,t_{\text{max}}]$.
Let $\mathbf{v}(\mathbf{x},t) = \mathbf{w}(\mathbf{x}, t)+\nabla T(\mathbf{x})$ the vector field that models the effect of wind and topography, and $f(u, \beta)$ a nonlinear heat source, then the model is defined as,

\begin{equation}
    \begin{array}{rcll}
        u_t &=& \kappa\,\Delta u - \mathbf{v} \cdot \nabla u + f(u, \beta),
            & \textrm{in} ~ \Omega\,\times\, ]0, t_{\text{max}}], \\
        \beta_t &=& g(u, \beta), & \textrm{in} ~ \Omega \, \times \, ]0, t_{\text{max}}], \\
        u(\mathbf{x}, t) &=& h_1(\mathbf{x},t), & \textrm{on} ~ \Gamma \, \times \, ]0, t_{\text{max}}], \\
        \beta(\mathbf{x}, t) &=& h_2(\mathbf{x},t), & \textrm{on} ~ \Gamma \, \times \, ]0, t_{\text{max}}], \\
        u(\mathbf{x}, 0) &=& u_0(\mathbf{x}), & \textrm{in} ~ \Omega, \hfill \\
        \beta(\mathbf{x}, 0) &=& \beta_0(\mathbf{x}), & \textrm{in} ~ \Omega,
    \end{array}
    \tag{1}
\end{equation}

where $\Gamma$ represents the boundary of $\Omega$ and,

\begin{align}
    f(u, \beta) &= H_{pc}(u)\beta \exp\left(\frac{u}{1 + \varepsilon u}\right) - \alpha u \nonumber, \\
    g(u, \beta) &= -H_{pc}(u)\frac{\varepsilon}{q}\beta\exp\left(\frac{u}{1 + \varepsilon u}\right) \nonumber,
\end{align}

with

\begin{equation}
    H_{pc}(u) =
    \begin{cases}
        1, & \text{if} ~ u \geq u_{pc}, \\
        0, & \text{otherwise}.
    \end{cases}
    \nonumber
\end{equation}

In this case the gradient is defined as
$\nabla = \left(\frac{\partial}{\partial x}, \frac{\partial}{\partial y}\right)$
and the Laplace operator $\Delta = \frac{\partial^2}{\partial x^2}+\frac{\partial^2}{\partial y^2}$.
This model is presented in a nondimensional form using the Frank-Kamenetskii change of variable
detailed in [1], so the nondimensional parameters of the model $(1)$ are: diffusion coefficient $\kappa$, the inverse of activation energy of fuel $\varepsilon$,
natural convection $\alpha$, reaction heat $q$ and the phase change threshold $u_{pc}$.

## Difference with Asension-Ferragut 2002

Diffusion term used by [3] is $\operatorname{div}(K(u)\,\nabla u)=\nabla \cdot (K(u)\,\nabla u)$ instead of $\kappa\,\Delta u$, where $K(u)=\bar{\kappa}\,(1 + \varepsilon\, u)^3 + 1$. The diffusion constant coefficient is assumed in works such as [4] and [5].
Expanding $\operatorname{div}(K(u)\,\nabla u)$ we have,

\begin{equation}
    \begin{split}
        \nabla \cdot (K(u)\,\nabla u) &= \left(\frac{\partial}{\partial x}, \frac{\partial}{\partial y}\right)
            \cdot \left(K(u)\,\left(\frac{\partial u}{\partial x}, \frac{\partial u}{\partial y}\right)\right)\\
            &= \left(\frac{\partial}{\partial x}, \frac{\partial}{\partial y}\right)
            \cdot \left(K(u)\frac{\partial u}{\partial x}, K(u)\frac{\partial u}{\partial y}\right)\\
            &= \frac{\partial}{\partial x}\left(K(u)\frac{\partial u}{\partial x}\right) +
               \frac{\partial}{\partial y}\left(K(u)\frac{\partial u}{\partial y}\right) \\
            &=\frac{\partial K(u)}{\partial x}\frac{\partial u}{\partial x} + K(u)\frac{\partial^2 u}{\partial x^2} +
              \frac{\partial K(u)}{\partial y}\frac{\partial u}{\partial y} + K(u)\frac{\partial^2 u}{\partial y^2} \\
            &= \frac{\partial K(u)}{\partial u}\frac{\partial u}{\partial x}\frac{\partial u}{\partial x}
                + \frac{\partial K(u)}{\partial u}\frac{\partial u}{\partial y}\frac{\partial u}{\partial y}
                + K(u)\,\left(\frac{\partial^2 u}{\partial x^2} + \frac{\partial^2 u}{\partial y^2}\right) \\
            &= \frac{\partial K(u)}{\partial u}\left(\frac{\partial u}{\partial x}\right)^2
                + \frac{\partial K(u)}{\partial u}\left(\frac{\partial u}{\partial y}\right)^2
                + K(u)\,\left(\frac{\partial^2 u}{\partial x^2} + \frac{\partial^2 u}{\partial y^2}\right) \\
            &= \frac{\partial K(u)}{\partial u}\left(\left(\frac{\partial u}{\partial x}\right)^2
                + \left(\frac{\partial u}{\partial y}\right)^2 \right) + K(u)\,\Delta u
    \end{split}
\end{equation}

and $\dfrac{\partial K(u)}{\partial u} = 3\bar{\kappa}\varepsilon\,(1+\varepsilon\,u)^2$. To use this model, set the physical parameter ```complete = True```.

## Physical and nondimensional relation

### Variables

* Space and time:

\begin{equation}
    x = \frac{\bar{x}}{l_0}, \quad y = \frac{\bar{y}}{l_0}, \quad t=\frac{\bar{t}}{t_0}
\end{equation}

* Model components:

\begin{equation}
    u = \frac{\bar{u}-u_{\infty}}{\varepsilon u_{\infty}}, \quad \beta = \frac{\bar{\beta}}{\beta_0},
        \quad \mathbf{v} = \frac{t_0}{l_0}\bar{\mathbf{v}}
\end{equation}

$l_0$ is a characteristic length, $t_0$ is characteristic time, $u_{\infty}$ is a reference temperature and $\beta_0$ is a reference fuel. The bar over variables indicates physical magnitude. According to [3,5] $\beta\in[0,1]$.

### Units

|Unit of | Symbol   | Basic SI|
|:------:|:--------:|:-------------------------------------------:|
|Amount of substance|$\text{mol}$||
|Distance|$\text{m}$||
|Energy|$\text{J}$|$\dfrac{\text{kg}\cdot\text{m}^2}{\text{s}^{2}}$|
|Mass|$\text{kg}$||
|Temperature|$\text{K}$||
|Time|$\text{s}$||
| Power  |$\text{W}$|$\dfrac{\text{kg}\cdot\text{m}^2}{\text{s}^{3}}$|

### Constants
* **Universal gas constant**: $R=8.31446261815324\,[\text{J}\cdot\text{K}^{-1} \cdot\text{mol}^{-1}]$.
* **Stefan-Boltzmann constant**: $\sigma = 5.670374419... \times 10^{-8} \,[\text{W}\cdot\text{m}^{-2}\cdot\text{K}^{-4}]$.

### Definitions
* **Thermal conductivity coefficient** $k$, units $[\text{W}\cdot\text{m}^{-1}\cdot\text{K}^{-1}]$.
    - *Wood:* $0.059\,[\text{W}\cdot\text{m}^{-1}\cdot\text{K}^{-1}]$ for maple or oak to $1.17\,[\text{W}\cdot\text{m}^{-1}\cdot\text{K}^{-1}]$ for sawdust.
    - *Air:* $0.024\,[\text{W}\cdot\text{m}^{-1}\cdot\text{K}^{-1}]$
* **Optical path length for radiation through the substance** $\delta$, units $[\text{m}]$.
* **Density** $\rho$, units $[\text{kg}\cdot\text{m}^{-3}]$.
    - *Wood:* Density varies from $420\,[\text{kg}\cdot\text{m}^{-3}]$ for fir to $640\,[\text{kg}\cdot\text{m}^{-3}]$ for yellow pine.
    - *Air:* At atmospheric pressure and ambient temperature $\rho=1.1774\,[\text{kg}\cdot\text{m}^{-3}]$.
* **Specific heat** $C$, units $[\text{m}^2\cdot\text{K}^{-1}\cdot\text{s}^{-2}]$.
    - *Wood:* $2400\,[\text{m}^2\cdot\text{K}^{-1}\cdot\text{s}^{-2}]$ for maple to $2800\,[\text{m}^2\cdot\text{K}^{-1}\cdot\text{s}^{-2}]$ for yellow pine.
    - *Air:* $1005.7\,[\text{m}^2\cdot\text{K}^{-1}\cdot\text{s}^{-2}]$
* **Natural convection coefficient** $h$, units $[\text{W}\cdot\text{m}^{-2}\cdot\text{K}^{-1}]$.
* **Activation Energy** $E_A$, units $[\text{J}\cdot\text{mol}^{-1}]$.
    - Typical values $E_A\approx 83680\,[\text{J}\cdot\text{mol}^{-1}]$.
* **Heat of combustion** $H$, units $[\text{k}\text{J}\cdot\text{kg}^{-1}]$.
    - For cellulose $H=66525.6\,[\text{k}\text{J}\cdot\text{kg}^{-1}]$.
    - Realistic cases may consider smaller values because there are other components in fuel with lower heats of combustion.
* **Pre-exponential factor** $A$, units $[\text{s}^{-1}]$.
    - Admissible value $A=10^9\,[\text{s}^{-1}]$
* **Reference temperature** $u_{\infty}$, units $[\text{K}]$.
    - Typical value $u_{\infty}=300\,[\text{K}]$
* **Phase change temperature**, units $[\text{K}]$.
    - Estimated value $\bar{u}_{pc}=550\,[\text{K}]$.

### Parameters
* **Inverse of conductivity** $\kappa=\dfrac{4\sigma\delta u_{\infty}^3}{k}$.
* **Inverse of activation energy** $\varepsilon = \dfrac{R~u_{\infty}}{E_A}$.
* **Reaction Heat** $q=\dfrac{H\beta_0}{C\,u_{\infty}}$.
* **Natural convection** $\alpha=\dfrac{t_0h}{\rho C}$.
* **Phase change temperature** $u_{pc}=\dfrac{\bar{u}_{pc}-u_{\infty}}{\varepsilon u_{\infty}}$

### Characteristics
* **Time** $t_0=\dfrac{\varepsilon}{qA}\exp\left(\dfrac{1}{\varepsilon}\right)\,[\text{s}]$.
* **Length** $l_0=\sqrt{\dfrac{t_0 k}{\rho C}}\,[\text{m}]$.

#### Used in [3]:

##### Physical
* $\rho=10^2\,[\text{kg}\cdot\text{m}^{-3}]$
* $C=10^3\,[\text{m}^2\cdot\text{K}^{-1}\cdot\text{s}^{-2}]$
* $k=1\,[\text{W}\cdot\text{m}^{-1}\cdot\text{K}^{-1}]$
* $H=66525.6\,[\text{k}\text{J}\cdot\text{kg}^{-1}]$
* $u_{\infty}=300\,[\text{K}]$
* $\bar{u}_{pc}=550\,[\text{K}]$
* $E_A\approx 83680\,[\text{J}\cdot\text{mol}^{-1}]$
* $t_0=8987\,[\text{s}]$
* $l_0=0.3\,[\text{m}]$

##### Non-dimensional
* $\bar{\kappa}=0.1$
* $\varepsilon \approx 0.03$
* $q=1$
* $\alpha=10^{-3}$

Using this values, we may estimate a constant $\kappa$ to replace $K(u)$, $K(u)=\bar{\kappa}\,(1 + \varepsilon\, u)^3 + 1=0.1\,(1+0.03\cdot 300)^3+1\approx 101$.

```python
widget_params_ = widgets.interact_manual(checkParams, k=k_w, delta=delta_w, rho=rho_w,
                                        C=C_w, h=h_w, E_A=E_A_w, H=H_w, A=A_w, U_inf=U_inf_w, B_0=B_0_w)
```

    interactive(children=(FloatText(value=1.0, description='$k$'), FloatText(value=0.01632918494453172, descriptio…

## Physical parameters

Parameters used in our works.

```python
# Physical parameters for the model
physical_parameters = {
    'jcc2018': {
        'kap': 1e-1, # diffusion coefficient
        'eps': 3e-1, # inverse of activation energy
        'upc': 3, # u phase change
        'q': 1, # reaction heat
        'alp': 1e-3, # natural convection,
        # Domain
        'x_lim': (0, 90), # x-axis domain
        'y_lim': (0, 90), # y-axis domain
        't_lim': (0, 30), # time domain
    },
    'jcc2019': {
        'kap': 1e-1, # diffusion coefficient
        'eps': 3e-1, # inverse of activation energy
        'upc': 3, # u phase change
        'q': 1, # reaction heat
        'alp': 1e-3, # natural convection,
        # Domain
        'x_lim': (0, 90), # x-axis domain
        'y_lim': (0, 90), # y-axis domain
        't_lim': (0, 20), # time domain
    },
    'af2002': {
        'kap': 1e-1, # diffusion coefficient
        'eps': 3e-2, # inverse of activation energy
        'upc': 0, # u phase change
        'q': 1.0, # reaction heat
        'alp': 1e-3, # natural convection,
        # Domain
        'x_lim': (0, 300), # x-axis domain
        'y_lim': (0, 300), # y-axis domain
        't_lim': (0, 0.2), # time domain
        'complete': True
    }
}
```

## Numerical Parameters

### Asension & Ferragut 2002 [3]
* Temperature initial condition:

\begin{equation}
    u_0(x,y)=3.4\exp\left(-\dfrac{(x-50)^2 + (y-50)^2}{100}\right)
\end{equation}

* Fuel initial condition:

\begin{equation}
    \beta_0(x,y) \sim \text{Uniform}((0,1)^2)
\end{equation}

* Vector field:

\begin{equation}
    \mathbf{v}(x, y, t) = (300, 300)
\end{equation}

### San Martin & Torres 2018 [1]
* Temperature initial condition:

\begin{equation}
    u_0(x,y)=6\exp\left(-\dfrac{(x-20)^2 + (y-20)^2}{20}\right)
\end{equation}

* Fuel initial condition:

\begin{equation}
    \beta_0(x,y) \sim \text{Uniform}((0,1)^2)
\end{equation}

* Vector field:

\begin{equation}
    \mathbf{v}(x, y, t) = \left(\cos\left(\frac{\pi}{4}\right), \sin\left(\frac{\pi}{4}\right)\right)
\end{equation}

### San Martín & Torres 2019 [2]
* Temperature initial condition:

\begin{equation}
    u_0(x,y)=7\exp\left(-\dfrac{(x-20)^2 + (y-20)^2}{20}\right) + 4\exp\left(-\dfrac{(x-80)^2 + (y-70)^2}{20}\right) + 4\exp\left(-\dfrac{(x-20)^2 + (y-35)^2}{70}\right)
\end{equation}

* Fuel initial condition:

\begin{equation}
    \beta_0(x,y) = 1
\end{equation}

* Vector field:

\begin{equation}
    \begin{split}
        \mathbf{w}(x, y, t) &= \left(\cos\left(\dfrac{\pi}{4}+0.025 t\right),
            \sin\left(\dfrac{\pi}{4} + 0.025 t\right)\right) \\
        T(x,y) &= 1.5\,\bigg[3\exp\left(\dfrac{(x-45)^2+ (y-45)^2}{40}\right)
            + 2\exp\left(\dfrac{(x-30)^2+(y-30)^2}{60}\right) \\
              &\quad + 3\exp\left(\dfrac{(x-70)^2 + (y-70)^2}{60}\right)
                + 2\exp\left(\dfrac{(x-20)^2 + (y-70)^2}{70}\right)\bigg] \\
        \mathbf{v}(x, y, t) &= \mathbf{w}(x, y, t)
            + \left(\dfrac{\partial T(x,y)}{\partial x}, \dfrac{\partial T(x,y)}{\partial y}\right)
    \end{split}
\end{equation}

```python
solve_fun = {
    'af2002': {
        'u0': u0_af2002,
        'b0': b0_af2002,
        'V' : V_af2002
    },
    'jcc2018': {
        'u0': u0_jcc2018,
        'b0': b0_af2002,
        'V' : V_jcc2018
    },
    'jcc2019': {
        'u0': u0_jcc2019,
        'b0': b0_jcc2019,
        'V' : V_jcc2019
    },
}
```

```python
def simulation(experiment, space_method, Nx, Ny, acc, time_method, Nt):
    wildfire_ = wildfire.Fire(**physical_parameters[experiment])
    u0 = solve_fun[experiment]['u0']
    b0 = solve_fun[experiment]['b0']
    V  = solve_fun[experiment]['V']
    t, X, Y, U, B = wildfire_.solvePDE(Nx, Ny, Nt, u0, b0, V, space_method, time_method, last=False, acc=acc)
    time_steps_sl = widgets.IntSlider(value=0, min=0, max=Nt, step=1, continuous_update=False)
    # PHYSICAL VALUES
    t0 = 8987#11067.041552426204
    l0 = 0.3#0.33267163318242515
    U = wildfire_.getTemperature(U)
    B = wildfire_.getFuel(B)
    V = wildfire_.getV(V, l0, t0)
    X, Y = wildfire_.getSpace(X, Y, l0)
    t = wildfire_.getTime(t, t0)
    widgets.interact(showSimulation, t=widgets.fixed(t), X=widgets.fixed(X), Y=widgets.fixed(Y),
        U=widgets.fixed(U), B=widgets.fixed(B), V=widgets.fixed(V), k=time_steps_sl)
```

```python
widget_ = widgets.interact_manual(simulation, experiment=experiment_dp, space_method=space_method_dp,
    Nx=space_nx_text, Ny=space_ny_text, acc=space_acc_dp,
    time_method=time_method_dp_2, Nt=time_nt_text)
```

    interactive(children=(Dropdown(description='Experiment', index=1, options=(('Asensio & Ferragut 2002', 'af2002…

# References

* [1] San Martín, D., & Torres, C. E. (2018). Ngen-Kütral: Toward an Open Source Framework for Chilean Wildfire Spreading. In 2018 37th International Conference of the Chilean Computer Science Society (SCCC) (pp. 1–8). https://doi.org/10.1109/SCCC.2018.8705159
* [2] San Martín, D., & Torres, C. E. (2019). Exploring a Spectral Numerical Algorithm for Solving a Wildfire Mathematical Model. In 2019 38th International Conference of the Chilean Computer Science Society (SCCC) (pp. 1–7). https://doi.org/10.1109/SCCC49216.2019.8966412
* [3] Asensio, M. I., & Ferragut, L. (2002). On a wildland fire model with radiation. International Journal for Numerical Methods in Engineering, 54(1), 137-157. https://doi.org/10.1002/nme.420
* [4] Weber, R. O. (1991). Toward a comprehensive wildfire spread model. International Journal of Wildland Fire, 1(4), 245–248. https://doi.org/10.1071/WF9910245
* [5] Eberle, S., Freeden, W., & Matthes, U. (2015). Forest fire spreading. In Freeden Willi, M. Z. and Nashed, & and Sonar Thomas (Eds.), Handbook of Geomathematics: Second Edition (pp. 1349–1385). Berlin, Heidelberg: Springer Berlin Heidelberg. https://doi.org/10.1007/978-3-642-54551-1_70