```python
#Importando sympy
from sympy import *

#Impresión para "jupyter notebook"
init_printing(use_latex = 'mathjax')
```

```python
#Variables del Hamiltoniano
var('x m hbar')

#Variables del método de variaciones
var('W')
```

$$W$$

```python
#Definición de función: user_variables
def user_variables(yn):
    if yn == 1:
        variable = ''
        a = 0
        print("\nAñada las variables simbólicas que requiera. Cuando haya finalizado, ingrese 'done'.")
        while (variable != 'done'):
            a = a + 1
            variable = input("var_"+str(a)+" = ")
            if variable != 'done':
                var(variable)
```

```python
#Definición de función: Hamiltonian
def Hamiltonian(psi):
    print("\nLa energía cinética del sistema viene definida por: ")
    print("\n-(hbar**2/(2*m))*diff(psi, x, 2)")
    t = -(hbar**2/(2*m))*diff(psi, x, 2)
    print("\nDefina el potencial al que está sometida la partícula. Recuerde que sólo puede ultilizar las variables disponibles.")
    print("\n¿Desea declarar más variables? (0:No; 1:Sí)")
    user_variables(int(input()))
    v = sympify(input("V = "))
    return (t+v*psi)
```

```python
#Añadir variables simbólicas del usuario
print("El programa ya reconoce las variables simbolicas x, m, hbar y W.")
print("\n¿Desea declarar más variables? (0:No; 1:Sí)")
user_variables(int(input()))
```

    El programa ya reconoce las variables simbolicas x, m, hbar y W.

    ¿Desea declarar más variables? (0:No; 1:Sí)
    1

    Añada las variables simbólicas que requiera. Cuando haya finalizado, ingrese 'done'.
    var_1 = l
    var_2 = done

```python
#Ingresar las funciones de base
f = ''
a = 0
print("Introduzca las funciones base, en función de 'x', que requiera. Cuando haya acabado introduzca: 'done'.")
while (f != 'done'):
    a = a + 1
    f = input("f_"+str(a)+" = ")
    if f != 'done':
        if a == 1:
            F = Matrix([sympify(f)])
            C = Matrix([var('c'+str(a))])
        else:
            F = F.col_insert(a-1, Matrix([sympify(f)]))
            C = C.row_insert(a-1, Matrix([[var('c'+str(a))]]))
print("\nFunción variacional lineal:")
print("\nc_k*f_k = \n")
F*C
```

    Introduzca las funciones base, en función de 'x', que requiera. Cuando haya acabado introduzca: 'done'.
    f_1 = x*(l-x)
    f_2 = x**2*(l-x)**2
    f_3 = x*(l-x)*((l/2)-x)
    f_4 = x**2*(l-x)**2*((l/2)-x)
    f_5 = done

    Función variacional lineal:

    c_k*f_k =

$$\left[\begin{matrix}c_{1} x \left(l - x\right) + c_{2} x^{2} \left(l - x\right)^{2} + c_{3} x \left(\frac{l}{2} - x\right) \left(l - x\right) + c_{4} x^{2} \left(\frac{l}{2} - x\right) \left(l - x\right)^{2}\end{matrix}\right]$$

```python
#Calcular integral de solapamiento
var('x1, x2')
S = Integral(F.T*F, (x, x1, x2))
print("\nS_j_k = \n")
S
```

    S_j_k =

$$\int_{x_{1}}^{x_{2}} \left[\begin{matrix}x^{2} \left(l - x\right)^{2} & x^{3} \left(l - x\right)^{3} & x^{2} \left(\frac{l}{2} - x\right) \left(l - x\right)^{2} & x^{3} \left(\frac{l}{2} - x\right) \left(l - x\right)^{3}\\x^{3} \left(l - x\right)^{3} & x^{4} \left(l - x\right)^{4} & x^{3} \left(\frac{l}{2} - x\right) \left(l - x\right)^{3} & x^{4} \left(\frac{l}{2} - x\right) \left(l - x\right)^{4}\\x^{2} \left(\frac{l}{2} - x\right) \left(l - x\right)^{2} & x^{3} \left(\frac{l}{2} - x\right) \left(l - x\right)^{3} & x^{2} \left(\frac{l}{2} - x\right)^{2} \left(l - x\right)^{2} & x^{3} \left(\frac{l}{2} - x\right)^{2} \left(l - x\right)^{3}\\x^{3} \left(\frac{l}{2} - x\right) \left(l - x\right)^{3} & x^{4} \left(\frac{l}{2} - x\right) \left(l - x\right)^{4} & x^{3} \left(\frac{l}{2} - x\right)^{2} \left(l - x\right)^{3} & x^{4} \left(\frac{l}{2} - x\right)^{2} \left(l - x\right)^{4}\end{matrix}\right]\, dx$$

```python
print("Para calcular la integral de solapamiento 'S_j_k' ingrese los límites de integración los mismos límites serán utilizados para calcular la integral variacional 'H_j_k'. Recuerde las variables que tiene a su disposición, incluyendo las que usted declaró.")
x1 = sympify(input("\nx1 = "))
x2 = sympify(input("x2 = "))
S = integrate(F.T*F, (x, x1, x2))
print("\nS = \n")
S
```

    Para calcular la integral de solapamiento 'S_j_k' ingrese los límites de integración los mismos límites serán utilizados para calcular la integral variacional 'H_j_k'. Recuerde las variables que tiene a su disposición, incluyendo las que usted declaró.

    x1 = 0
    x2 = l

    S =

$$\left[\begin{matrix}\frac{l^{5}}{30} & \frac{l^{7}}{140} & 0 & 0\\\frac{l^{7}}{140} & \frac{l^{9}}{630} & 0 & 0\\0 & 0 & \frac{l^{7}}{840} & \frac{l^{9}}{5040}\\0 & 0 & \frac{l^{9}}{5040} & \frac{l^{11}}{27720}\end{matrix}\right]$$

```python
F.T*Hamiltonian(F)
```

    La energía cinética del sistema viene definida por:

    -(hbar**2/(2*m))*diff(psi, x, 2)

    Defina el potencial al que está sometida la partícula. Recuerde que sólo puede ultilizar las variables disponibles.

    ¿Desea declarar más variables? (0:No; 1:Sí)
    0
    V = 0

$$\left[\begin{matrix}\frac{\hbar^{2} x}{m} \left(l - x\right) & - \frac{\hbar^{2} x}{m} \left(l - x\right) \left(x^{2} - 4 x \left(l - x\right) + \left(l - x\right)^{2}\right) & - \frac{3 \hbar^{2} x}{2 m} \left(- l + 2 x\right) \left(l - x\right) & - \frac{\hbar^{2} x}{m} \left(l - x\right) \left(\frac{x^{2}}{2} \left(l - 2 x\right) + 2 x^{2} \left(l - x\right) - 2 x \left(l - 2 x\right) \left(l - x\right) - 2 x \left(l - x\right)^{2} + \frac{1}{2} \left(l - 2 x\right) \left(l - x\right)^{2}\right)\\\frac{\hbar^{2} x^{2}}{m} \left(l - x\right)^{2} & - \frac{\hbar^{2} x^{2}}{m} \left(l - x\right)^{2} \left(x^{2} - 4 x \left(l - x\right) + \left(l - x\right)^{2}\right) & - \frac{3 \hbar^{2} x^{2}}{2 m} \left(- l + 2 x\right) \left(l - x\right)^{2} & - \frac{\hbar^{2} x^{2}}{m} \left(l - x\right)^{2} \left(\frac{x^{2}}{2} \left(l - 2 x\right) + 2 x^{2} \left(l - x\right) - 2 x \left(l - 2 x\right) \left(l - x\right) - 2 x \left(l - x\right)^{2} + \frac{1}{2} \left(l - 2 x\right) \left(l - x\right)^{2}\right)\\\frac{\hbar^{2} x}{m} \left(\frac{l}{2} - x\right) \left(l - x\right) & - \frac{\hbar^{2} x}{m} \left(\frac{l}{2} - x\right) \left(l - x\right) \left(x^{2} - 4 x \left(l - x\right) + \left(l - x\right)^{2}\right) & - \frac{3 \hbar^{2} x}{2 m} \left(- l + 2 x\right) \left(\frac{l}{2} - x\right) \left(l - x\right) & - \frac{\hbar^{2} x}{m} \left(\frac{l}{2} - x\right) \left(l - x\right) \left(\frac{x^{2}}{2} \left(l - 2 x\right) + 2 x^{2} \left(l - x\right) - 2 x \left(l - 2 x\right) \left(l - x\right) - 2 x \left(l - x\right)^{2} + \frac{1}{2} \left(l - 2 x\right) \left(l - x\right)^{2}\right)\\\frac{\hbar^{2} x^{2}}{m} \left(\frac{l}{2} - x\right) \left(l - x\right)^{2} & - \frac{\hbar^{2} x^{2}}{m} \left(\frac{l}{2} - x\right) \left(l - x\right)^{2} \left(x^{2} - 4 x \left(l - x\right) + \left(l - x\right)^{2}\right) & - \frac{3 \hbar^{2} x^{2}}{2 m} \left(- l + 2 x\right) \left(\frac{l}{2} - x\right) \left(l - x\right)^{2} & - \frac{\hbar^{2} x^{2}}{m} \left(\frac{l}{2} - x\right) \left(l - x\right)^{2} \left(\frac{x^{2}}{2} \left(l - 2 x\right) + 2 x^{2} \left(l - x\right) - 2 x \left(l - 2 x\right) \left(l - x\right) - 2 x \left(l - x\right)^{2} + \frac{1}{2} \left(l - 2 x\right) \left(l - x\right)^{2}\right)\end{matrix}\right]$$

```python
#Calcular integral variacional
H = Integral(F.T*Hamiltonian(F), (x, x1, x2))
print("\nH = \n")
H
```

    La energía cinética del sistema viene definida por:

    -(hbar**2/(2*m))*diff(psi, x, 2)

    Defina el potencial al que está sometida la partícula. Recuerde que sólo puede ultilizar las variables disponibles.

    ¿Desea declarar más variables? (0:No; 1:Sí)
    0
    V = 0

    H =

$$\int_{0}^{l} \left[\begin{matrix}\frac{\hbar^{2} x}{m} \left(l - x\right) & - \frac{\hbar^{2} x}{m} \left(l - x\right) \left(x^{2} - 4 x \left(l - x\right) + \left(l - x\right)^{2}\right) & - \frac{3 \hbar^{2} x}{2 m} \left(- l + 2 x\right) \left(l - x\right) & - \frac{\hbar^{2} x}{m} \left(l - x\right) \left(\frac{x^{2}}{2} \left(l - 2 x\right) + 2 x^{2} \left(l - x\right) - 2 x \left(l - 2 x\right) \left(l - x\right) - 2 x \left(l - x\right)^{2} + \frac{1}{2} \left(l - 2 x\right) \left(l - x\right)^{2}\right)\\\frac{\hbar^{2} x^{2}}{m} \left(l - x\right)^{2} & - \frac{\hbar^{2} x^{2}}{m} \left(l - x\right)^{2} \left(x^{2} - 4 x \left(l - x\right) + \left(l - x\right)^{2}\right) & - \frac{3 \hbar^{2} x^{2}}{2 m} \left(- l + 2 x\right) \left(l - x\right)^{2} & - \frac{\hbar^{2} x^{2}}{m} \left(l - x\right)^{2} \left(\frac{x^{2}}{2} \left(l - 2 x\right) + 2 x^{2} \left(l - x\right) - 2 x \left(l - 2 x\right) \left(l - x\right) - 2 x \left(l - x\right)^{2} + \frac{1}{2} \left(l - 2 x\right) \left(l - x\right)^{2}\right)\\\frac{\hbar^{2} x}{m} \left(\frac{l}{2} - x\right) \left(l - x\right) & - \frac{\hbar^{2} x}{m} \left(\frac{l}{2} - x\right) \left(l - x\right) \left(x^{2} - 4 x \left(l - x\right) + \left(l - x\right)^{2}\right) & - \frac{3 \hbar^{2} x}{2 m} \left(- l + 2 x\right) \left(\frac{l}{2} - x\right) \left(l - x\right) & - \frac{\hbar^{2} x}{m} \left(\frac{l}{2} - x\right) \left(l - x\right) \left(\frac{x^{2}}{2} \left(l - 2 x\right) + 2 x^{2} \left(l - x\right) - 2 x \left(l - 2 x\right) \left(l - x\right) - 2 x \left(l - x\right)^{2} + \frac{1}{2} \left(l - 2 x\right) \left(l - x\right)^{2}\right)\\\frac{\hbar^{2} x^{2}}{m} \left(\frac{l}{2} - x\right) \left(l - x\right)^{2} & - \frac{\hbar^{2} x^{2}}{m} \left(\frac{l}{2} - x\right) \left(l - x\right)^{2} \left(x^{2} - 4 x \left(l - x\right) + \left(l - x\right)^{2}\right) & - \frac{3 \hbar^{2} x^{2}}{2 m} \left(- l + 2 x\right) \left(\frac{l}{2} - x\right) \left(l - x\right)^{2} & - \frac{\hbar^{2} x^{2}}{m} \left(\frac{l}{2} - x\right) \left(l - x\right)^{2} \left(\frac{x^{2}}{2} \left(l - 2 x\right) + 2 x^{2} \left(l - x\right) - 2 x \left(l - 2 x\right) \left(l - x\right) - 2 x \left(l - x\right)^{2} + \frac{1}{2} \left(l - 2 x\right) \left(l - x\right)^{2}\right)\end{matrix}\right]\, dx$$

```python
H = H.doit()
print("Se utilizaron los mismos límites de integración que ingresó para la integral de solapamiento.")
print("\nH = \n")
H
```

    Se utilizaron los mismos límites de integración que ingresó para la integral de solapamiento.

    H =

$$\left[\begin{matrix}\frac{\hbar^{2} l^{3}}{6 m} & \frac{\hbar^{2} l^{5}}{30 m} & 0 & 0\\\frac{\hbar^{2} l^{5}}{30 m} & \frac{\hbar^{2} l^{7}}{105 m} & 0 & 0\\0 & 0 & \frac{\hbar^{2} l^{5}}{40 m} & \frac{\hbar^{2} l^{7}}{280 m}\\0 & 0 & \frac{\hbar^{2} l^{7}}{280 m} & \frac{\hbar^{2} l^{9}}{1260 m}\end{matrix}\right]$$

```python
#Calcular energías del sistema
print("\nCalculando energías del sistema...")
M = H-S*W
M
```

    Calculando energías del sistema...

$$\left[\begin{matrix}- \frac{W l^{5}}{30} + \frac{\hbar^{2} l^{3}}{6 m} & - \frac{W l^{7}}{140} + \frac{\hbar^{2} l^{5}}{30 m} & 0 & 0\\- \frac{W l^{7}}{140} + \frac{\hbar^{2} l^{5}}{30 m} & - \frac{W l^{9}}{630} + \frac{\hbar^{2} l^{7}}{105 m} & 0 & 0\\0 & 0 & - \frac{W l^{7}}{840} + \frac{\hbar^{2} l^{5}}{40 m} & - \frac{W l^{9}}{5040} + \frac{\hbar^{2} l^{7}}{280 m}\\0 & 0 & - \frac{W l^{9}}{5040} + \frac{\hbar^{2} l^{7}}{280 m} & - \frac{W l^{11}}{27720} + \frac{\hbar^{2} l^{9}}{1260 m}\end{matrix}\right]$$

```python
polyW = M.det()
polyW
```

$$\frac{W^{4} l^{32}}{147867793920000} - \frac{W^{3} \hbar^{2} l^{30}}{840157920000 m} + \frac{373 W^{2} \hbar^{4} l^{28}}{6161158080000 m^{2}} - \frac{W \hbar^{6} l^{26}}{1047816000 m^{3}} + \frac{\hbar^{8} l^{24}}{296352000 m^{4}}$$

```python
w = solve(polyW, W)
w
```

$$\left [ \frac{2 \hbar^{2}}{l^{2} m} \left(- \sqrt{133} + 14\right), \quad \frac{2 \hbar^{2}}{l^{2} m} \left(\sqrt{133} + 14\right), \quad \frac{2 \hbar^{2}}{l^{2} m} \left(- 9 \sqrt{5} + 30\right), \quad \frac{2 \hbar^{2}}{l^{2} m} \left(9 \sqrt{5} + 30\right)\right ]$$

```python
#Ordenar enegías del sistema de menor a mayor-------------------------------------------------!!!
for a in range(0, len(w)):
    w[a] = w[a]*(m*l**2/hbar**2)
w.sort()
for a in range(0, len(w)):
    w[a] = w[a]*(hbar**2/(m*l**2))
w
```

$$\left [ \frac{\hbar^{2}}{l^{2} m} \left(- 2 \sqrt{133} + 28\right), \quad \frac{\hbar^{2}}{l^{2} m} \left(- 18 \sqrt{5} + 60\right), \quad \frac{\hbar^{2}}{l^{2} m} \left(2 \sqrt{133} + 28\right), \quad \frac{\hbar^{2}}{l^{2} m} \left(18 \sqrt{5} + 60\right)\right ]$$

```python
#Encontrando coeficientes de la función variacional lineal
print("\nCalculando coeficientes de la función variacional lineal...")
Ec = M*C
Ec
```

    Calculando coeficientes de la función variacional lineal...

$$\left[\begin{matrix}c_{1} \left(- \frac{W l^{5}}{30} + \frac{\hbar^{2} l^{3}}{6 m}\right) + c_{2} \left(- \frac{W l^{7}}{140} + \frac{\hbar^{2} l^{5}}{30 m}\right)\\c_{1} \left(- \frac{W l^{7}}{140} + \frac{\hbar^{2} l^{5}}{30 m}\right) + c_{2} \left(- \frac{W l^{9}}{630} + \frac{\hbar^{2} l^{7}}{105 m}\right)\\c_{3} \left(- \frac{W l^{7}}{840} + \frac{\hbar^{2} l^{5}}{40 m}\right) + c_{4} \left(- \frac{W l^{9}}{5040} + \frac{\hbar^{2} l^{7}}{280 m}\right)\\c_{3} \left(- \frac{W l^{9}}{5040} + \frac{\hbar^{2} l^{7}}{280 m}\right) + c_{4} \left(- \frac{W l^{11}}{27720} + \frac{\hbar^{2} l^{9}}{1260 m}\right)\end{matrix}\right]$$

```python
for a in range(1, len(C)+1):
    if a == 1:
        EC = Matrix([Ec.subs(W, w[a-1])])
    else:
        EC = EC.col_insert(a-1, Ec.subs(W, w[a-1]))
EC
```

$$\left[\begin{matrix}c_{1} \left(- \frac{\hbar^{2} l^{3}}{30 m} \left(- 2 \sqrt{133} + 28\right) + \frac{\hbar^{2} l^{3}}{6 m}\right) + c_{2} \left(- \frac{\hbar^{2} l^{5}}{140 m} \left(- 2 \sqrt{133} + 28\right) + \frac{\hbar^{2} l^{5}}{30 m}\right) & c_{1} \left(- \frac{\hbar^{2} l^{3}}{30 m} \left(- 18 \sqrt{5} + 60\right) + \frac{\hbar^{2} l^{3}}{6 m}\right) + c_{2} \left(- \frac{\hbar^{2} l^{5}}{140 m} \left(- 18 \sqrt{5} + 60\right) + \frac{\hbar^{2} l^{5}}{30 m}\right) & c_{1} \left(- \frac{\hbar^{2} l^{3}}{30 m} \left(2 \sqrt{133} + 28\right) + \frac{\hbar^{2} l^{3}}{6 m}\right) + c_{2} \left(- \frac{\hbar^{2} l^{5}}{140 m} \left(2 \sqrt{133} + 28\right) + \frac{\hbar^{2} l^{5}}{30 m}\right) & c_{1} \left(- \frac{\hbar^{2} l^{3}}{30 m} \left(18 \sqrt{5} + 60\right) + \frac{\hbar^{2} l^{3}}{6 m}\right) + c_{2} \left(- \frac{\hbar^{2} l^{5}}{140 m} \left(18 \sqrt{5} + 60\right) + \frac{\hbar^{2} l^{5}}{30 m}\right)\\c_{1} \left(- \frac{\hbar^{2} l^{5}}{140 m} \left(- 2 \sqrt{133} + 28\right) + \frac{\hbar^{2} l^{5}}{30 m}\right) + c_{2} \left(- \frac{\hbar^{2} l^{7}}{630 m} \left(- 2 \sqrt{133} + 28\right) + \frac{\hbar^{2} l^{7}}{105 m}\right) & c_{1} \left(- \frac{\hbar^{2} l^{5}}{140 m} \left(- 18 \sqrt{5} + 60\right) + \frac{\hbar^{2} l^{5}}{30 m}\right) + c_{2} \left(- \frac{\hbar^{2} l^{7}}{630 m} \left(- 18 \sqrt{5} + 60\right) + \frac{\hbar^{2} l^{7}}{105 m}\right) & c_{1} \left(- \frac{\hbar^{2} l^{5}}{140 m} \left(2 \sqrt{133} + 28\right) + \frac{\hbar^{2} l^{5}}{30 m}\right) + c_{2} \left(- \frac{\hbar^{2} l^{7}}{630 m} \left(2 \sqrt{133} + 28\right) + \frac{\hbar^{2} l^{7}}{105 m}\right) & c_{1} \left(- \frac{\hbar^{2} l^{5}}{140 m} \left(18 \sqrt{5} + 60\right) + \frac{\hbar^{2} l^{5}}{30 m}\right) + c_{2} \left(- \frac{\hbar^{2} l^{7}}{630 m} \left(18 \sqrt{5} + 60\right) + \frac{\hbar^{2} l^{7}}{105 m}\right)\\c_{3} \left(- \frac{\hbar^{2} l^{5}}{840 m} \left(- 2 \sqrt{133} + 28\right) + \frac{\hbar^{2} l^{5}}{40 m}\right) + c_{4} \left(- \frac{\hbar^{2} l^{7}}{5040 m} \left(- 2 \sqrt{133} + 28\right) + \frac{\hbar^{2} l^{7}}{280 m}\right) & c_{3} \left(- \frac{\hbar^{2} l^{5}}{840 m} \left(- 18 \sqrt{5} + 60\right) + \frac{\hbar^{2} l^{5}}{40 m}\right) + c_{4} \left(- \frac{\hbar^{2} l^{7}}{5040 m} \left(- 18 \sqrt{5} + 60\right) + \frac{\hbar^{2} l^{7}}{280 m}\right) & c_{3} \left(- \frac{\hbar^{2} l^{5}}{840 m} \left(2 \sqrt{133} + 28\right) + \frac{\hbar^{2} l^{5}}{40 m}\right) + c_{4} \left(- \frac{\hbar^{2} l^{7}}{5040 m} \left(2 \sqrt{133} + 28\right) + \frac{\hbar^{2} l^{7}}{280 m}\right) & c_{3} \left(- \frac{\hbar^{2} l^{5}}{840 m} \left(18 \sqrt{5} + 60\right) + \frac{\hbar^{2} l^{5}}{40 m}\right) + c_{4} \left(- \frac{\hbar^{2} l^{7}}{5040 m} \left(18 \sqrt{5} + 60\right) + \frac{\hbar^{2} l^{7}}{280 m}\right)\\c_{3} \left(- \frac{\hbar^{2} l^{7}}{5040 m} \left(- 2 \sqrt{133} + 28\right) + \frac{\hbar^{2} l^{7}}{280 m}\right) + c_{4} \left(- \frac{\hbar^{2} l^{9}}{27720 m} \left(- 2 \sqrt{133} + 28\right) + \frac{\hbar^{2} l^{9}}{1260 m}\right) & c_{3} \left(- \frac{\hbar^{2} l^{7}}{5040 m} \left(- 18 \sqrt{5} + 60\right) + \frac{\hbar^{2} l^{7}}{280 m}\right) + c_{4} \left(- \frac{\hbar^{2} l^{9}}{27720 m} \left(- 18 \sqrt{5} + 60\right) + \frac{\hbar^{2} l^{9}}{1260 m}\right) & c_{3} \left(- \frac{\hbar^{2} l^{7}}{5040 m} \left(2 \sqrt{133} + 28\right) + \frac{\hbar^{2} l^{7}}{280 m}\right) + c_{4} \left(- \frac{\hbar^{2} l^{9}}{27720 m} \left(2 \sqrt{133} + 28\right) + \frac{\hbar^{2} l^{9}}{1260 m}\right) & c_{3} \left(- \frac{\hbar^{2} l^{7}}{5040 m} \left(18 \sqrt{5} + 60\right) + \frac{\hbar^{2} l^{7}}{280 m}\right) + c_{4} \left(- \frac{\hbar^{2} l^{9}}{27720 m} \left(18 \sqrt{5} + 60\right) + \frac{\hbar^{2} l^{9}}{1260 m}\right)\end{matrix}\right]$$

```python
CRcol = C[:,:]
CRcol
```

$$\left[\begin{matrix}c_{1}\\c_{2}\\c_{3}\\c_{4}\end{matrix}\right]$$

```python
CRcol = C[:,:]
for a in range(1, len(C)+1):
    C_val = solve(EC.col(a-1), C)
    for b in range(1, len(C)+1):
        if sympify('c'+str(b)) in C_val:
            CRcol[b-1] = C_val[sympify('c'+str(b))]
        else:
            CRcol[b-1] = sympify('c'+str(b))
    if a == 1:
        CR = Matrix([CRcol])
    else:
        CR = CR.col_insert(a-1, CRcol)
simplify(CR)
```

$$\left[\begin{matrix}\frac{c_{2} l^{2}}{21} \left(7 + \sqrt{133}\right) & 0 & \frac{c_{2} l^{2}}{21} \left(- \sqrt{133} + 7\right) & 0\\c_{2} & 0 & c_{2} & 0\\0 & \frac{c_{4} l^{2}}{33} \left(1 + 3 \sqrt{5}\right) & 0 & \frac{c_{4} l^{2}}{33} \left(- 3 \sqrt{5} + 1\right)\\0 & c_{4} & 0 & c_{4}\end{matrix}\right]$$

```python
print("\nCalculando constante de normalización...")
PHI = CR.T*F.T
PHI
```

    Calculando constante de normalización...

$$\left[\begin{matrix}c_{2} x^{2} \left(l - x\right)^{2} + x \left(l - x\right) \left(\frac{c_{2} l^{2}}{3} + \frac{\sqrt{133} c_{2}}{21} l^{2}\right)\\c_{4} x^{2} \left(\frac{l}{2} - x\right) \left(l - x\right)^{2} + x \left(\frac{l}{2} - x\right) \left(l - x\right) \left(\frac{c_{4} l^{2}}{33} + \frac{\sqrt{5} c_{4}}{11} l^{2}\right)\\c_{2} x^{2} \left(l - x\right)^{2} + x \left(l - x\right) \left(- \frac{\sqrt{133} c_{2}}{21} l^{2} + \frac{c_{2} l^{2}}{3}\right)\\c_{4} x^{2} \left(\frac{l}{2} - x\right) \left(l - x\right)^{2} + x \left(\frac{l}{2} - x\right) \left(l - x\right) \left(- \frac{\sqrt{5} c_{4}}{11} l^{2} + \frac{c_{4} l^{2}}{33}\right)\end{matrix}\right]$$

```python
PHISQ = zeros(len(C), 1)
for a in range(1, len(C)+1):
    PHISQ[a-1] = PHI[a-1]**2
PHISQ
```

$$\left[\begin{matrix}\left(c_{2} x^{2} \left(l - x\right)^{2} + x \left(l - x\right) \left(\frac{c_{2} l^{2}}{3} + \frac{\sqrt{133} c_{2}}{21} l^{2}\right)\right)^{2}\\\left(c_{4} x^{2} \left(\frac{l}{2} - x\right) \left(l - x\right)^{2} + x \left(\frac{l}{2} - x\right) \left(l - x\right) \left(\frac{c_{4} l^{2}}{33} + \frac{\sqrt{5} c_{4}}{11} l^{2}\right)\right)^{2}\\\left(c_{2} x^{2} \left(l - x\right)^{2} + x \left(l - x\right) \left(- \frac{\sqrt{133} c_{2}}{21} l^{2} + \frac{c_{2} l^{2}}{3}\right)\right)^{2}\\\left(c_{4} x^{2} \left(\frac{l}{2} - x\right) \left(l - x\right)^{2} + x \left(\frac{l}{2} - x\right) \left(l - x\right) \left(- \frac{\sqrt{5} c_{4}}{11} l^{2} + \frac{c_{4} l^{2}}{33}\right)\right)^{2}\end{matrix}\right]$$

```python
INTPHISQ = integrate(PHISQ, (x, 0, l))
INTPHISQ
```

$$\left[\begin{matrix}- \frac{7 c_{2}^{2}}{18} l^{9} + l^{7} \left(- \frac{2 \sqrt{133}}{147} c_{2}^{2} l^{2} + \frac{16 c_{2}^{2}}{21} l^{2}\right) + l^{6} \left(- \frac{c_{2}^{2} l^{3}}{3} + \frac{\sqrt{133} c_{2}^{2}}{21} l^{3}\right) + l^{5} \left(- \frac{16 \sqrt{133}}{315} c_{2}^{2} l^{4} - \frac{37 c_{2}^{2}}{315} l^{4}\right) + l^{4} \left(- \frac{5 c_{2}^{2}}{126} l^{5} + \frac{\sqrt{133} c_{2}^{2}}{126} l^{5}\right) + l^{3} \left(\frac{2 \sqrt{133}}{189} c_{2}^{2} l^{6} + \frac{26 c_{2}^{2}}{189} l^{6}\right)\\- \frac{9 c_{4}^{2}}{22} l^{11} + l^{9} \left(- \frac{2 \sqrt{5}}{99} c_{4}^{2} l^{2} + \frac{1345 c_{4}^{2}}{1188} l^{2}\right) + l^{8} \left(- \frac{355 c_{4}^{2}}{264} l^{3} + \frac{\sqrt{5} c_{4}^{2}}{11} l^{3}\right) + l^{7} \left(- \frac{821 \sqrt{5}}{5082} c_{4}^{2} l^{4} + \frac{6712 c_{4}^{2}}{7623} l^{4}\right) + l^{6} \left(- \frac{445 c_{4}^{2}}{1452} l^{5} + \frac{205 \sqrt{5}}{1452} c_{4}^{2} l^{5}\right) + l^{5} \left(- \frac{109 \sqrt{5}}{1815} c_{4}^{2} l^{6} + \frac{245 c_{4}^{2}}{4356} l^{6}\right) + l^{4} \left(- \frac{35 c_{4}^{2}}{2904} l^{7} + \frac{9 \sqrt{5}}{968} c_{4}^{2} l^{7}\right) + l^{3} \left(\frac{\sqrt{5} c_{4}^{2}}{2178} l^{8} + \frac{23 c_{4}^{2}}{6534} l^{8}\right)\\- \frac{7 c_{2}^{2}}{18} l^{9} + l^{7} \left(\frac{2 \sqrt{133}}{147} c_{2}^{2} l^{2} + \frac{16 c_{2}^{2}}{21} l^{2}\right) + l^{6} \left(- \frac{\sqrt{133} c_{2}^{2}}{21} l^{3} - \frac{c_{2}^{2} l^{3}}{3}\right) + l^{5} \left(- \frac{37 c_{2}^{2}}{315} l^{4} + \frac{16 \sqrt{133}}{315} c_{2}^{2} l^{4}\right) + l^{4} \left(- \frac{\sqrt{133} c_{2}^{2}}{126} l^{5} - \frac{5 c_{2}^{2}}{126} l^{5}\right) + l^{3} \left(- \frac{2 \sqrt{133}}{189} c_{2}^{2} l^{6} + \frac{26 c_{2}^{2}}{189} l^{6}\right)\\- \frac{9 c_{4}^{2}}{22} l^{11} + l^{9} \left(\frac{2 \sqrt{5}}{99} c_{4}^{2} l^{2} + \frac{1345 c_{4}^{2}}{1188} l^{2}\right) + l^{8} \left(- \frac{355 c_{4}^{2}}{264} l^{3} - \frac{\sqrt{5} c_{4}^{2}}{11} l^{3}\right) + l^{7} \left(\frac{821 \sqrt{5}}{5082} c_{4}^{2} l^{4} + \frac{6712 c_{4}^{2}}{7623} l^{4}\right) + l^{6} \left(- \frac{205 \sqrt{5}}{1452} c_{4}^{2} l^{5} - \frac{445 c_{4}^{2}}{1452} l^{5}\right) + l^{5} \left(\frac{245 c_{4}^{2}}{4356} l^{6} + \frac{109 \sqrt{5}}{1815} c_{4}^{2} l^{6}\right) + l^{4} \left(- \frac{9 \sqrt{5}}{968} c_{4}^{2} l^{7} - \frac{35 c_{4}^{2}}{2904} l^{7}\right) + l^{3} \left(- \frac{\sqrt{5} c_{4}^{2}}{2178} l^{8} + \frac{23 c_{4}^{2}}{6534} l^{8}\right)\end{matrix}\right]$$

```python
for a in range(1, len(C)+1):
    SOL = solve(INTPHISQ[a-1]-1, C)
    if len(C) == 1:
        SOL = list([{c1:SOL[0]}, {c1:SOL[1]}])
    for b in range(1, len(C)+1):
        for c in range(1, len(SOL)+1):
            if sympify('c'+str(b)) in SOL[c-1]:
                if c == 1:
                    COEFrow = Matrix([SOL[c-1][sympify('c'+str(b))]])
                else:
                    COEFrow = COEFrow.col_insert(c-1, Matrix([SOL[c-1][sympify('c'+str(b))]]))
    if a == 1:
        COEF = COEFrow
    else:
        COEF = COEF.row_insert(a-1, COEFrow)
COEF
```

$$\left[\begin{matrix}- \frac{21 \sqrt{30} \sqrt{\frac{1}{l^{9}}}}{\sqrt{23 \sqrt{133} + 266}} & \frac{21 \sqrt{30} \sqrt{\frac{1}{l^{9}}}}{\sqrt{23 \sqrt{133} + 266}}\\- \frac{66 \sqrt{70} \sqrt{\frac{1}{l^{11}}}}{\sqrt{13 \sqrt{5} + 30}} & \frac{66 \sqrt{70} \sqrt{\frac{1}{l^{11}}}}{\sqrt{13 \sqrt{5} + 30}}\\- \frac{21 \sqrt{30} \sqrt{\frac{1}{l^{9}}}}{\sqrt{- 23 \sqrt{133} + 266}} & \frac{21 \sqrt{30} \sqrt{\frac{1}{l^{9}}}}{\sqrt{- 23 \sqrt{133} + 266}}\\- \frac{66 \sqrt{70} \sqrt{\frac{1}{l^{11}}}}{\sqrt{- 13 \sqrt{5} + 30}} & \frac{66 \sqrt{70} \sqrt{\frac{1}{l^{11}}}}{\sqrt{- 13 \sqrt{5} + 30}}\end{matrix}\right]$$

```python
#Seleccionar entre las 2 raices-----------------------------------------------------------!!!
CRF = CR[:, :]
for a in range(1, len(C)+1):
    SOL = solve(INTPHISQ[a-1]-1, C)
    if len(C) == 1:
        SOL = list([{c1:SOL[0]}, {c1:SOL[1]}])
    for b in range(1, len(C)+1):
        if sympify('c'+str(b)) in SOL[1]:
            Z = CRF.col(a-1).subs(sympify('c'+str(b)), SOL[1][sympify('c'+str(b))])
            CRF.col_del(a-1)
            CRF = CRF.col_insert(a-1, Z)
print("\nCoefincientes de la funcion variacional lineal.")
print("\nc_i = \n")
CRF.evalf()
```

    Coefincientes de la funcion variacional lineal.

    c_i =

$$\left[\begin{matrix}4.40399751133633 l^{2} \left(\frac{1}{l^{9}}\right)^{0.5} & 0 & - 28.6462005494649 l^{2} \left(\frac{1}{l^{9}}\right)^{0.5} & 0\\4.99034859672658 \left(\frac{1}{l^{9}}\right)^{0.5} & 0 & 132.721876195613 \left(\frac{1}{l^{9}}\right)^{0.5} & 0\\0 & 16.7823521557751 l^{2} \left(\frac{1}{l^{11}}\right)^{0.5} & 0 & - 98.9866286733696 l^{2} \left(\frac{1}{l^{11}}\right)^{0.5}\\0 & 71.8478164291383 \left(\frac{1}{l^{11}}\right)^{0.5} & 0 & 572.256840303692 \left(\frac{1}{l^{11}}\right)^{0.5}\end{matrix}\right]$$

```python
#Calcular la función de onda
for a in range(1, len(C)+1):
    if a == 1:
        PHIS = Matrix([F*CRF.col(a-1)])
    else:
        PHIS = PHIS.row_insert(a-1, Matrix([F*CRF.col(a-1)]))

print("\nFunciones de onda.")
print("\nf_i = \n")
PHIS.evalf()
```

    Funciones de onda.

    f_i =

$$\left[\begin{matrix}4.40399751133633 l^{2} x \left(l - x\right) \left(\frac{1}{l^{9}}\right)^{0.5} + 4.99034859672658 x^{2} \left(l - x\right)^{2} \left(\frac{1}{l^{9}}\right)^{0.5}\\16.7823521557751 l^{2} x \left(0.5 l - x\right) \left(l - x\right) \left(\frac{1}{l^{11}}\right)^{0.5} + 71.8478164291383 x^{2} \left(0.5 l - x\right) \left(l - x\right)^{2} \left(\frac{1}{l^{11}}\right)^{0.5}\\- 28.6462005494649 l^{2} x \left(l - x\right) \left(\frac{1}{l^{9}}\right)^{0.5} + 132.721876195613 x^{2} \left(l - x\right)^{2} \left(\frac{1}{l^{9}}\right)^{0.5}\\- 98.9866286733696 l^{2} x \left(0.5 l - x\right) \left(l - x\right) \left(\frac{1}{l^{11}}\right)^{0.5} + 572.256840303692 x^{2} \left(0.5 l - x\right) \left(l - x\right)^{2} \left(\frac{1}{l^{11}}\right)^{0.5}\end{matrix}\right]$$

```python
#Graficar phi y |phi|^2------------------------------------------------------------------------!!!
PHISSQ = zeros(len(C), 1)
for a in range(1, len(C)+1):
    PHISSQ[a-1] = PHIS[a-1]**2
INTPHISSQ = integrate(PHISSQ, x)
plot((PHIS.subs(l, 1)[0], (x, 0, 1)), (PHISSQ.subs(l, 1)[0], (x, 0, 1)))
plot((PHIS.subs(l, 1)[1], (x, 0, 1)), (PHISSQ.subs(l, 1)[1], (x, 0, 1)))
plot((PHIS.subs(l, 1)[2], (x, 0, 1)), (PHISSQ.subs(l, 1)[2], (x, 0, 1)))
plot((PHIS.subs(l, 1)[3], (x, 0, 1)), (PHISSQ.subs(l, 1)[3], (x, 0, 1)))
```

```python
print("Esta linea sólo es para compara con los resultados del libro.")
var('X')
PHIS.subs(x, X*l).evalf()
```

```python

```

```python

```


# Kevitsa Gravity Inversion

**Objective:**

In this tutorial we will invert simulated gravity data over the Kevitsa Ni-Cu-PGE deposit.
The two main objectives are

1. Walk through the gravity inversion workflow with SimPEG
2. Determine the depth resolution of the gravity survey

Ultimately, we want to know if the recovered compact density model can delinate the shape and vertical extent of the Kevitsa intrusion. We will compare our density model to the published geological horizons picked from 2d/3D seismic survey.

**References**

Koivisto, E., Malehmir, A., Hellqvist, N., Voipio, T. and Wijns, C. (2015), Building a 3D model of lithological contacts and near-mine structures in the Kevitsa mining and exploration site, Northern Finland: constraints from 2D and 3D reflection seismic data. Geophysical Prospecting, 63: 754–773. doi:10.1111/1365-2478.12252

Here goes nothing...

```python
# The usual, we need to load some libraries
from SimPEG import Mesh, Utils, Maps, PF
from SimPEG import mkvc, Regularization, DataMisfit, Optimization, InvProblem, Directives,Inversion
from SimPEG.Utils import mkvc
from SimPEG.Utils.io_utils import download
import numpy as np
import scipy as sp
import os
%pylab inline
```

    Populating the interactive namespace from numpy and matplotlib

```python
# Download data from the cloud
url = "https://storage.googleapis.com/simpeg/kevitsa_synthetic/"

cloudfiles = [
    'Mesh_global_100m_padded.msh','GravSim.dat',
    'Kevitsa.topo', 'SimPEG_GRAV.inp'
]
keys = ['mesh', 'data', 'topo', 'input']

# Download to ./KevitsaGrav
files = download([url+f for f in cloudfiles], folder='./KevitsaGrav', overwrite=True)
files = dict(zip(keys, files))  # allows us to name the files

# Read in the input file which included all parameters at once (mesh, topo, model, survey, inv param, etc.)
inputFile = files['input'] # input file was the last downloaded
driver = PF.GravityDriver.GravityDriver_Inv()
driver.basePath = './KevitsaGrav'

# All the parameters in the input files can be access via the driver object
# For example, to get the survey:
obs = driver.readGravityObservations(files['data'])
mesh = Mesh.TensorMesh.readUBC(files['mesh'])

```

    overwriting Mesh_global_100m_padded.msh
    overwriting GravSim.dat
    overwriting Kevitsa.topo
    overwriting SimPEG_GRAV.inp
    Downloading https://storage.googleapis.com/simpeg/kevitsa_synthetic/Mesh_global_100m_padded.msh
    Downloading https://storage.googleapis.com/simpeg/kevitsa_synthetic/GravSim.dat
    Downloading https://storage.googleapis.com/simpeg/kevitsa_synthetic/Kevitsa.topo
    Downloading https://storage.googleapis.com/simpeg/kevitsa_synthetic/SimPEG_GRAV.inp
    Download completed!

## Setup

The relation between density and the gravity field is well known, thanks to the classic work of Newton in 1686. Since we generally only measure the vertical component of the field, this relationship can be written as:

$$G(r)_z = \gamma \int_{V}   \rho(r) \left(\frac{z - z_0}{{|\vec r - \vec r_0|}^3}\right) \; dV $$

where $\rho$ is the anomalous density and $\gamma$ is the Newton's gravitational constant.
This integral can be evaluated analytically for simple prisms, giving rise to a linear system of equations relating a discrete Earth to the observed data:

$$ \mathbf{d}_z =  \mathbf{F} \; \boldsymbol{\rho} $$

```python
# The gridded data holds 20k+ observation points, too large for a quick inversion
# Let's grab a random subset
nD = 500
indx = randint(0,high=obs.dobs.shape[0],size=nD)

# Create a new downsampled survey
locXYZ  = obs.srcField.rxList[0].locs[indx,:]

rxLoc = PF.BaseGrav.RxObs(locXYZ)
srcField = PF.BaseGrav.SrcField([rxLoc])
survey = PF.BaseGrav.LinearSurvey(srcField)
survey.dobs = obs.dobs[indx]
survey.std = obs.std[indx]

ph = PF.Gravity.plot_obs_2D(survey.srcField.rxList[0].locs, survey.dobs,'Observed Data')
```

```python
# Create a mesh, we will start coarse. Feel free to change the
# the mesh, but make sure you have enough memory and coffee brakes...
dx = 200.
npad = 5
hxind = [(dx, npad, -1.3), (dx, 65), (dx, npad, 1.3)]
hyind = [(dx, npad, -1.3), (dx, 45), (dx, npad, 1.3)]
hzind = [(dx, npad, -1.3), (150, 15), (10, 10, -1.3), (10,5)]

# Create the mesh and move the location to the center of the data
mesh = Mesh.TensorMesh([hxind, hyind, hzind], 'CC0')
mesh._x0 += [np.mean(locXYZ[:,0]), np.mean(locXYZ[:,1]), np.max(locXYZ[:,2])-np.sum(mesh.hz)]

ax = mesh.plotGrid()

# We will get the topography from the input file
topo = np.genfromtxt(files['topo'], skip_header=1)

# Find the active cells
actv = Utils.surface2ind_topo(mesh, topo, 'N')

actv = np.asarray(
    [inds for inds, elem in enumerate(actv, 1) if elem], dtype=int
) - 1

nC = len(actv)

print("Number of data points: " + str(nD))
print("Number of model cells: " + str(nC))
```

**Forward system:**

Now that we have all our spatial components, we can create our linear system relating the data and anomalous density:

$$ d^{obs} = \mathbf{F\; \rho}$$

where $\mathbf{F} \in \mathbb{R}^{nd \times nc}$ is our $forward$ operator.

```python
# Create active map to go from reduce set to full
actvMap = Maps.InjectActiveCells(mesh, actv, -100)

# Create reduced identity map
idenMap = Maps.IdentityMap(nP=nC)
mstart = np.ones(nC)*1e-4

# Create gravity problem
prob = PF.Gravity.GravityIntegral(mesh, rhoMap=idenMap, actInd=actv)

survey.pair(prob)
```

```python
# Make depth weighting,
# this will also require the calculation of the forward operator ... time for coffee
wr = np.sum(prob.G**2., axis=0)**0.5
wr = (wr/np.max(wr))
```

    Begin calculation of forward operator: z
    Done 0.0 %

## Inverse problem

We have generated synthetic data, we now want to see if we can solve the inverse problem and recover our synthetic density model. Using the usual formulation, we seek a model that can reproduce the data, let’s say a least-squares measure of data fit of the form:

\begin{equation}
\phi_d =   \|\mathbf{W}_d \left( \mathbb{F}[\mathbf{m}] - \mathbf{d}^{obs} \right)\|_2^2
\end{equation}

The inverse problem is hard because we don’t have great data coverage, and the Earth is big, and there is usually noise in the data. So we need to add something to regularize it.
The simplest way to do it is to penalize solutions that won’t make sense geologically. For example we can assume that the model is smooth and that anomalous density should remain small.
The usual smooth inversion function use an l2-norm measure:

\begin{equation}
\phi_d =   \|\mathbf{W}_d \left( \mathbb{F}[\mathbf{m}] - \mathbf{d}^{obs} \right)\|_2^2 \\
\phi_m = \beta \Big [ {\| \mathbf{W}_s \;( \mathbf{m - m^{ref}})\|}^2_2  + \sum_{i = x,y,z}  {\|   \mathbf{W}_i  \; \mathbf{G}_i \; \mathbf{m}\|}^2_2  \Big ]\;,
\end{equation}

The full objective function to be minimized can be written as:

\begin{equation}
\phi(m) =  \phi_d + \beta \phi_m\;,
\end{equation}

which will yield our usual *small* and *smooth* models.

We propose a fancier regularization function that can allow to recover *sparse* and *blocky* solutions.
Starting with the well known Ekblom norm:
\begin{equation}
\phi_m =  \sum_{i=1}^{nc} {(x_i^2 + \epsilon^2)}^{p/2} \;,
\end{equation}
where $x_i$ denotes some function of the model parameter, and $\epsilon$ is a small value to avoid singularity as $m\rightarrow0$.
For p=2, we get the usual least-squares measure and we recover the regularization presented above. For $p \leq 1$, the function becomes non-linear which requires some tweaking.

We can linearize the function by updating the penality function iteratively, commonly known as an Iterative Re-weighted Least-Squares (IRLS) method:

\begin{equation}
\phi_m^{(k)} =  \frac{1}{2}\sum_{i=1}^{nc} r_i \; x_i^2
\end{equation}

where we added the superscript $\square^{(k)}$ to denote the IRLS iterations. The weights $r(x)$ are computed from model values obtained at a previous iteration such that:

\begin{equation}
	{r}_i  ={\Big( {({x_i}^{(k-1)})}^{2} + \epsilon^2 \Big)}^{p/2 - 1} \;,
\end{equation}

where ${r}(x) \in \mathbb{R}^{nc}$.

In matrix form, our objective function simply becomes:

\begin{equation}
\phi(m) =   \|\mathbf{W}_d \left( \mathbb{F}[\mathbf{m}] - \mathbf{d}^{obs} \right)\|_2^2 + \beta \Big [ {\| \mathbf{W}_s \;\mathbf{R}_s\;( \mathbf{m - m^{ref}})\|}^2_2  + \sum_{i = x,y,z}  {\|   \mathbf{W}_i\; \mathbf{R}_i  \; \mathbf{G}_i \; \mathbf{m}\|}^2_2  \Big ]\;,
\end{equation}

where the IRLS weights $\mathbf{R}_s$ and $\mathbf{R}_i$ are diagonal matrices defined as:

\begin{equation}
\begin{split}
	{R}_{s_{jj}}  &=  \sqrt{\eta_p}{\Big[ {({m_j}^{(k-1)})}^{2} + \epsilon_p^2 \Big]}^{(p/2 - 1)/2} \\
	{R}_{i_{jj}}  &=  \sqrt{\eta_q}{\Big[ {\left ({{(G_i\;m^{(k-1)})}_j }\right)}^{2} + \epsilon_q^2 \Big]}^{(q/2 - 1)/2} \\
\eta_p &=  {\epsilon_p}^{(1-p/2)} \\
\eta_q &=   {\epsilon_q}^{(1-q/2)}  \;,
\end{split}
\end{equation}

we added two scaling parameters $\eta_p$ and $\eta_q$ for reasons that we won't dicuss here, but turn out to be important to get stable solves.

In order to initialize the IRLS and get an estimate for the stabilizing parameters $\epsilon_p$ and $\epsilon_q$, we first invert with the smooth $l_2$-norm.
The whole IRLS process is implemented with a directive added to the inversion workflow (see below).

```python
# % Create inversion objects
reg = Regularization.Sparse(mesh, indActive=actv, mapping=idenMap)
reg.cell_weights = wr
reg.norms = [0,2,2,2]

opt = Optimization.ProjectedGNCG(maxIter=100, lower=-.5,upper=0.5, maxIterLS = 20, maxIterCG= 10, tolCG = 1e-3)
dmis = DataMisfit.l2_DataMisfit(survey)
dmis.W = 1./survey.std

# This is where the misfit function and regularization are put together
invProb = InvProblem.BaseInvProblem(dmis, reg, opt)

# Here are few directives to make the inversion work and apply sparsity.
# After the l2, beta is re-adjusted on the fly to stay near the target misfit
betaest = Directives.BetaEstimate_ByEig()
IRLS = Directives.Update_IRLS(f_min_change=1e-4, minGNiter=3)
update_Jacobi = Directives.Update_lin_PreCond()
inv = Inversion.BaseInversion(invProb, directiveList=[betaest, IRLS,
                                                      update_Jacobi])
# Run the inversion
mrec = inv.run(mstart)
```

### View the inversion results

```python
# Here is a quick script to slice through the final model
import ipywidgets as widgets

def ModSlicer(mesh, model):

    def plotIt(m, normal, panel, vmin, vmax):

        ypanel = int(mesh.vnC[1]/2)
        plt.figure(figsize=(10, 8))
        ax = plt.subplot(211)
        ph = mesh.plotSlice(model[m], ax=ax, normal=normal, ind=int(panel),
                       grid=True,
                       clim=(vmin,vmax), pcolorOpts={'cmap': 'jet', })

        # Set default limits
        if normal == 'X':
            Xlim = [mesh.vectorNy.min(), mesh.vectorNy.max()]
            Ylim = [mesh.vectorNz.min(), mesh.vectorNz.max()]
        elif normal == 'Y':
            Xlim = [mesh.vectorNx.min(), mesh.vectorNx.max()]
            Ylim = [mesh.vectorNz.min(), mesh.vectorNz.max()]
        else:
            Xlim = [mesh.vectorNx.min(), mesh.vectorNx.max()]
            Ylim = [mesh.vectorNy.min(), mesh.vectorNy.max()]

        ax.set_xlim(Xlim)
        ax.set_ylim(Ylim)
        ax.set_aspect('equal')
        plt.colorbar(ph[0])
        plt.title('Plan lp-model.')
        plt.gca().set_aspect('equal')
        plt.ylabel('y')
        plt.gca().set_aspect('equal', adjustable='box')
        plt.show()

    out = widgets.interactive(plotIt,
                              m = widgets.ToggleButtons(
                                        options=['l2', 'lp'],
                                        description='Model:'),
                              normal = widgets.ToggleButtons(
                                        options=['X', 'Y', 'Z'],
                                        description='Normal:',
                                        disabled=False,
                                        button_style='', # 'success', 'info', 'warning', 'danger' or ''
                                        tooltip='Description'),
                              panel = widgets.FloatSlider(min=0, max=mesh.vnC.max(), step=1,value=1, continuous_update=False),
                              vmin = widgets.FloatSlider(min=model['l2'][~np.isnan(model['l2'])].min(), max=model['l2'][~np.isnan(model['l2'])].max(), step=0.001,value=model['l2'][~np.isnan(model['l2'])].min(), continuous_update=False),
                              vmax = widgets.FloatSlider(min=model['l2'][~np.isnan(model['l2'])].min(), max=model['l2'][~np.isnan(model['l2'])].max(), step=0.001,value=model['l2'][~np.isnan(model['l2'])].max(), continuous_update=False),
)
    return out

# Plot the result
m_lp = actvMap * mrec
m_lp[m_lp == -100] = np.nan

m_l2 = actvMap*IRLS.l2model
m_l2[m_l2 == -100] = np.nan

model={'l2':m_l2,'lp':m_lp}
```

```python
# Execute the ploting function
ModSlicer(mesh, model)
```

```python

```


# GP Regression with Uncertain Inputs

## Introduction

In this notebook, we're going to demonstrate one way of dealing with uncertainty in our training data. Let's say that we're collecting training data that models the following function.

\begin{align}
y &= \sin(2\pi x) + \epsilon \\
  \epsilon &\sim \mathcal{N}(0, 0.2)
\end{align}

However, now assume that we're a bit uncertain about our features. In particular, we're going to assume that every `x_i` value is not a point but a distribution instead. E.g.

$$ x_i \sim \mathcal{N}(\mu_i, \sigma_i). $$

## Using stochastic variational inference to deal with uncertain inputs

To deal with this uncertainty, we'll use variational inference (VI) in conjunction with stochastic optimization. At every optimization iteration, we'll draw a sample `x_i` from the input distribution. The objective function (ELBO) that we compute will be an unbiased estimate of the true ELBO, and so a stochastic optimizer like SGD or Adam should converge to the true ELBO (or at least a local minimum of it).

```python
import math
import torch
import tqdm
import gpytorch
from matplotlib import pyplot as plt

%matplotlib inline
%load_ext autoreload
%autoreload 2
```

    The autoreload extension is already loaded. To reload it, use:
      %reload_ext autoreload

### Set up training data

In the next cell, we set up the training data for this example. We'll be using 20 regularly spaced points on [0,1].
We'll represent each of the training points $x_i$ by their mean $\mu_i$ and variance $\sigma_i$.

```python
# Training data is 100 points in [0,1] inclusive regularly spaced
train_x_mean = torch.linspace(0, 1, 20)
# We'll assume the variance shrinks the closer we get to 1
train_x_stdv = torch.linspace(0.03, 0.01, 20)

# True function is sin(2*pi*x) with Gaussian noise
train_y = torch.sin(train_x_mean * (2 * math.pi)) + torch.randn(train_x_mean.size()) * 0.2
```

```python
f, ax = plt.subplots(1, 1, figsize=(8, 3))
ax.errorbar(train_x_mean, train_y, xerr=(train_x_stdv * 2), fmt="k*", label="Train Data")
ax.legend()
```

## Setting up the model

Since we're performing VI to deal with the feature uncertainty, we'll be using a `~gpytorch.models.ApproximateGP`. Similar to the [SVGP example](./SVGP_Regression_CUDA.ipynb), we'll use a `VariationalStrategy` and a `CholeskyVariationalDistribution` to define our posterior approximation.

```python
from gpytorch.models import ApproximateGP
from gpytorch.variational import CholeskyVariationalDistribution
from gpytorch.variational import VariationalStrategy

class GPModel(ApproximateGP):
    def __init__(self, inducing_points):
        variational_distribution = CholeskyVariationalDistribution(inducing_points.size(0))
        variational_strategy = VariationalStrategy(self, inducing_points, variational_distribution, learn_inducing_locations=True)
        super(GPModel, self).__init__(variational_strategy)
        self.mean_module = gpytorch.means.ConstantMean()
        self.covar_module = gpytorch.kernels.ScaleKernel(gpytorch.kernels.RBFKernel())

    def forward(self, x):
        mean_x = self.mean_module(x)
        covar_x = self.covar_module(x)
        return gpytorch.distributions.MultivariateNormal(mean_x, covar_x)

inducing_points = torch.randn(10, 1)
model = GPModel(inducing_points=inducing_points)
likelihood = gpytorch.likelihoods.GaussianLikelihood()
```

## Training the model with uncertain features

The training iteration should look pretty similar to the SVGP example -- where we optimize the variational parameters and model hyperparameters. The key difference is that, at every iteration, we will draw samples from our features distribution (since we don't have point measurements of our features).

```python
# Inside the training iteration...
train_x_sample = torch.distributions.Normal(train_x_mean, train_x_stdv).rsample()
# Rest of training iteration...
```

```python
# this is for running the notebook in our testing framework
import os
smoke_test = ('CI' in os.environ)
training_iter = 2 if smoke_test else 400

model.train()
likelihood.train()

# We use SGD here, rather than Adam. Emperically, we find that SGD is better for variational regression
optimizer = torch.optim.Adam([
    {'params': model.parameters()},
    {'params': likelihood.parameters()},
], lr=0.01)

# Our loss object. We're using the VariationalELBO
mll = gpytorch.mlls.VariationalELBO(likelihood, model, num_data=train_y.size(0))

iterator = tqdm.tqdm_notebook(range(training_iter))
for i in iterator:
    # First thing: draw a sample set of features from our distribution
    train_x_sample = torch.distributions.Normal(train_x_mean, train_x_stdv).rsample()

    # Now do the rest of the training loop
    optimizer.zero_grad()
    output = model(train_x_sample)
    loss = -mll(output, train_y)
    iterator.set_postfix(loss=loss.item())
    loss.backward()
    optimizer.step()
```

    HBox(children=(IntProgress(value=0, max=400), HTML(value='')))

```python
# Get into evaluation (predictive posterior) mode
model.eval()
likelihood.eval()

# Test points are regularly spaced along [0,1]
# Make predictions by feeding model through likelihood
with torch.no_grad(), gpytorch.settings.fast_pred_var():
    test_x = torch.linspace(0, 1, 51)
    observed_pred = likelihood(model(test_x))

with torch.no_grad():
    # Initialize plot
    f, ax = plt.subplots(1, 1, figsize=(8, 3))

    # Get upper and lower confidence bounds
    lower, upper = observed_pred.confidence_region()
    # Plot training data as black stars
    ax.errorbar(train_x_mean.numpy(), train_y.numpy(), xerr=train_x_stdv, fmt='k*')
    # Plot predictive means as blue line
    ax.plot(test_x.numpy(), observed_pred.mean.numpy(), 'b')
    # Shade between the lower and upper confidence bounds
    ax.fill_between(test_x.numpy(), lower.numpy(), upper.numpy(), alpha=0.5)
    ax.set_ylim([-3, 3])
    ax.legend(['Observed Data', 'Mean', 'Confidence'])
```

This is a toy example, but it can be useful in practice for more complex datasets where features are more likely to be missing.


# Analysis of Oscillations

# Preliminaries

## Imports

```python
import tellurium as te
import sympy
import matplotlib.pyplot as plt
import numpy as np
import os

from common_python.sympy import sympyUtil as su
from common_python.ODEModel.ODEModel import ODEModel
from common_python.ODEModel.oscillationFinder import OscillationFinder, XDict
import matplotlib.pyplot as plt
```

## Constants

```python
su.addSymbols("S0 S1 S2 S3 S4 S5 S6 S7 S8 S9 k0 k1 k2 k3 k4 k5 k6 k7 k8 k9 k10 k11 k12 k13 k14 k15 k16 k17 k18 k19 k20")
su.addSymbols("J0 J1 J2 J3 J4 J5 J6 J7 J8 J9 J10 J11 J12 J13 J14 J15 J16 J`7 J18 J19 J20")
FLUXES = [J0, J1, J2, J3, J4, J5, J6, J7, J8, J9, J10, J11, J12, J13, J14, J15]
SPECIES = [S0, S1, S2, S3, S4, S5, S6, S7, S8]
MODEL_DIR = "/home/ubuntu/ComplexSystems/models/3-state"
```

```python
MODEL = """
J0: S0 -> S2; k0*S0
J1: S3 + S3 -> S0; k5*S3*S3
J2: S2 -> S3; k6*S2
J3: S3 ->; k9*S2*S3
J4: -> S3; k10*S0
k0 = 6+0.9011095014634776
k5 = 1.4823891153952284
k6 = -10+15.149868787476994
k9 = 91.19197034598812
k10 = 200
S0 = 1.0
S1 = 5.0
S2 = 9.0
S3 = 3.0
S4 = 10.0
"""
RR = te.loada(MODEL)
```

# Helper Functions

```python
# Create dictionary relating reaction names to species
def mkStoichiometryExpressionDct(rr):
    stoichiometryArr = rr.getFullStoichiometryMatrix()
    reactionNames = [n[1:] if n[0] == "_" else n for n in stoichiometryArr.colnames]
    stoichiometryArr.colnames = reactionNames
    speciesNames = stoichiometryArr.rownames
    dct = {}
    for idx, species in enumerate(speciesNames):
        sym = eval(species)
        entry = ["%d*%s" % (stoichiometryArr[idx, n], reactionNames[n]) for n in range(len(reactionNames))]
        expressionStr = " + ".join(entry)
        dct[sym] = eval(expressionStr)
        dct[sym] = sympy.simplify(dct[sym])
    return dct

# Tests
stoichiometryDct = mkStoichiometryExpressionDct(RR)
stoichiometryDct
```

    {S0: -J0 + J1, S2: J0 - J2, S3: -2*J1 + J2 - J3 + J4}

```python
# Do plot
def plotEigenInfo(rr, title="", k10Range=None, fixedPointIdx=1, **kwargs):
    """
    Plots information about the dominant eigenvalue and fixed point for the
    indicated fixed point.

    Parameters
    ----------
    rr: ExtendedRoadrunner
    title: str
    k10Range: range of k10
    fixedPointIdx: int
        index of the fixed point to study
    kwargs: dict
        fractional adjustment in value
    """
    if k10Range is None:
        k10Range = [150 + v for v in range(60)]
    # Construct the data
    subDct = {k0: rr.k0, k5: rr.k5, k6: rr.k6, k9: rr.k9, k10: rr.k10}
    for key, value in kwargs.items():
        if isinstance(key, str):
            newKey = [s for s in subDct.keys() if s.name == key][0]
        else:
            newKey = key
        subDct[newKey] = subDct[newKey] * value
    xvs = []
    reals = []
    imags = []
    fixedPointDcts = []
    for c10 in k10Range:
        subDct[k10] = c10
        dominantReal = None
        dominantImag = None
        modelInfo = ODEModel.mkODEModel(rr, subs=subDct, isEigenvecs=False)
        model = modelInfo.mdl
        #model = ODEModel(REDUCED_STATE_DCT, subs=subDct, isEigenvecs=False)
        # Find the dominant eigenvalue for the fixed points
        if len(model.fixedPoints) > fixedPointIdx:
            fixedPointDcts.append(dict(model.fixedPoints[fixedPointIdx].valueDct))
            # Find the dominant eigenvalue
            for entry in model.fixedPoints[fixedPointIdx].eigenEntries:
                value = entry.value
                if isinstance(value, complex):
                    real, imag = su.asRealImag(value)
                else:
                    real = value
                    imag = 0
                if (dominantReal is None) or (real > dominantReal):
                    dominantReal = real
                    dominantImag = np.abs(imag)
        xvs.append(c10)
        reals.append(dominantReal)
        imags.append(dominantImag)
    # Plot the dominant eigenvalue
    _, ax = plt.subplots(1)
    ax.plot(xvs, reals, color="blue")
    ax.plot(xvs, imags, color="brown")
    ax.plot([xvs[0], xvs[-1]], [0, 0], linestyle="--", color="black")
    ax.legend(["real", "imag"])
    ax.set_title(title)
    ax.set_xlabel("k10")
    # Plot the indexed fixed point
    states = list(fixedPointDcts[0].keys())
    _, ax = plt.subplots(1)
    COLORS = ["red", "green", "brown"]
    for idx, state in enumerate(states):
        yvs = [f[state] for f in fixedPointDcts]
        ax.plot(xvs, yvs, color=COLORS[idx])
    ax.legend(states)
    ax.set_title("Fixed Points")
    ax.set_xlabel("k10")
    return fixedPointDcts

# Test
dcts = plotEigenInfo(RR, k10Range=[100 + 5*v for v in range(5)], k9=1, title="Dominant eigenvalue for 2nd fixed point.")
```

```python
def runSim(model=MODEL, endTime=100, startTime=0, **kwargs):
    def findIdx(arr, time):
        """Finds the index of the time in the simulation results array."""
        bestIdx = 0
        diff = np.abs(arr[0, 0] - time)
        for idx, value in enumerate(arr[:, 0]):
            if np.abs(value - time) < diff:
                diff = np.abs(value - time)
                bestIdx = idx
        return bestIdx
    rr = te.loada(MODEL)
    # Adjust the parameters
    for key, value in kwargs.items():
        if isinstance(key, sympy.core.symbol.Symbol):
            newKey = s.name
        else:
            newKey = key
        rr[newKey] = rr[newKey] * value
    #rr.plot(rr.simulate(startTime, endTime, 10*endTime))
    arr = rr.simulate(0, endTime, 10*endTime)
    _, ax = plt.subplots(1)
    startIdx = findIdx(arr, startTime)
    endIdx = findIdx(arr, endTime)
    for idx in range(len(arr.colnames[1:])):
        ax.plot(arr[startIdx:endIdx,0], arr[startIdx:endIdx, idx+1])
    ax.legend(arr.colnames[1:])

# Tests
runSim(k9=0.5, startTime=900, endTime=910)
```

# Numercial Evaluations

Analyzes the eigenvalues numerically for the antimony simulation model

```python
files = [os.path.join(MODEL_DIR, f) for f in os.listdir(MODEL_DIR) if os.path.isfile(os.path.join(MODEL_DIR, f))]
files
```

    ['/home/ubuntu/ComplexSystems/models/3-state/Model_8570403923242184953.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_308756242966137270.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_1179159900924707683.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_495895561502583999.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_504265579707051998.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_7767344276012911592.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_191588763966222966.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_5596570675878850909.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_8734971625908708850.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_2039186835123380747.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_118483669170045219.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_6648256619537599386.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_5692220670942502751.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_2592649064797885084.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_5673427220991162920.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_6135779032141583586.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_7278606909816402353.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_803809246016972440.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_992571131540593136.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_5226499540516013182.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_3704614159356947411.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_2968569378260811723.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_2694988485423555897.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_3048224735933096233.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_2602015822777867368.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_2181137665730427804.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_1746218547843422981.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_5536852558416456242.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_6941093215103206868.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_8824795445416268729.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_7903555879340380137.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_6136176155644140162.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_9100832023263796312.ant',
     '/home/ubuntu/ComplexSystems/models/3-state/Model_4681618827199333103.ant']

```python
def analyzeFile(path, **kwargs):
    def mkTitle(string):
        start = path.index("Model_") + len("Model_")
        end = path.index(".ant")
        prefix = path[start:end]
        return "%s: %s" % (prefix, string)
    #
    rr = te.loada(path)
    finder = OscillationFinder(rr)
    # Change the parameter values
    initialParameterXD = XDict.mkParameters(rr)
    initialParameterXD = XDict(initialParameterXD.keys(), 1)
    # finder.setParameters(initialParameterXD)
    finder.simulate(parameterXD=initialParameterXD)
    finder.plot(title=mkTitle("Initial Parameters"))
    # Find the parameters
    feasibleParameterXD = finder.find(**kwargs)
    if feasibleParameterXD is not None:
        finder.simulate(parameterXD=feasibleParameterXD)
        finder.plot(title=mkTitle("Optimized Parameters"))
    else:
        print(mkTitle("No feasible parameters found!"))
    return feasibleParameterXD

# Tests
parameterXD = analyzeFile(files[0])

```

```python
for ffile in files:
    analyzeFile(ffile)
```

```python
STOP
```

# Damped Model 2

```python
MODEL2 = """
var S0
var S1
var S2
var S3
var S4
var S5
var S6
var S7
ext S8
J0: S4 -> S7+S5; k0*S4
J1: S2 -> S4+S4; k1*S2
J2: S4 -> S3+S3; k2*S4
J3: S4 -> S2+S3; k3*S4
J4: S0 -> S5; k4*S0
J5: S5 + S4 -> S5; k5*S5*S4
J6: S5 -> S3; k6*S5
J7: S8 + S3 -> S0; k7*S8*S3
J8: S3 -> S6+S5; k8*S3
J9: S6 + S5 -> S4; k9*S6*S5
J10: S7 + S5 -> S0 + S2; k10*S7*S5
J11: S3 -> S5+S6; k11*S3
J12: S6 + S1 -> S5; k12*S6*S1
J13: S5 -> S5; k13*S5
J14: S1 + S7 -> S1 + S1; k14*S1*S7
k0 = 2.5920480618068815
k1 = 422.2728070204247
k2 = 28.978192374985912
k3 = 29.723263589242986
k4 = 21.04114996098882
k5 = 1.5111236529181926
k6 = 14.363185343334044
k7 = 0.8231126169112812
k8 = 54.27226867691914
k9 = 58.17954213283633
k10 = 10.682986014127339
k11 = 194.08273474192015
k12 = 15.989508525207631
k13 = 13.186614071108659
k14 = 35.67582901156382
S0 = 1.0
S1 = 5.0
S2 = 9.0
S3 = 3.0
S4 = 10.0
S5 = 3.0
S6 = 7.0
S7 = 1.0
S8 = 6.0
"""

rr = te.loada(MODEL2)
rr.plot(rr.simulate())
```

```python
mat = sympy.Matrix(rr.getFullStoichiometryMatrix())
mat
```

```python
SPECIES_FLUX_DCT = mkStoichiometryExpressionDct(rr)
SPECIES_FLUX_DCT
```

```python
nullspace = mat.nullspace()
```

```python
# Kinetics dictionary
kineticDct = {
    J0:  k0*S4,
    J1:  k1*S2,
    J2:  k2*S4,
    J3:  k3*S4,
    J4:  k4*S0,
    J5:  k5*S5*S4,
    J6:  k6*S5,
    J7:  k7*S8*S3,
    J8:  k8*S3,
    J9:  k9*S6*S5,
    J10:  k10*S7*S5,
    J11:  k11*S3,
    J12:  k12*S6*S1,
    J13:  k13*S5,
    J14:  k14*S1*S7,
}
```

```python
modelInfo = ODEModel.mkODEModel(rr, isFixedPoints=False, isEigenvecs=False)
```

```python
# Note that S8 is external and so is constant
modelInfo.mdl.stateDct
```

```python
STATE_DCT = {s: SPECIES_FLUX_DCT[s].subs(kineticDct) for s in SPECIES_FLUX_DCT.keys() }
STATE_DCT
```

```python
relaxationResult = su.mkQuadraticRelaxation(modelInfo.mdl.stateDct)
mat = sympy.simplify(relaxationResult.mat)
mat
```

```python
relaxationResult.sub
```

```python
bVec = sympy.zeros(relaxationResult.mat.rows, 1)
nullResult = relaxationResult.mat.gauss_jordan_solve(bVec)
```

```python
nullResult[1]
```

```python
nullVec = nullResult[0]
```

```python
# Construct the constraints
```

```python
nullResult[0][1] * nullResult[0][6], nullResult[0][8]
```

```python
nullResult[0][4]
```

```python
nullResult[0][5]
```

```python
nullResult[0][10]
```

```python
MODEL = ODEModel(STATE_DCT, isFixedPoints=False)
```

```python
# Need to find a linear combination of values in the null space
# such that the kinetic equations hold.
# Have N reactions, M species. So, N - M constants to find.
su.addSymbols("c c_0 c_1 c_2 c_3 c_4 c_5 c_6")
c = sympy.Matrix([c_0, c_1, c_2, c_3, c_4, c_5, c_6])
mat = sympy.Matrix(nullspace).reshape(15, 7)
mat * c
```

```python
105/7
```

```python
# Solve for log(S*)
exprs = [ j - b for j, b in zip(kineticDct.values(), mat*c) ]
exprs = [e.subs({S2: 0, S3: 0}) for e in exprs]
sympy.solve(exprs, [ S5])
```

**Approach**
1. $N$ =  stoichiometry matrix
1. $M$ = nullspace of $N$
1. Substitute 0 for any state variable that must be zero for all vectors $M \star c$.
1. Solve for log of $x_n$ (state variable) in terms of log of $J_n$ (fluxes)
1. We know that the fluxes for the fixed points must be in $M \star c$, where $c$ is a vector.
1. Substitute $J_n$ value from previous (5) into (4) to give an expression for $x_n$ in terms of $c_n$.

Issue: How do I find the $c_i$?

**Approach 2**
1. Solve for $x_n$ in terms of $J$

```python
# Looks like I can manually solve for most species
SPECIES_FLUX_DCT = {
    S0: J4/k4,
    S1: (J12/k12) /((J9 / k9) / (J6 / k6)),
    S2: J1 / k1,
    S3: J8 / k8,
    S4: J0 / k0,
    S5: J6 / k6,
    S6: (J9 / k9) / (J6 / k6),
    S7: (J10 / k10) / (J6 / k6),
    S8: (J7 / k7) / (J8 / k8),

}
```

```python
dstateDct =  {s: SPECIES_FLUX_DCT[s].subs(kineticDct) for s in SPECIES_FLUX_DCT.keys()}
```

```python
dstateDct
```

# Reduced Model

```python
su.addSymbols("S0 S1 S2 S3 S4 S5 S6")
su.addSymbols("k0 k1 k2 k3 k4 k5 k6 k7 k8 k9 k10 k11 k12 k13 k14 k15 k16 k17 k18 k19 k20")
su.addSymbols("J0 J1 J2 J3 J4 J5 J6 J7 J8 J9 J10 J11 J12 J13 J14 J15 J16 J`7 J18 J19 J20")
REDUCED_FLUXES = [J0, J1, J2, J3, J4]
REDUCED_SPECIES = [S0, S1, S2, S3, S4]
```

```python
MODEL = """
J0: S0 -> S2; k0*S0
J1: S3 + S3 -> S0; k5*S3*S3
J2: S2 -> S3; k6*S2
J3: S3 ->; k9*S2*S3
J4: -> S3; k10*S0
k0 = 6+0.9011095014634776
k5 = 1.4823891153952284
k6 = -10+15.149868787476994
k9 = 91.19197034598812
k10 = 200
S0 = 1.0
S1 = 5.0
S2 = 9.0
S3 = 3.0
S4 = 10.0
"""
```

```python
MODEL = """
J0: S0 -> S2; k0*S0
J1: S3 + S3 -> S0; k5*S3*S3
J2: S2 -> S3; k6*S2
J3: S3 ->; k9*S2*S3
J4: -> S3; k10*S0
k0 = (6+0.9011095014634776)
k5 = 1.4823891153952284
k6 = (-10+15.149868787476994)
k9 = 91.19197034598812  # At k9 * 0.5, use k10 = k155
k10 = 200 # 164, 165 ins a transition from damped to stable oscillations
S0 = 1.0
S2 = 9.0
S3 = 3.0
"""

rr = te.loada(MODEL)
rr.plot(rr.simulate(0, 100, 100))
```

```python
runSim(k9=0.5, k10=140, endTime=1000)
```

```python
runSim(k9=0.5, k10=150, endTime=1000)
```

## Relaxation Analysis

1. Create a linear system by adding new variables for quadratics.
1. Find the nullspace of the linear system.
1. Solve for unknown constants using constraints.

```python
modelInfo = ODEModel.mkODEModel(rr, isFixedPoints=False, isEigenvecs=False)
```

```python
modelInfo.mdl.stateDct
```

```python
relaxationResult = su.mkQuadraticRelaxation(modelInfo.mdl.stateDct)
mat = sympy.simplify(relaxationResult.mat)
mat
```

```python
nullVecs = mat.nullspace(relaxationResult.vec)
nullVecs
```

```python
relaxationResult.vec
```

```python
su.addSymbols("c_0 c_1")
nullVec = nullVecs[0]*c_0 + nullVecs[1] * c_1
nullVec
```

```python
# Solve for the constraints
# Constraint 1: S2*S3 == S2S3
sympy.solve(nullVec[1] * nullVec[2] -   nullVec[3], [c_0, c_1])
```

```python
# Solve for the constraints
# Constraint 2: S3*S3 == S3S3
sympy.solve(nullVec[2] * nullVec[2] -   nullVec[4], [c_0, c_1])
```

So, $c_0 = k_6 \frac{-k_0 + k_{10}}{k_0 k_9}$. $c_1 = c_0^2$.

```python
# Verifying the solution
newNullVec = nullVec.subs(c_1, c_0**2)
newNullVec = newNullVec.subs(c_0, -k6*(k10-k0)/(k0*k9))
newNullVec
```

### Algorithm

```python
# Create the state equations from the roadrunner model
def findFixedPoints(rr):
    """
    Finds the fixed points of the roadrunner model that only has mass action kinetics
    and no more than two reactants. The approach uses relaxation by transforming the quadratic
    equations to a higher dimension linear space.

    Parameteters
    ------------
    rr: ExtendedRoadRunner

    Returns
    -------
    list-dict
        key: state variable symbol
        value: expressions
    """
    def findIdx(sym):
        return list(relaxationResult.vec).index(sym)
    #
    modelInfo = ODEModel.mkODEModel(rr, isFixedPoints=False, isEigenvecs=False)
    relaxationResult = su.mkQuadraticRelaxation(modelInfo.mdl.stateDct)
    mat = sympy.simplify(relaxationResult.mat)
    nullVecs = mat.nullspace(relaxationResult.vec)
    # Create solution for the nullspace
    constantStrs = ["c%d" % n for n in range(len(nullVecs))]
    su.addSymbols(" ".join(constantStrs), dct=globals())
    constantSyms = [globals()[s] for s in constantStrs]
    mat =  sympy.Matrix(nullVecs)
    numRow = nullVecs[0].rows
    numCol = len(nullVecs)
    nullspaceLst = [list(v) for v in nullVecs]
    mat = sympy.Matrix(nullspaceLst)
    mat = mat.transpose()
    constantVec = sympy.Matrix(constantSyms)
    nullspaceVec = mat * constantVec
    # Solve for the constraints
    symEntryDct = {s: nullspaceVec[n] for n, s in enumerate(relaxationResult.vec)}
    # Construct equality expressions and solve.
    eprs = []
    for quadSym, quadEpr in relaxationResult.sub.items():
        quadIdx = findIdx(quadSym)
        syms = list(quadEpr.args)
        if syms[1] == 2:
            syms[1] = syms[0]
        symIdxs = [findIdx(s) for s in syms]
        prodEpr = nullspaceVec[symIdxs[0]] * nullspaceVec[symIdxs[1]]
        eprs.append(nullspaceVec[quadIdx] - prodEpr)
    return eprs, constantSyms
    solutions = sympy.solve(eprs, constantSyms) # Values of constants to match constraints
    # Calculate the fixed points
    simpleSyms = set(relaxationResult.vec).symmetric_difference(relaxationResult.sub.keys())
    simpleSyms = sorted(simpleSyms, key=lambda s: s.name)
    fixedPointDcts = []
    for solution in solutions:
        # Calculate the vector for each solution
        numVec = len(nullspaceLst)
        vec = sympy.zeros(len(relaxationResult.vec), 1)
        for idx in range(numVec):
            vec += solution[idx] * sympy.Matrix(nullspaceLst[idx])
        # Create the fixed point from the vector
        fixedPointDcts.append({simpleSyms[n]: vec[n] for n in range(len(simpleSyms))})
    return fixedPointDcts

# Tests
fixedPointDcts = findFixedPoints(rr)
fixedPointDcts
```

```python
fixedPointDcts[1][S3]
```

### Evaluation

```python
files = [os.path.join(MODEL_DIR, f) for f in os.listdir(MODEL_DIR) if os.path.isfile(os.path.join(MODEL_DIR, f))]
files
```

```python
rr = te.loada(files[0])
print(rr.getAntimony())
```

```python
rr = te.loada(files[0])
result = findFixedPoints(rr)
if False:
    modelInfo = ODEModel.mkODEModel(rr, isFixedPoints=False, isEigenvecs=False)
    relaxationResult = su.mkQuadraticRelaxation(modelInfo.mdl.stateDct)
```

```python
len(result[0])
```

```python
result[0][0].expand()
```

```python
result[0][1].expand()
```

```python
result[0][2]
```

```python
sympy.fraction(1)
```

```python
sympy.fraction(result[0][2].expand().args[1])
```

```python
modelInfo.mdl.stateDct
```

## Miscellaneous Analysis

```python
plot1(k0=0.01, k5=0.1, k6=0.1, k9=0.1, base=100)
```

```python
REDUCED_SPECIES_FLUX_DCT = mkStoichiometryExpressionDct(rr)
REDUCED_SPECIES_FLUX_DCT
```

```python
kineticDct = {
    J0: k0 * S0,
    J1: k5 * S3 * S3,
    J2: k6 * S2,
    J3: k9 * S2 * S3,
    J4: k10*S0,  # Is this really mass action?

}
```

```python
# State equation is wrong for S2. Should be - S2*k6
REDUCED_STATE_DCT = {s: REDUCED_SPECIES_FLUX_DCT[s].subs(kineticDct) for s in REDUCED_SPECIES_FLUX_DCT.keys()}
REDUCED_STATE_DCT
```

```python
sympy.solve(list(REDUCED_STATE_DCT.values()), list(REDUCED_STATE_DCT.keys()))
```

```python
reducedModel = ODEModel(REDUCED_STATE_DCT)
```

```python
# Fixed points
[f.valueDct for f in reducedModel.fixedPoints]
```

```python
# Verify that these are fixed points
for fp in reducedModel.fixedPoints:
    print([sympy.simplify(e.subs(fp.valueDct)) for e in REDUCED_STATE_DCT.values()])
```

```python
# Look at the eigenvectors
if False:
    for entry in reducedModel.fixedPoints[1].eigenEntries:
        for vector in entry.vectors:
            print(vector)
```

```python
eigenvalues = [e.value for e in reducedModel.fixedPoints[1].eigenEntries]
```

**Approach**
1. Find the fixed points.
1. For non-zero fixed points:
    1. Find the eigenvalues in terms of each constant in turn, setting the other constants to 1.
    1. Search for values of constants that result in a positive but near zero real value and significant non-zero imaginary part

**Issue**
1. Eigenvalues have no relationship to the system behavior

## Finding Parameter Values

Given an ODEModel, find values of parameters that result in oscillations at different frequencies.

```python
c0 = rr.k0
c5 = rr.k5
c6 = rr.k6
c9 = rr.k9
c10 = rr.k10
for c10 in [150 + n for n in range(50)]:
    subDct = {k0: c0, k5: c5, k6: c6, k9: c9, k10: c10}
    model = ODEModel(REDUCED_STATE_DCT, subs=subDct)
    entries = model.fixedPoints[1].eigenEntries
    print((c10, [e.value for e in entries]))
```

## Plots

```python
dcts = plotEigenInfo(RR, k10Range=[100 + 5*v for v in range(25)], k9=1, title="Dominant eigenvalue for 2nd fixed point.")
```

```python
runSim(k10=100/200, startTime=0, endTime=10)
```

```python
runSim(k10=160/200, startTime=0, endTime=10)
```

```python
runSim(k9=1, k10=160/200, startTime=990, endTime=1000)
```

```python
runSim(k9=1, k10=170/200, startTime=990, endTime=1000)
```

```python
runSim(k9=1, k10=200/200, startTime=990, endTime=1000)
```

```python
4.5 / (2*np.pi)
```

```python
def plot1(base=150, **kwargs):
    k10Range=[base + 10*v for v in range(10)]
    title = ""
    for key, value in kwargs.items():
        title += " %s: %3.2f " % (str(key), value)
    plotComplexEigenvalue(rr, k10Range=k10Range, title=title, **kwargs)

plot1(k0=1, k5=1, k6=1, k9=1)
```

```python
plot1(k0=0.01, k5=0.1, k6=0.1, k9=0.1, base=100)
```

```python
plot1(k0=1, k5=1, k6=1, k9=0.5, base=100)

```

```python
runSim(k0=1, k5=1, k6=1, k9=0.5, k10=100, endTime=1000)
```

Am i excluding the dominant eigenvalue? Do the plots for all eigenvalues.


# Using NumPy and SciPy modules

In addition to using Cantera and Pint to help solve thermodynamics problems, we will need to use some additional packages in the scientific Python ecosystem to make plots, solve systems of equations, integrate ordinary differential equations, and more.

The [*SciPy Lecture Notes*](https://scipy-lectures.org) are excellent, detailed resources on all these topics, and Python programming in general.

The examples contained in this electronic book will integrate these techniques as needed, but this notebook contains some specific examples.

## Index

1. [Plotting](#Plotting)
2. [Solving systems of equations](#Solving-systems-of-equations)
3. [Integrating ODE systems](#Integrating-ODE-systems)
4. [Optimization](#Optimization)
5. [Differentiation](#Differentiation)

## Plotting

We can use [Matplotlib](https://matplotlib.org) to produce nice plots of our results. If you used Anaconda to set up your computing environment, you likely already have Matplotlib installed; if not, see their [installation instructions](https://matplotlib.org/users/installing.html).

Matplotlib provides an interface that is very similar to what you might already know from Matlab: `pyplot`. You can import this in Python files or a Jupyter notebook with the standard abbreviation `plt`:

```python
# this line makes figures interactive in Jupyter notebooks
%matplotlib inline
from matplotlib import pyplot as plt
```

```python
# these lines are only for helping improve the display
from IPython.display import set_matplotlib_formats
set_matplotlib_formats('pdf', 'png')
plt.rcParams['figure.dpi']= 150
plt.rcParams['savefig.dpi'] = 150
```

For example, let's generate some values of an independent variable $x$ linearly spaced between 0 and 10 (using the NumPy function [`linspace()`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.linspace.html)), and then plot the function $y(x) = \sin(x)$. We can also add labels to the axes, a legend, and a helpful grid.

```python
import numpy as np

x = np.linspace(0, 10, num=50, endpoint=True)
y = np.sin(x)

plt.plot(x, y)
plt.xlabel('x axis')
plt.ylabel('y axis')
plt.legend('y(x)')
plt.grid(True)
plt.show()
```

We can also plot multiple data series in a single figure:

```python

plt.plot(x, y, label='sin(x)')
plt.plot(x, np.cos(x), label='cos(x)')
plt.xlabel('x axis')
plt.ylabel('y axis')
plt.grid(True)
plt.legend()
plt.show()
```

Or, we can use subplots to plot multiple axes in the same overall figure:

```python
# 2 rows, 1 column
fig, axes = plt.subplots(2, 1)

axes[0].plot(x, y, label='sin(x)')
axes[0].set_ylabel('sin(x)')
axes[0].grid(True)

axes[1].plot(x, np.cos(x), label='cos(x)')
axes[1].set_xlabel('x axis')
axes[1].set_ylabel('cos(x)')
axes[1].grid(True)

plt.show()
```

## Solving systems of equations

Frequently we will encounter a system of one or more equations that involves an equal number of unknowns. If this is a linear system of equations, we can use linear algebra and the NumPy [`linalg.solve()`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.linalg.solve.html) function, but more often in thermodynamics we encounter complex and/or nonlinear systems.

In cases like this, we will need to set up our problems to find the roots, or zeroes, of the function(s); in other words, given a function $f(x)$, finding the root means to find the value of $x$ such that $f(x) = 0$. If we are dealing with a system of equations and the same number of unknowns, then these would be vectors: $\mathbf{f}(\mathbf{x}) = 0$.

(You might be wondering what to do about equations that don't equal zero... for example, if you have something like $f(x) = g(x)$. In this case, you just need to manipulate the equation to be in the form $f(x) - g(x) = 0$.)

The [SciPy optimization module](https://docs.scipy.org/doc/scipy/reference/optimize.html) provides functions to find roots of equations; for scalar equations, we can use [`root_scalar()`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.root_scalar.html#scipy.optimize.root_scalar), and for vector equations, we can use [`root()`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.root.html).

### Scalar equations

Let's first look at an example of a scalar function: one equation, one unknown.

Find the root of this equation:
\begin{equation}
\cos(x) = x^3
\end{equation}

We need to create a Python function that returns $f(x) = 0$, so that the function returns zero when the input value of $x$ is the (correct) root. Then, we can use the `root_scalar` function with some initial guesses.

```python
import numpy as np
from scipy import optimize

def func(x):
    return np.cos(x) - x**3

sol = optimize.root_scalar(func, x0=1.0, x1=2.0)

print(f'Root: x ={sol.root: .3f}')

print(f'Function evaluated at root: {func(sol.root)}')
```

    Root: x = 0.865
    Function evaluated at root: -2.220446049250313e-16

### Systems of equations / vector functions

We can also solve systems of equations in a similar fashion using the SciPy [`root()`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.root.html) functino, where we find the roots $\mathbf{x}$ that satisfy $\mathbf{f} (\mathbf{x}) = 0$.

For example, let's try to find the values of $x$ and $y$ that satisfy these equations:
\begin{align}
x \ln (x) &= y^3 \\
\sqrt(x) &= \frac{1}{y}
\end{align}
We have two equations and two unknowns, so we should be able to find the roots.

To solve, we create a function that evaluates these equations when made equal to zero, or
\begin{align}
x \ln (x) - y^3 &= 0 \\
\sqrt(x) - \frac{1}{y} &= 0
\end{align}
then we call `root` specifying this function and two initial guesses for $x$ and $y$:

```python
import numpy as np
from scipy import optimize

def system(vars):
    x = vars[0]
    y = vars[1]

    return [
        x*np.log(x) - y**3,
        np.sqrt(x) - (1/y)
        ]

sol = optimize.root(system, [1.0, 1.0])

x = sol.x[0]
y = sol.x[1]

print(f'Roots: x = {x: .3f}, y = {y: .3f}')
```

    Roots: x =  1.467, y =  0.826

## Integrating ODE systems

In some cases, we encounter problems that require integrating one or more ordinary different equations in time. Depending on the form of the problem, we may need to integrate a function between two points (definite integral), or we may have a system of ordinary differential equations.

### Numerical integral of samples

In some cases we have a set of $(x,y)$ data that we want to integrate numerically.
We can do this using the NumPy [`trapz()` function](https://docs.scipy.org/doc/numpy/reference/generated/numpy.trapz.html), which implements the composite trapezoidal rule.

For example, let's consider a situation where a reciprocating compressor is being used to compress ammonia vapor during a refrigeration cycle. We have some experimental measurements of the pressure-volume data during the compression stroke (see table), and we want to determine the work done on the ammonia by the piston.

| Pressure (psi) | Volume (in^3) |
|----------------|---------------|
| 65.1           | 80.0          |
| 80.5           | 67.2          |
| 93.2           | 60.1          |
| 110            | 52.5          |
| 134            | 44.8          |
| 161            | 37.6          |
| 190            | 32.5          |

To find the work done by the piston to the ammonia, we can integrate pressure with respect to volume:
\begin{equation}
W_{\text{in}} = -\int_{V_1}^{V_2} P \, dV
\end{equation}

```python
import numpy as np
from pint import UnitRegistry
ureg = UnitRegistry()
Q_ = ureg.Quantity

pressure = Q_([65.1, 80.5, 93.2, 110, 134, 161, 190], 'psi')
volume = Q_([80.0, 67.2, 60.1, 52.5, 44.8, 37.6, 32.5], 'in^3')

# convert to SI units
pressure.ito('Pa')
volume.ito('m^3')

work = -Q_(
    np.trapz(pressure.magnitude, volume.magnitude),
    pressure.units * volume.units
    )
print(f'Work done on fluid: {work.to("J"): .2f}')
```

    Work done on fluid: 589.45 joule

### Numerical integral of expression

We can also numerically integrate expressions/functions using the SciPy [`quad()` function](https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.quad.html), also in the `integrate` module.

Let's build on the previous example: first, let's fit the compressor data to the polytropic form $P V^n = c$, where $c$ and $n$ are constants, then we can integrate the resulting function to calculate work.

To fit the data, we need to rearrange the equation to the form $y = f(x)$:
\begin{equation}
P = c V^{-n}
\end{equation}
We'll use the [`scipy.optimize.curve_fit()`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html) function for that.

```python
import numpy as np
from scipy.optimize import curve_fit
from scipy.integrate import quad

from pint import UnitRegistry
ureg = UnitRegistry()
Q_ = ureg.Quantity

pressure = Q_([65.1, 80.5, 93.2, 110, 134, 161, 190], 'psi')
volume = Q_([80.0, 67.2, 60.1, 52.5, 44.8, 37.6, 32.5], 'in^3')

# convert to SI units
pressure.ito('Pa')
volume.ito('m^3')

def fit(V, n, c):
    '''Evaluate P = c * V**(-n).

    n and c will be found by the curve_fit functino.
    '''
    return c * np.power(V, -n)

# this function will automatically fit the unknown constants
params, cov = curve_fit(fit, volume.magnitude, pressure.magnitude)

print(f'Parameters: n={params[0]: 5.3f}, c={params[1]: 5.3f}')
```

    Parameters: n= 1.170, c= 194.200

    /usr/local/miniconda3/envs/jupyter/lib/python3.7/site-packages/ipykernel_launcher.py:21: RuntimeWarning: overflow encountered in power

```python
plt.plot(volume.magnitude, pressure.magnitude, 'o', label='Data')

x = np.linspace(volume[0].magnitude, volume[-1].magnitude, 100)

# plot data and fit
plt.plot(x, fit(x, *params), 'r-',
         label=f'fit: n={params[0]: 5.3f}, c={params[1]: 5.3f}'
         )
plt.xlabel('Volume (m^3)')
plt.ylabel('Pressure (Pa)')
plt.legend()
plt.ticklabel_format(style='sci', axis='y', scilimits=(0,0))
```

That looks like a great fit to the data, so now let's evaluate the work by integrating:

```python
y, err = quad(
    fit, volume[0].magnitude, volume[-1].magnitude,
    args=(params[0], params[1])
    )

work = -Q_(y, pressure.units * volume.units)

print(f'Work done on fluid: {work.to("J"): .2f}')
```

    Work done on fluid: 586.26 joule

### Initial value problems

In other words, we may have a system like
\begin{equation}
\frac{d \mathbf{y}}{dt} = \mathbf{f} (t, \mathbf{y})
\end{equation}
where $\mathbf{y}$ is the vector of state variables, for which we should have the initial values ($\mathbf{y}(t=0)$). In this case, we can use the SciPy function [`solve_ivp()` function](https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.solve_ivp.html), part of the `integrate` module.

For example, let's consider a problem where we want to find the volume of water, volume of air, and pressure in a tank as a function of time, as the tank releases water to an environment. The volumetric flow rate through the tank's valve is based on its instantaneous pressure:
\begin{equation}
\dot{V}_f = \frac{P - P_{\text{atm}}}{R_v} \;,
\end{equation}
where $P_{\text{atm}}$ is atmospheric pressure and $R_v = 10$ psi/gpm is the valve resistance parameter. The tank has volume 50 gal, and is initially filled with water (volume fraction $f$ = 0.8); the air in the tank is initially at 100 psi. We will treat the air as an ideal gas, and the water as an incompressible substance.

We have the initial air pressure, and we can calculate the initial volumes of water and air:
\begin{align}
V_{f,0} &= f \, V \\
V_{g,0} &= (1 - f) V \\
\end{align}

We can perform a mass balance on the water in the tank to obtain a rate equation for the volume of water:
\begin{align}
0 &= \dot{m}_f + \frac{d m_f}{dt} = \dot{V}_f \rho_f + \rho_f \frac{d V_f}{dt} \\
0 &= \dot{V}_f + \frac{dV_f}{dt} \\
\therefore \frac{dV_f}{dt} &= -\frac{(P - P_{\text{atm}})}{R_v}
\end{align}

Since the overall volume of the tank is constant, we can obtain the rate of change of the volume of air:
\begin{align}
V &= V_g + V_f \\
\frac{dV_g}{dt} + \frac{dV_f}{dt} &= 0 \\
\therefore \frac{dV_g}{dt} &= -\frac{dV_f}{dt}
\end{align}

Finally, a mass balance on the air in the tank allows us to find the rate of change of pressure:
\begin{align}
0 &= \frac{dm_a}{dt} \\
0 &= \frac{d}{dt} \left( \frac{P V_g}{R T} \right) \\
0 &= V_g \frac{dP}{dt} + P \frac{dV_g}{dt} \\
\therefore \frac{dP}{dt} &= -\frac{P}{V_g} \frac{dV_g}{dt}
\end{align}

Now, we can integrate this system of ODEs:

```python
from pint import UnitRegistry
ureg = UnitRegistry()
Q_ = ureg.Quantity

# initial conditions and constants
valve_resistance = Q_(10, 'psi/(gal/min)')
pressure_atmosphere = Q_(1, 'atm')

volume_tank = Q_(50, 'gal')
water_fraction = 0.8

pressure_initial = Q_(100, 'psi')
volume_water_initial = water_fraction * volume_tank
volume_air_initial = (1.0 - water_fraction) * volume_tank
```

```python
def tank_equations(t, y, valve_resistance, pressure_atmosphere):
    '''Rates of change for water volume, air volume, and air pressure in tank.

    Input values in SI units.
    '''
    volume_water = Q_(y[0], 'm^3')
    volume_air = Q_(y[1], 'm^3')
    pressure_air = Q_(y[2], 'Pa')

    dVf_dt = -(pressure_air - pressure_atmosphere) / valve_resistance
    dVg_dt = -dVf_dt
    dP_dt = -(pressure_air / volume_air) * dVg_dt

    return [
        dVf_dt.to('m^3/s').magnitude,
        dVg_dt.to('m^3/s').magnitude,
        dP_dt.to('Pa/s').magnitude
        ]
```

Now that we have the initial conditions and rate function set up, we can integrate in time. Let's do this for 500 seconds, and then plot the water volume and tank pressure as functions of time:

```python
from scipy.integrate import solve_ivp

# now integrate for 500 seconds, specifying the function, time interval,
#initial conditions, and additional arguments to the function

sol = solve_ivp(
    tank_equations, [0, 500.0],
    [volume_water_initial.to('m^3').magnitude,
     volume_air_initial.to('m^3').magnitude,
     pressure_initial.to('Pa').magnitude
     ],
    args=(valve_resistance, pressure_atmosphere,),
    method='BDF'
    )
```

```python
time = sol.t
volume_water = Q_(sol.y[0], 'm^3')
volume_air = Q_(sol.y[1], 'm^3')
pressure = Q_(sol.y[2], 'Pa')

plt.plot(time, pressure.to('psi').magnitude, label='Tank pressure (psi)')
plt.plot(time, volume_water.to('gal').magnitude, label='Water volume (gal)')
plt.grid(True)
plt.xlabel('Time (s)')
plt.ylabel('Water volume (gal) and tank pressure (psi)')
plt.ylim(ymin=0.0) # ensure lower y-axis bound is zero
plt.legend()
plt.show()
```

## Optimization

For some problems, we may want to find the input parameter(s) that minimize or maximize some function. In these cases, we can use the SciPy [`minimize_scalar()`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize_scalar.html) function for a scalar function with one input variable, or the [`minimize()`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html) function for a scalar function of one or more input variables.

(Note: this is a fairly complicated topic, and we will only consider relatively simple optimization problems.)

For example, consider a spring with a horizontal force applied; we can calculate the potential energy of the spring:
\begin{equation}
PE(x) = 0.5 k x^2 - F x \;.
\end{equation}
At equilibrium, the potential energy will be at a minimum, so given a particular force we can find the displacement by finding the value that minimizes the potential energy.

```python
from scipy.optimize import minimize_scalar

spring_constant = 2.0 # N/cm
force = 5.0 # N

def spring(x, k, F):
    '''Calculates potential energy of spring pulled by force.
    '''
    return 0.5 * k * x**2 - F * x

sol = minimize_scalar(spring, args=(spring_constant, force))

print(f'Equilibrium displacement: {sol.x: .2f} cm')
```

    Equilibrium displacement:  2.50 cm

A more complicated problem is a system with two springs, connected at one end with a force in some general direction (with $x$ and $y$ components); the system has both horizontal and vertical components. The springs have spring constants $k_a$ and $k_b$, and unloaded lengths $L_a$ and $L_b$.

The potential energy for this system is
\begin{equation}
PE(x,y) = 0.5 k_a \left( \sqrt{x^2 + (L_a - y)^2} - L_a \right)^2 + 0.5 k_b \left( \sqrt{x^2 + (L_b + y)^2} - L_b \right)^2 - F_x x - F_y y
\end{equation}
where $x$ and $y$ are the horizontal and vertical deformations from the unloaded state.
We can minimize this function of two variables to find the displacement based on the force:

```python
from scipy.optimize import minimize

spring_constant_a = 9.0 # N/cm
spring_constant_b = 2.0 # N/cm
length_a = 10.0 # cm
length_b = 10.0 # cm

force_x = 2.0 # N
force_y = 4.0 # N

def spring_system(xvec, ka, kb, La, Lb, Fx, Fy):
    '''Calculates potential energy of springs pulled by force.
    '''
    x = xvec[0]
    y = xvec[1]
    return (
        0.5*ka * (np.sqrt(x**2 + (La - y)**2) - La)**2 +
        0.5*kb * (np.sqrt(x**2 + (Lb + y)**2) - Lb)**2
        - (Fx * x) - (Fy * y)
        )

guesses = [1.0, 1.0]
sol = minimize(
    spring_system, guesses,
    args=(spring_constant_a, spring_constant_b,
          length_a, length_b, force_x, force_y
          )
    )

x = sol.x[0]
y = sol.x[1]
print(f'Equilibrium displacement: x={x: .2f} cm, y={y: .2f} cm')
```

    Equilibrium displacement: x= 4.95 cm, y= 1.28 cm

## Differentiation

Many thermodynamic properties are derivatives of other properties, so you may find the need to take derivatives of either data or of an expression. We can use the NumPy function [`gradient()`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.gradient.html) to take numerical derivatives of data using finite differences, and we can use [SymPy](https://www.sympy.org/en/index.html) to find analytical derivatives of expressions.

### Numerical derivatives

Let's say we have data for some function $f(x) = x^2$ at some locations $x$:

```python
x = np.linspace(0, 1, 10)
f = x**2

plt.plot(x, f)
plt.xlabel('x')
plt.ylabel('f(x)')
plt.grid(True)
plt.show()
```

We do not need to know the functional form of $f(x)$ to take the numerical derivatives—this example just uses a simple function for ease of checking the results.

The `gradient()` function uses second-order central differences to evaluate the derivative of input data, using forward and backward differences at the boundaries. Let's use that to get the derivative, and compare against the exact derivative ($\frac{df}{dx} = 2x$):

```python
dfdx = np.gradient(f, x)
dfdx_exact = 2*x

plt.plot(x, dfdx_exact, label='Exact')
plt.plot(x, dfdx, 'o', label='Numerical')
plt.xlabel('x')
plt.ylabel('df/dx')

plt.grid(True)
plt.legend()
plt.show()
```

The numerical derivative we obtain is very accurate, thanks to the linear nature of the derivative. However, at the boundaries the approximate derivative is a bit off, due to the first-order finite differences used there.

### Analytical derivative

In some cases we may be given (or obtain) analytical expressions that we need to differentiate. Given a function, we have two options for calculating the derivative:

1. Use the function to calculate values over the desired range, then numerically differentiate.
2. Obtain the exact derivative by differentiating analytically.

We'll now focus on the latter case; we can use SymPy to construct a function symbolically, and then find the exact derivative.

```python
import sympy
sympy.init_printing(use_latex='mathjax')

x = sympy.symbols('x', real=True)

f = x**2

# take derivative of f with respect to x
dfdx = sympy.diff(f, x)

# this complicated expression is only necessary for printing.
# It creates an equation object, that sets the unevaluated
# derivative of f equal to the calculated derivative.
display(sympy.Eq(sympy.Derivative(f), dfdx))
```

$\displaystyle \frac{d}{d x} x^{2} = 2 x$

Once we have evaluated the analytical derivative, we can even turn it into a Python function for evaluation! Passing the `'numpy'` argument creates a NumPy array-compatible function.

```python
calc_derivative = sympy.lambdify(x, dfdx, 'numpy')

x_vals = np.linspace(0, 1, 10)

plt.plot(x_vals, 2*x_vals, label='Exact derivative')
plt.plot(x_vals, calc_derivative(x_vals), 'o', label='SymPy derivative')

plt.xlabel('x')
plt.ylabel('df/dx')
plt.grid(True)
plt.legend()
plt.show()
```

As expected, the exact derivative we calculated using SymPy matches the known derivative perfectly—this was obtained symbolically, so there are no approximations/errors involved.

When taking the derivative of more complicated expressions that involve other functions (e.g., $\sin$, $\log$), you will need to use the SymPy-provided versions: `sympy.sin`, `sympy.log`, etc. We also need to explicitly define symbolic variables, either using `x = sympy.Symbol('x')` to create one variable at a time or `x, y = sympy.symbols('x y', real=True)` to create multiple variables at once. (The string you specify is the displayed representation of the variable, and can include complex formatting such as subscripts/superscripts and Greek letters.)

For example, let's take the derivative of
\begin{equation}
f(x) = \log(x) + x^3
\end{equation}

```python
x = sympy.Symbol('x')
f = sympy.log(x) + x**3

dfdx = sympy.diff(f, x)

display(dfdx)
```

$\displaystyle 3 x^{2} + \frac{1}{x}$