Universidad Nacional de Córdoba - Facultad de Matemática, Astronomía, Física y Computación

Diplomatura en Ciencia de Datos, Aprendizaje Automático y sus Aplicaciones

 Regresión Linear - Ejemplo

 Análisis y Visualización de Datos - 2019

En este ejemplo veremos cómo implementar una regresión logística para predecir una variable numérica. Volveremos a utilizar el dataset [Human Freedom Index 2018](https://www.cato.org/human-freedom-index-new) de el instituto Cato.

Usaremos una [versión ya limpia del dataset](https://www.kaggle.com/gsutters/the-human-freedom-index/home) que pueden descargar desde Kaggle.

En esta notebook vamos a tratar de estimar una función lineal que modele el cambio a través del tiempo de la libertad personal y la económica.

```python
%matplotlib inline
import matplotlib.pyplot as plt
import numpy
import pandas
import seaborn
```

```python
seaborn.set_context(context='talk', font_scale=1.2)
```

```python
BLUE = '#35A7FF'
RED = '#FF5964'
GREEN = '#6BF178'
YELLOW = '#FFE74C'
```

```python
dataset = pandas.read_csv('../datasets/hfi_cc_2018.csv')
dataset.shape
```

    (1458, 123)

En el práctico habíamos trabajado sobre las variables `ef_score` y `pf_score`, que hacen referencia a los índices de libertad personal y libertad económica de cada páis. Además, sabemos que el dataset incluye una medición del índice anual por país desde 2008 hasta 2016, aunque hay datos faltantes de algunos indicadores.

La motivación de este análisis comienza con este gráfico, que muestra una tendencia decreciente de la libertad personal y una tendencia ascendiente de la libertad económica. La libertad humana o `pf_score` es el promedio de ambos indicadores

```python
plt.figure(figsize=(10,6))
seaborn.lineplot(data=dataset,
                 x='year', y='ef_score',
                 label='Economic Freedom', ci='sd')
seaborn.lineplot(data=dataset,
                 x='year', y='pf_score',
                 label='Personal Freedom', ci='sd')

seaborn.lineplot(data=dataset,
                 x='year', y='hf_score',
                 label='Human Freedom', ci='sd')
plt.legend()
plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)
plt.ylabel('Index score')
seaborn.despine()
```

Este fenómeno podría estar dado por varios factores:
  * Hay pocos países en los que la libertad personal está decreciendo, pero su libertad económica se mantiene constante.
  * Los países para los cuales sube la libertad económica decrecen en libertad personal.
  * **¿Otras?**

Veamos qué sucede en Argentina. Si graficamos ambas variables, vemos que "van bajando". Formalmente, esto significa que hay la recta que las modela tiene una pendiente negativa.

**¿Y esto, es grave?**

```python
seaborn.regplot(data=dataset[(dataset.ISO_code == 'ARG')],
                    x='year', y='ef_score', label='Economic Freedom')
seaborn.regplot(data=dataset[(dataset.ISO_code == 'ARG')],
                    x='year', y='pf_score', label='Personal Freedom')
seaborn.regplot(data=dataset[(dataset.ISO_code == 'ARG')],
                    x='year', y='hf_score', label='Human Freedom')
plt.legend()
plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)
seaborn.despine()
plt.xlim(2008, 2016)
```

Podemos graficar varios países, pero es difícil comparar visualmente entre tantas variables, qué países "decrecen" más rápido que otros.

```python
countries = ['Argentina', 'Brazil', 'Mexico', 'Bolivia',
             'Uruguay', 'Peru', 'Colombia', 'Venezuela']
g = seaborn.FacetGrid(dataset, col="countries",
                      margin_titles=True, aspect=2, col_wrap=2,
                      col_order=countries)
g.map(seaborn.lineplot, "year", "ef_score", color=BLUE)
g.map(seaborn.lineplot, "year", "pf_score", color=RED)
g.set(xlim=(2008, 2016))

print('Scores de libertad para Medio Oriente');
```

Para poder comparar la situación de Argentina con otros países, podemos comparar la pendiente de la recta de la regresión lineal. A partir del gráfico anterior pudimos ver que la mayoría de los países tiene tendencias similares y que se pueden estimar con una recta sin perder generalidad. Esto es posible también, en cierta medida, porque tenemos pocos puntos para estimar.

## Regresión lineal

Queremos ver cuál es el coeficiente que relaciona ambas variables.

Primero: **¿Cuál es la variable dependiente? ¿Cuál la independiente?**

Una vez que hemos determinado eso, lo que queremos encontrar es la función de la siguiente forma:

$$ef = a * year + b$$

Reescribiremos esto como una función $e$ (por economic), cuyo parámetro es el valor $y$ (por year):

$$e(y) = a * y + b$$

Vamos a describir los ejemplos como pares $(x_y, x_e)$, donde $x_y$ denota el `year` y $x_e$ denota `ef_score`.

Para encontrar la recta $e$ que mejor describe los datos, queremos minimizar el error cuadrático medio, definido como:

$$mse = \frac{1}{|X|} \sum_{x \in X} (e(x_y) - x_e)^2 $$

Recordemos que para minimizar una función, una buena opción es comenzar por buscar los puntos estacionarios, donde la derivada se anula. Por suerte, la función $mse$ es convexa, y por lo tanto tiene todos sus puntos estacionarios son minimizadores. El minimizador es el valor de los parámetros $a$ y $b$ que minimizan la función. Ahora, en hemos cambiado nuestras "variables", lo que buscamos es encontrar la función adecuada, por lo tanto lo que cambia son los valores de los parámetros que definen la función.

Primero, notemos que:

$$\frac{\partial}{\partial a}e(y) = x_p$$

$$\frac{\partial}{\partial b}e(y) = 1$$

Con eso, calculamos las derivadas parciales para cada parámetro de la función $mse$.

$$\frac{\partial}{\partial a}mse = \frac{2}{|X|} \sum_{x \in X} (e(x_p) - x_e) \frac{\partial}{\partial a} (e(x_p) - x_e) = \frac{2}{|X|} \sum_{x \in X} (e(x_p) - x_e) e_p $$

$$\frac{\partial}{\partial b}mse = \frac{2}{|X|} \sum_{x \in X} \frac{\partial}{\partial b} e(x_p) - x_e = \frac{2}{|X|} \sum_{x \in X} e(x_p) - x_e $$

A pesar del formulerío, es bastante simple. Sólo reemplazamos $mse$ por su definición, y luego aplicamos un par de reglas como "la derivada de la suma es la suma de las derivadas", la regla de la cadena, o la definición de la derivada de la función cuadrática.

Una vez que tenemos esos valores, tenemos que igualarlos a cero para encontrar los puntos estacionarios.

\begin{align}
 \frac{\partial}{\partial a}mse &= \frac{2}{|X|} \sum_{x \in X} (e(x_y) - x_e) x_y = 0 \\
                                &\Rightarrow a = \frac{\bar{x_y} \bar{x_e} - \overline{x_yx_e}}{(\bar{x_y})^2 - \overline{x_y^2}}
\end{align}

\begin{align}
 \frac{\partial}{\partial b}mse &= \frac{2}{|X|} \sum_{x \in X} e(x_y) - x_e = 0 \\
                                &\Rightarrow b = \bar{x_e} - a \bar{x_y}
\end{align}

Donde $\bar{x}$ es la media del valor para todos los ejemplos. Vamos a confiar en estas fórmulas, pero una demostración de las mismas está en:

https://medium.freecodecamp.org/machine-learning-mean-squared-error-regression-line-c7dde9a26b93

```python
def estimate_params(X_y, X_e):
    """Caculates the value of a using all the examples."""
    num = numpy.mean(X_y)*numpy.mean(X_e) - numpy.mean(numpy.multiply(X_y, X_e))
    denom = numpy.mean(X_y)**2 - numpy.mean(numpy.multiply(X_y, X_y))
    a = num / denom
    b = numpy.mean(X_e) - a * numpy.mean(X_y)
    return a, b
```

```python
# Asumimos que todos los registros que tienen pf_score tienen el año.
a, b = estimate_params(
    dataset[(dataset.ISO_code == 'ARG') &
            (dataset.pf_score.notnull())].year.dropna(),
    dataset[dataset.ISO_code == 'ARG'].pf_score)
a, b
```

    (-0.019938222416395018, 48.23648292700899)

```python
def base_linear_regression(x_y, a):
    return a * x_y
```

```python
def regplot2(data, x, y, reg_func, **reg_func_args):
    """Plots the x, y columns from data and builds a
    line with the regression reg_func."""
    seaborn.scatterplot(data=data, x=x, y=y, color=BLUE)
    minimum = data[x].min()
    maximum = data[x].max()
    plt.plot([minimum, maximum],
             [reg_func(minimum, **reg_func_args),
              reg_func(maximum, **reg_func_args)],
             color=GREEN)
    seaborn.despine()
    plt.show()
```

```python
regplot2(dataset[dataset.ISO_code == 'ARG'],
         x='year', y='pf_score', reg_func=base_linear_regression,
         a=a)
```

Vemos que la recta va en el sentido correcto, pero está demasiado abajo. Esto ocurre porque no hemos usado el término de bias.

Redefinamos entonces la regresión logística

```python
def linear_regression(x_y, a, b):
    return a * x_y + b
```

```python
regplot2(dataset[dataset.ISO_code == 'ARG'],
         x='year', y='pf_score', reg_func=linear_regression,
         a=a, b=b)
```

## Continuamos el análisis

Perfecto! Ahora podemos calcular las pendientes y los biases para todos los años, para regresiones que estimen el `pf_score`.

```python
def build_regressions(data, x_var='year', y_var='pf_score'):
    records = []
    for code in data.ISO_code.unique():
        record = [code, data[data.ISO_code == code].region.values[0],
                  data[data.ISO_code == code].countries.values[0]]
        y_data = data[data.ISO_code == code][y_var].dropna()
        # Comprobamos que hay datos en el intervalo
        if len(y_data) <= 1:
            continue
        x_data = data[(data.ISO_code == code) &
                      (data[y_var].notnull())][x_var].dropna()
        # Estimamos los parámetros
        a, b = estimate_params(x_data, y_data)
        # Calculamos el error cuadrático medio de la regresión lineal estimada
        predictions = numpy.apply_along_axis(
            lambda x: linear_regression(x, a, b), 0, x_data)
        mse = numpy.mean(numpy.power(predictions - y_data, 2))
        record.extend([a, b, mse])
        # Agregamos el registro
        records.append(record)
    return pandas.DataFrame.from_records(
        records, columns=['ISO_code', 'region', 'country',
                          'slope', 'bias', 'mse']
    )
```

```python
pf_regressions = build_regressions(dataset).set_index('ISO_code')
pf_regressions[:10]
```

      region
      country
      slope
      bias
      mse

      ISO_code

      ALB
      Eastern Europe
      Albania
      -0.030238
      68.536465
      0.005275

      DZA
      Middle East & North Africa
      Algeria
      0.020887
      -36.776002
      0.005174

      AGO
      Sub-Saharan Africa
      Angola
      0.152157
      -300.282847
      0.128365

      ARG
      Latin America & the Caribbean
      Argentina
      -0.019938
      48.236483
      0.006254

      ARM
      Caucasus & Central Asia
      Armenia
      -0.075801
      159.703656
      0.012481

      AUS
      Oceania
      Australia
      -0.009244
      27.829172
      0.002955

      AUT
      Western Europe
      Austria
      0.046339
      -84.027982
      0.005218

      AZE
      Caucasus & Central Asia
      Azerbaijan
      -0.127696
      262.997442
      0.020314

      BHS
      Latin America & the Caribbean
      Bahamas
      -0.095505
      199.892810
      0.011764

      BHR
      Middle East & North Africa
      Bahrain
      -0.091655
      190.607798
      0.061043

```python
plt.figure(figsize=(10,6))
seaborn.distplot(
    pf_regressions[pf_regressions.region == 'Latin America & the Caribbean'].slope,
    color=BLUE, label='Latam y Caribe')
seaborn.distplot(pf_regressions.slope, color=RED, label='Global')
plt.xlabel('Slope of linear regression that fits the pf_score of each country')
plt.legend()
seaborn.despine()
```

```python
def plot_regressions(regressions):
    plt.figure(figsize=(10,6))
    colors = seaborn.color_palette("cubehelix", len(regressions))
    for color, (year, row) in zip(colors, regressions.iterrows()):
        minimum, maximum = 2008, 2016
        plt.plot([minimum, maximum],
                 [linear_regression(minimum, row['slope'], row['bias']),
                  linear_regression(maximum, row['slope'], row['bias'])],
                 color=color, label=str(year), linestyle='--')
    plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)
    seaborn.despine()
```

```python
plot_regressions(pf_regressions.loc[['ARG', 'BRA', 'VEN', 'CAN']])
plt.xlabel('year')
plt.ylabel('pf_score')
plt.ylim(4, 10)
```

### Libertad Económica

```python
ef_regressions = build_regressions(dataset, y_var='ef_score').set_index('ISO_code')
ef_regressions[:10]
```

      region
      country
      slope
      bias
      mse

      ISO_code

      ALB
      Eastern Europe
      Albania
      0.037167
      -67.430444
      0.006890

      DZA
      Middle East & North Africa
      Algeria
      -0.031167
      67.739556
      0.003564

      AGO
      Sub-Saharan Africa
      Angola
      0.061500
      -118.595778
      0.050513

      ARG
      Latin America & the Caribbean
      Argentina
      -0.159667
      326.482667
      0.045977

      ARM
      Caucasus & Central Asia
      Armenia
      0.005000
      -2.425556
      0.005080

      AUS
      Oceania
      Australia
      -0.006667
      21.408889
      0.001528

      AUT
      Western Europe
      Austria
      -0.007333
      22.396889
      0.000770

      AZE
      Caucasus & Central Asia
      Azerbaijan
      0.053833
      -102.077111
      0.007016

      BHS
      Latin America & the Caribbean
      Bahamas
      -0.024833
      57.344667
      0.003578

      BHR
      Middle East & North Africa
      Bahrain
      0.007000
      -6.681778
      0.006713

```python
plot_regressions(ef_regressions.loc[['ARG', 'BRA', 'VEN', 'CAN']])
plt.xlabel('year')
plt.ylabel('ef_score')
```

```python
plt.figure(figsize=(10,6))
seaborn.distplot(
    ef_regressions[ef_regressions.region == 'Latin America & the Caribbean'].slope,
    color=BLUE, label='Latam y Caribe')
seaborn.distplot(ef_regressions.slope, color=RED, label='Global')
plt.xlabel('Slope of linear regression that fits the ef_score of each country')
plt.legend()
seaborn.despine()
```

## Análisis conjunto

**¿Cuáles es el 10% de países en los que la libertad humana disminuye más rápidamente?**

```python
quantil = pf_regressions.slope.quantile(0.1)
pf_regressions[pf_regressions.slope < quantil].country
```

    ISO_code
    AZE           Azerbaijan
    BRA               Brazil
    BRN    Brunei Darussalam
    BDI              Burundi
    EGY                Egypt
    GIN               Guinea
    LBY                Libya
    NIC            Nicaragua
    NER                Niger
    PHL          Philippines
    SYR                Syria
    TJK           Tajikistan
    THA             Thailand
    TLS          Timor-Leste
    UKR              Ukraine
    YEM          Yemen, Rep.
    Name: country, dtype: object

**¿Cuáles es el 10% de países en los que la libertad económica disminuye más rápidamente?**

```python
quantil = ef_regressions.slope.quantile(0.1)
ef_regressions[ef_regressions.slope < quantil].country
```

    ISO_code
    ARG          Argentina
    BRA             Brazil
    EGY              Egypt
    FJI               Fiji
    GHA              Ghana
    GRC             Greece
    IRN               Iran
    KWT             Kuwait
    LBR            Liberia
    MWI             Malawi
    NAM            Namibia
    PNG    Pap. New Guinea
    SYR              Syria
    TUN            Tunisia
    VEN          Venezuela
    ZMB             Zambia
    Name: country, dtype: object

**¿Cuáles son los paises en los que la libertad económica aumenta pero la libertad personal disminuye (rápidamente)?**

```python
all_countries = dataset.ISO_code.unique()
codes = []
for code in all_countries:
    if (code in ef_regressions.index and code in pf_regressions.index and
        ef_regressions.loc[code].slope > 0.02 and
        pf_regressions.loc[code].slope < -0.02):
        codes.append(code)
ef_regressions.loc[codes].country
```

    ISO_code
    ALB           Albania
    AZE        Azerbaijan
    BTN            Bhutan
    BDI           Burundi
    KHM          Cambodia
    CPV        Cape Verde
    CHN             China
    COD    Congo, Dem. R.
    GMB       Gambia, The
    GEO           Georgia
    GTM         Guatemala
    GIN            Guinea
    IDN         Indonesia
    ISR            Israel
    KAZ        Kazakhstan
    MKD         Macedonia
    MYS          Malaysia
    MEX            Mexico
    MMR           Myanmar
    NPL             Nepal
    NIC         Nicaragua
    NER             Niger
    NGA           Nigeria
    PRY          Paraguay
    PHL       Philippines
    POL            Poland
    RUS            Russia
    RWA            Rwanda
    ESP             Spain
    TJK        Tajikistan
    TZA          Tanzania
    TLS       Timor-Leste
    YEM       Yemen, Rep.
    Name: country, dtype: object

# Errores

Calculamos el mse pero nunca lo usamos. Veamos cómo son los países para los que la regresión linear no produce una buena aproximación

```python
pf_regressions.mse.sort_values()[-10:]
```

    ISO_code
    GUY    0.110310
    SLE    0.119609
    UKR    0.124567
    CAF    0.126684
    AGO    0.128365
    IND    0.128720
    VEN    0.130987
    BRN    0.153219
    MMR    0.168191
    BDI    0.221579
    Name: mse, dtype: float64

```python
plt.figure(figsize=(10,6))
countries = ['BDI', 'VEN', 'IND', 'UKR']
seaborn.lineplot(data=dataset[dataset.ISO_code.isin(countries)], x='year', y='hf_score',
                 hue='countries')
plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)
seaborn.despine()
```

Claramente se ve que estas funciones no podían ser estimadas satisfactoriamente con una recta, pero aún así, la tendencia general (descendiente o ascendiente) habría sido aproximada


## Lista de Exercício 02 - Fundamentos de Matemática para Computação.

# Universidade Federal do ABC - UFABC
## Centro de Matemática, Computação e Cognição - CMCC

## Disciplina:  Fundamentos de Matemática para a Computação -  CCM-007

Responsável: Prof. Dr. Saul Leite

Estudantes: Bruno Aristimunha.

Santo André, 26 de Fevereiro de 2019

### Primeira Parte do Curso

#### Objetivos

O objetivo dessa lista é explorar e aplicar os conceitos de sala de aula sobre **Polinômio de Taylor**.

Para alcançar esse objetivo a resolução dos exercícios é necessária.

```
import matplotlib.pylab as plt

import sympy as sy

import numpy as np

from scipy.special import factorial
```

___
## Exercício 01.

Encontre o polinômio de Taylor de grau 1, 2, 3, e 4 para a função $f (x) = e^{x^{2}}$ , expandindo em torno de $x_0 = 0$. Usando Python, faça os gráficos dos polinômios e da função.
___

$f(x) = e^{x^{2}}$

$f'(x) = \frac{d(e^{x^{2}})}{dx} = e^{x^{2}}\cdot 2 \cdot = 2 \cdot x \cdot e^{x^{2}}$

$f^{''}(x) = \frac{d (2 \cdot x \cdot e^{x^{2}})}{dx} = 2 \cdot (e^{x^{2}} + x\cdot e^{x^{2}} \cdot 2x) =  2\cdot e^{x^{2}} + 2\cdot x \cdot e^{x^{2}} = 2 \cdot e^{x^2} (1 + 2x^2) $

$f^{(3)}(x) = \frac{d (2\cdot e^{x^2} \cdot (1+2x^2))}{dx} = 2 \cdot e^{x^{2}} \cdot 2\cdot x (1+2x^2) + 2 \cdot e^{x^{2}} (4x) = 4 e^2\cdot x + 8x^3 \cdot e^{x^2} + 8\cdot e^{x^{2}}\cdot x $

$\qquad \quad = 12x\cdot e^{x^{2}} + 8 x^3 e^{x^2} = 4 x e^{x^2} (3 + 2x^2)$

$f^{(4)} = 12 e^{x^2} + 12xe^{x^2}\cdot 2x + 24 x^2 e^{x^2} + 8x^3 e^{x^2}2x $

$f^{(4)} = 12 e^{x^2} + 48xe^{x^2} + 16x^3 e^{x^2}$

Para $x_0 = 0$.

> $f(x_0) = f(0) = e^{0^2} = 1$

> $f'(x_0) = f'(0) = 2\cdot 0 \cdot e^{0^{2}} = 0 $

> $f''(x_0) = 2 \cdot e^{0^{2}} (1 + 2\cdot 0^{2}) = 2\cdot 1 = 2 $

> $f^{(3)}(x_0) = 4\cdot 0 \cdot e^{x^{2}} \cdot (3+2\cdot 0^2) = 0$

> $f^{(4)}(x_0) = 16\cdot 0^{4} \cdot e^{x^{2}} + 48\cdot 0^2 \cdot e^{0^2} + 12\cdot  e^{0^²}) = 0$

Grau 1:

>$P_n (x) = f(x_0) + f'(x_0)(x-x_0)$

>$P_1 (x) = 1 + 0\cdot (x-0) = 1$

Grau 2:

>$P_2(x) = f(x_0) + f'(x_0)(x-x_0)) + f^{''}(x) \frac{(x-x_0)^2}{2}$

>$P_2(x) = 1 + 0\cdot (x-0) + 2 \frac{(x-0)^2}{2} = 1 + 0 + x² = x²+1$

Grau 3:

>$P_3(x) = 1+ x^2 $

Grau 4:

  > $P_4(x) = 1 +x^2 +12 \cdot \frac{x^{4}}{24} = 1 + x^2 + \frac{x^4}{2}$

```
 plt.style.use("ggplot")

x1 = np.linspace(-1, 1, 10)

f = [lambda x: np.e**x**2,    # função original
   lambda x: 1,               # polinômio de Taylor de grau 1
   lambda x: x**2 + 1,        # polinômio de Taylor de grau 2
   lambda x: 1 + x**2,        # polinômio de Taylor de grau 3
   lambda x: 1 + x**2 +x**4   # polinômio de Taylor de grau 4
  ]

```

```
plt.plot(list(map(f[0],x1)),label = 'função original',c ='r')
plt.plot(list(map(f[1],x1)),label = '1 grau'  ,c = 'b')
plt.plot(list(map(f[2],x1)),label = '2 grau'  ,c = 'y')
plt.plot(list(map(f[3],x1)),label = '3 grau'  ,c = 'g')
plt.plot(list(map(f[4],x1)),label = '4 grau', c ='k')

plt.legend(loc='upper center', fontsize='x-large')

plt.show()
```

---

## Exercício 02.

E2: Use os polinômios de Taylor para mostrar que $(1 + t)^n  = \sum_{j=0}^n {n\choose j} \cdot t^{j}$, para $n$ inteiro e maior do que 1 e onde é o coeficiente binomial

---

Seja:

$f(t)=(1+t)^n\\$

$f'(t) = n \cdot (1+t)^{n-1}\\$

$f''(t) = n \cdot (n-1)\cdot (1+t)^{n-2}\\$

$f'''(t) = n \cdot (n-1) \cdot (n-2)  \cdot (1+t)^{n-3}$

Do caso geral do polinômio de Taylor temos que:

$\sum_{k=0}^n f^{(k)}(t_0) \frac{(t-t_0)^k}{k!}$

Partindo o polinômio de Taylor, seja uma $f(x) = x^n$ polinomial qualquer. Ao expandir o polinômio, temos que:

>$f(x-\varepsilon) = x^n + n x^{n-1} \varepsilon + (n(n+1))x^{n-2}\frac{\varepsilon}{2!} + \dots + n! x^{0} \frac{\varepsilon^{n}}{n!} +  \varnothing$

Seja $x = 1$.

> $1 + n \varepsilon +n(n-1) \frac{\varepsilon^2}{2!} + n(n-1)(n-2) \frac{\varepsilon^3}{3!} + \dots + n! \frac{\varepsilon^n}{n!}$

Manipulando algebricamente temos:
> $\frac{n!}{n!}  + \frac{n!}{1!(n-1)!}\varepsilon + \frac{n!}{(n-2)!}\frac{\varepsilon^2}{2!} + \frac{n!}{(n-3)!}\frac{\varepsilon^3}{3!} + \dots + \frac{n!}{n! 1!} \varepsilon^n$

Ou seja:

>$\sum^n_{j=0} {n \choose j} \varepsilon^j$.

Com $\varepsilon = t$, C.Q.D.

---

## Exercício 03.

E3: Usando a linguagem Python, criar uma função para calcular $f (x) = sin(x)$ utilizando os polinômios de Taylor em torno de $x_0 = 0$. O polinômio deve ter o menor grau possível de modo que o erro da aproximação seja inferior a $10^{−7}$ para qualquer valor de $x$. (Dica: o valor de $x$, argumento da sua função, pode ser transformado antes de operar o polinômio.)

---

Inicialmente precisamos descobri qual o grau minímo do polinômio de Taylor para aproximar com a precisão com resíduo igual $10^{-7}$.

$R_{n+1} \leq 10^{-7}$, para todo $x\in[-\pi,\pi]$

Observe:

$|R_n(x)| = f^{n+1} (\xi_n) \frac{\varepsilon^{n+1}}{(n+1)!}$

$|R_{n+1}(x)| \leq |\sin(\xi)| \left| \frac{x^{n+1}}{(n+1)!} \right|$

$|R_{n+1}(x) |\leq \max_{t\in [-\pi,\pi]}|\sin(t)| \cdot  \max_{y\in [-\pi,\pi]} \left| \frac{y^{n+1}}{(n+1)!} \right|$

Apesar da derivada da função $\sin$ depender do $n$ do resíduo, o máximo nesse intervalo será $1$.

$|R_{n+1}(x) |\leq 1 \cdot \left| \frac{\pi^{n+1}}{(n+1)!} \right|$, para todo $x \in [-\pi, \pi]$

Dessa forma:

$(n+1)! \geq 10^7 \left( \pi^{n+1} \right)$

```
fd

condicao = False

while not condicao:
  condicao = (factorial(n+1) >=10**7 *(np.pi)**(n+1))
  print("teste com n =",n,"é: ", condicao)
  n = n+1
```

    teste com n = 1 é:  False
    teste com n = 2 é:  False
    teste com n = 3 é:  False
    teste com n = 4 é:  False
    teste com n = 5 é:  False
    teste com n = 6 é:  False
    teste com n = 7 é:  False
    teste com n = 8 é:  False
    teste com n = 9 é:  False
    teste com n = 10 é:  False
    teste com n = 11 é:  False
    teste com n = 12 é:  False
    teste com n = 13 é:  False
    teste com n = 14 é:  False
    teste com n = 15 é:  False
    teste com n = 16 é:  False
    teste com n = 17 é:  False
    teste com n = 18 é:  True

Dessa forma, o nosso polinômio será de grau 9.

```
from sympy.functions import sin

#Implementação retirada de: http://firsttimeprogrammer.blogspot.com/2015/03/taylor-series-with-python-and-sympy.html

# Define the variable and the function to approximate
x = sy.Symbol('x')
f_sen = sin(x)

# Taylor approximation at x0 of the function 'function'
def taylor(function,x0,n):
    i = 0
    p = 0
    while i <= n:
        p = p + (function.diff(x,i).subs(x,x0))/(factorial(i))*(x-x0)**i
        i += 1
    return p

```

```
x2 = np.arange(-np.pi,np.pi, 0.00001)

taylor(f_sen,0,18)
```

    2.81145725434552e-15*x**17 - 7.64716373181982e-13*x**15 + 1.60590438368216e-10*x**13 - 2.50521083854417e-8*x**11 + 2.75573192239859e-6*x**9 - 0.000198412698412698*x**7 + 0.00833333333333333*x**5 - 0.166666666666667*x**3 + 1.0*x

```
f_taylor_18 = lambda x: 2.81145725434552e-15*x**17 - 7.64716373181982e-13*x**15 + 1.60590438368216e-10*x**13 - 2.50521083854417e-8*x**11 + 2.75573192239859e-6*x**9 - 0.000198412698412698*x**7 + 0.00833333333333333*x**5 - 0.166666666666667*x**3 + 1.0*x

```

```
plt.plot(f_taylor_18(x2), c='b')
plt.plot(np.sin(x2), c='r')

```

Soma das diferenças no intervalo

### Dentro do erro esperado :) O valor de $\pi$ usado será esse:

```
sum(f_taylor_18(x2)-np.sin(x2))
```

    -1.1898375539887341e-08

### Dado a periodicidade da função $\sin$, para cada valor recebido basta retirar o módulo de $2\pi$.

Testando para duas vezes o valor de $\pi$ temos que:

```
entrada = float(input("Informe o valor de x que você deseja calcular sen(x)"))
```

    Informe o valor de x que você deseja calcular sen(x)-6.283185307179586

```
f_taylor_18(entrada%(2*np.pi))
```

    0.0


# Solving elasticity problems using JuliaFEM

Author(s): Jukka Aho

**Abstract**: Solving elasticity equations using JuliaFEM.

### Weak form

Given function spaces
\begin{align}
\boldsymbol{\mathcal{U}} & =\left\{ \boldsymbol{u}\in H^{1}\left(\Omega\right)|\boldsymbol{u}\left(\boldsymbol{X},t\right)=\hat{\boldsymbol{u}}\left(\boldsymbol{X},t\right)\text{ on }\Gamma_{\mathrm{u}}\right\} ,\\
\boldsymbol{\mathcal{V}} & =\left\{ \delta\boldsymbol{u}\in H^{1}\left(\Omega\right)|\delta\boldsymbol{u}\left(\boldsymbol{X}\right)=0\text{ on }\Gamma_{\mathrm{u}}\right\} ,
\end{align}
find $\boldsymbol{u}\in\boldsymbol{\mathcal{U}}$ such that
\begin{equation}
\delta\mathcal{W}:=\int_{\Omega_{0}}\rho_{0}\ddot{\boldsymbol{u}}\cdot\delta\boldsymbol{u}\,\mathrm{d}V_{0}+\int_{\Omega_{0}}\boldsymbol{S}:\delta\boldsymbol{E}\,\mathrm{d}V_{0}-\int_{\Omega_{0}}\hat{\boldsymbol{b}}_{0}\cdot\delta\boldsymbol{u}\,\mathrm{d}V_{0}-\int_{\Gamma_{\sigma}}\hat{\boldsymbol{t}}_{0}\cdot\delta\boldsymbol{u}\,\mathrm{d}A_{0} =0 \qquad\forall\delta\boldsymbol{u}\in\boldsymbol{\mathcal{V}}
\end{equation}

### Some formulas
\begin{align}
J & =\det\left(F\right)\\
I_{c} & =\mbox{tr}\left(C\right)\\
\mathbf{C} & =\mathbf{F}^{\mathrm{T}}\mathbf{F}\\
\mathbf{F} & =\mathbf{I}+\nabla\mathbf{u}\\
\mathbf{E} & =\frac{1}{2}\left(\mathbf{F}^{\mathrm{T}}\mathbf{F}-\mathbf{I}\right)
\end{align}

### Potential energy

\begin{equation}
\underset{u\in\boldsymbol{\mathcal{U}}}{\min}\Pi\left(\mathbf{u}\right)
\end{equation}
\begin{equation}
\Pi\left(\mathbf{u}\right)=\int_{\Omega}\psi\left(\mathbf{u}\right)-\int_{\Omega}\hat{\mathbf{b}}_{0}\cdot\mathbf{u}-\int_{\Gamma_{\sigma}}\hat{\mathbf{t}}_{0}\cdot\mathbf{u}\,\mathrm{d}A_{0}
\end{equation}

### Material models

https://en.wikipedia.org/wiki/Strain_energy_density_function

Saint-Venant-Kirchhoff model https://en.wikipedia.org/wiki/Hyperelastic_material
\begin{equation}
\psi\left(\mathbf{E}\right)=\frac{\lambda}{2}\left[\mbox{tr}\left(\mathbf{E}\right)\right]^{2}+\mu\mbox{tr}\left(\mathbf{E}^2\right)
\end{equation}

neo-Hookean material https://en.wikipedia.org/wiki/Neo-Hookean_solid
\begin{equation}
\psi=\frac{\mu}{2}\left(I_{c}-3\right)-\mu\ln\left(J\right)+\frac{\lambda}{2}\ln\left(J\right)^{2}
\end{equation}

```julia
using JuliaFEM.API: Model, Material, add_material!, LoadCase, add_element_set!, ForceBC, DisplacementBC
using JuliaFEM.API: add_boundary_condition!, add_load_case!, add_solver!
using JuliaFEM.Interfaces: solve!
using JuliaFEM.Preprocess: parse_abaqus
```

```julia
@time begin
    # Linear models
    #model = open(JuliaFEM.Core.parse_abaqus, "../geometry/piston/piston_8789_P1.inp")
    #model = open(JuliaFEM.Core.parse_abaqus, "../geometry/piston/piston_16436_P1.inp")
    #model = open(JuliaFEM.Core.parse_abaqus, "../geometry/piston/piston_27343_P1.inp")
    #model = open(JuliaFEM.Core.parse_abaqus, "../geometry/piston/piston_45510_P1.inp")
    #model = open(JuliaFEM.Core.parse_abaqus, "../geometry/piston/piston_75470_P1.inp")
    #model = open(JuliaFEM.Core.parse_abaqus, "../geometry/wrench/wrench_128903_P1.inp")

    # Quadratic models
    #model = open(JuliaFEM.Core.parse_abaqus, "../geometry/piston/piston_19611_P2.inp")
    #model = open(JuliaFEM.Core.parse_abaqus, "../geometry/piston/piston_55950_P2.inp")
    #model = open(JuliaFEM.Core.parse_abaqus, "/Temp/piston_107168_P2.inp")
    #model = open(JuliaFEM.Core.parse_abaqus, "../geometry/piston/piston_345757_P2.inp")

    mesh = open(parse_abaqus, "/Temp/piston_107168_P2.inp")
    model = Model("piston model", mesh)
    info("model loaded.")
end
```

    INFO: Parsing nodes
    INFO: Parsing elements. Type: Tet10
    INFO: Parsing elements. Type: Tri6
    INFO: Creating elset BC1
    INFO: Creating elset BC2
    INFO: model loaded.

      4.209444 seconds (19.12 M allocations: 565.872 MB, 11.55% gc time)

```julia
mat = Material("steel")
mat["youngs modulus"] = 210.0e3
mat["poissons ratio"] = 0.3
add_material!(model, "PISTON", mat)
```

    JuliaFEM.API.Material("steel",Dict("poissons ratio"=>0.3,"youngs modulus"=>210000.0))

```julia
simulation = LoadCase(:ElasticityProblem)

# simulation = Simulation(:ElasticityProblem, "elasticity equations")
# add_element_set!(simulation, "PISTON")
# add_boundary_condition!(simulation, bc1, bc2)
# add_simulation!(model, simulation)
# add_solver!(simulation, :DirectSolver)
# solve!(model, "elasticity equations", 0.0)
# solve!(model, "heat equations", 0.0)
# solve!(model, "solve equations 3", 0.0)

add_element_set!(simulation, "PISTON")
```

    1-element Array{ASCIIString,1}:
     "PISTON"

```julia
traction = Vector{Float64}[[0.0, 0.0, -10.0] for i in 1:6]
bc1 = ForceBC("BC1", "displacement traction force" => traction)
bc2 = DisplacementBC("BC2", "displacement" => 0.0)

# JuliaFEM.API.add_boundary_condition!(prob, bc1, bc2)
add_boundary_condition!(simulation, bc1)
add_boundary_condition!(simulation, bc2)
```

    1-element Array{JuliaFEM.API.DirichletBC,1}:
     JuliaFEM.API.DirichletBC("BC2","displacement"=>0.0)

```julia
add_solver!(simulation, :DirectSolver)
add_load_case!(model, "elasticity equations", simulation)
```

    JuliaFEM.API.LoadCase(:ElasticityProblem,[JuliaFEM.API.NeumannBC("BC1","displacement traction force"=>[[0.0,0.0,-10.0],[0.0,0.0,-10.0],[0.0,0.0,-10.0],[0.0,0.0,-10.0],[0.0,0.0,-10.0],[0.0,0.0,-10.0]])],[JuliaFEM.API.DirichletBC("BC2","displacement"=>0.0)],:DirectSolver,ASCIIString["PISTON"])

```julia
solver = JuliaFEM.Interfaces.get_solver(model, "elasticity equations", 0.0);
```

```julia
celements = solver.field_problems[1].elements;
celements[1]
```

    JuliaFEM.Core.Element{JuliaFEM.Core.Tet10}([1,2,3,4,5,6,7,8,9,10],Dict{ASCIIString,JuliaFEM.Core.Field{A<:Union{JuliaFEM.Core.Continuous,JuliaFEM.Core.Discrete},B<:Union{JuliaFEM.Core.Constant,JuliaFEM.Core.Variable},C<:Union{JuliaFEM.Core.TimeInvariant,JuliaFEM.Core.TimeVariant}}}("poissons ratio"=>JuliaFEM.Core.Field{JuliaFEM.Core.Discrete,JuliaFEM.Core.Constant,JuliaFEM.Core.TimeInvariant}(0.3),"geometry"=>JuliaFEM.Core.Field{JuliaFEM.Core.Discrete,JuliaFEM.Core.Variable,JuliaFEM.Core.TimeInvariant}([[13.96894,1.54855,-2.99382],[15.71724,2.88794,-2.79243],[14.76115,1.1419,-1.23125],[15.93754,1.0623,-2.82067],[14.84309,2.21825,-2.89313],[15.2392,2.01492,-2.01184],[14.36505,1.34523,-2.11254],[14.95324,1.30543,-2.90725],[15.82739,1.97512,-2.80655],[15.34935,1.1021,-2.02596]]),"youngs modulus"=>JuliaFEM.Core.Field{JuliaFEM.Core.Discrete,JuliaFEM.Core.Constant,JuliaFEM.Core.TimeInvariant}(210000.0)))

```julia
surface_elements = filter((e) -> isa(e, JuliaFEM.Core.Element{JuliaFEM.Core.Tri6}), celements)
info("$(length(surface_elements)) surface elements")
```

    INFO: 0 surface elements

```julia
length(celements)
```

    62454

```julia
solve!(model, "elasticity equations", 0.0)
```

    INFO: # of field problems: 1
    INFO: # of boundary problems: 1
    INFO: Starting iteration 1
    INFO: Assembling field problems...
    INFO: Assembling body 1...
    INFO: Assembly: 10.0 % done.
    INFO: Assembly: 20.0 % done.
    INFO: Assembly: 30.0 % done.
    INFO: Assembly: 40.0 % done.
    INFO: Assembly: 50.0 % done.
    INFO: Assembly: 60.0 % done.
    INFO: Assembly: 70.0 % done.
    INFO: Assembly: 80.0 % done.
    INFO: Assembly: 90.0 % done.
    INFO: Assembly: 100.0 % done.
    INFO: Assembling boundary problems...
    INFO: Assembling boundary 1...
    INFO: Assembly: 10.0 % done.
    INFO: Assembly: 20.0 % done.
    INFO: Assembly: 30.0 % done.
    INFO: Assembly: 40.0 % done.
    INFO: Assembly: 50.0 % done.
    INFO: Assembly: 60.0 % done.
    INFO: Assembly: 70.0 % done.
    INFO: Assembly: 80.0 % done.
    INFO: Assembly: 90.0 % done.
    INFO: Assembly: 100.0 % done.
    INFO: Solving system
    INFO: all dofs = 321504
    INFO: interior dofs = 312936
    INFO: boundary dofs = 8568
    INFO: preparation in 3.385999917984009 seconds
    INFO: displacement on boundary solved.
    INFO: norm[u_boundary_dofs] = 0.0
    INFO: homogeneous dirichlet boundary
    INFO: solve boundary = 0.42100000381469727
    INFO: factorizations done in 14.133000135421753 seconds
    INFO: solved interior in 0.2969999313354492 seconds. norm = 0.0
    INFO: timing info for non-linear iteration:

    (1,true)

    INFO: boundary assembly       : 2.1679999828338623
    INFO: field assembly          : 279.9069998264313
    INFO: dump matrices to disk   : 0.0
    INFO: solve problem           : 19.37600016593933
    INFO: update element data     : 1.6999998092651367
    INFO: non-linear iteration    : 303.1509997844696
    INFO: solver finished in 304.5089998245239 seconds.

## Saving results to file

```julia
xdoc, xmodel = JuliaFEM.Postprocess.xdmf_new_model()
temporal_collection = JuliaFEM.Postprocess.xdmf_new_temporal_collection(xmodel)
grid = JuliaFEM.Postprocess.xdmf_new_grid(temporal_collection; time=0)
```

    <Grid Name="Grid">
      <Time Value="0"/>
    </Grid>

Save geometry to xdmf file

```julia
nnodes = length(model["nodes"])
info("number of nodes in model: $nnodes")

X = zeros(3, nnodes)
for nid in keys(model["nodes"])
    X[:, perm[nid]] = model["nodes"][nid]
end

nelements = length(model["elsets"]["PISTON"])
info("Number of elements in model: $nelements")
elmap = zeros(Int64, 5, nelements)
#elmap[1,:] = 0x0026
elmap[1,:] = 0x6
for (i, elid) in enumerate(model["elsets"]["PISTON"])
    elmap[2:end,i] = Int64[perm[nid] for nid in model["elements"][elid]]
end
```

    INFO: number of nodes in model: 8789
    INFO: Number of elements in model: 31437

```julia
JuliaFEM.Postprocess.xdmf_new_mesh(grid, X, elmap)
```

    true

Save nodal data to model

```julia
using JuliaFEM.Core: get_connectivity

u = zeros(3, nnodes)

for element in values(elements)
    isa(element, Element{Tet4}) || continue
    connectivity = get_connectivity(element)
    field = element["displacement"](0.0)
    for (i, nid) in enumerate(connectivity)
        u[:, nid] = field[i]
    end
end
size(u)
```

    (3,8789)

```julia
JuliaFEM.Postprocess.xdmf_new_field(grid, "Displacement", "nodes", u)
```

    true

```julia
JuliaFEM.Postprocess.xdmf_save_model(xdoc, "/tmp/piston_8789_P1.xmf")
```

    1525505

```julia
u[:, 1:10]
```

    3x10 Array{Float64,2}:
     0.0582911   0.339694  0.431768    …   0.124351  0.346492    0.000617283
     0.0900323   0.22174   0.433814        0.140503  0.275036    0.0044649
     0.0838928  -0.148224  0.00262483     -0.11171   0.0843743  -0.0175412

```julia
minimum(u, 2)'
```

    1x3 Array{Float64,2}:
     -0.0471355  -0.468786  -0.222571

```julia
maximum(u, 2)'
```

    1x3 Array{Float64,2}:
     0.511168  0.563004  0.66328

```julia
piston_8789_P1_solution_norm = 38.52232721814439
```

    38.52232721814439

```julia
@assert isapprox(norm(vec(u)), piston_8789_P1_solution_norm)
```

```julia

```


# New features v0.3.3

[1. Classical-quantum maps and mixed circuits](#1.-Classical-quantum-maps-and-mixed-circuits)

- `discopy.quantum.cqmap` implements Bob and Aleks' classical-quantum maps.
- Now `discopy.quantum.circuit` diagrams have two generating objects: `bit` and `qubit`.
- New boxes `Discard`, `Measure` and `ClassicalGate` can be simulated with `cqmap` or sent to `pytket`.

[2. ZX diagrams and PyZX interface](#2.-ZX-diagrams-and-PyZX-interface)

- `discopy.quantum.zx` implements diagrams with spiders, swaps and Hadamard boxes.
- `to_pyzx` and `from_pyzx` methods can be used to turn diagrams into graphs, simplify then back.

[3. Parametrised diagrams, formal sums and automatic gradients](#3.-Parametrised-diagrams,-formal-sums-and-automatic-gradients)

- We can use `sympy.Symbols` as variables in our diagrams (tensor, circuit or ZX).
- We can take formal sums of diagrams. `TensorFunctor` sends formal sums to concrete sums.
- Given a diagram (tensor, circuit or ZX) with a free variable, we can compute its gradient as a sum.

[4. Learning functors, diagrammatically](#4.-Learning-functors,-diagrammatically)

- We can use automatic gradients to learn functors (classical and/or quantum) from data.

## 1. Classical-quantum maps and mixed circuits

```python
from discopy.quantum import *

circuit = H @ X >> CX >> Measure() @ Id(qubit)
circuit.draw()
```

```python
circuit.eval()
```

    CQMap(dom=Q(Dim(2, 2)), cod=C(Dim(2)) @ Q(Dim(2)), array=[0.0, 0.0, 0.0, 0.49999997, 0.49999997, 0.0, 0.0, 0.0, ..., 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.49999997])

```python
circuit.init_and_discard().draw()
```

```python
circuit.init_and_discard().eval()
```

    CQMap(dom=CQ(), cod=C(Dim(2)), array=[0.49999997, 0.49999997])

```python
circuit.to_tk()
```

    tk.Circuit(2, 1).H(0).X(1).CX(0, 1).Measure(0, 0)

```python
from pytket.backends.ibm import AerBackend
backend = AerBackend()
circuit.eval(backend)
```

    Tensor(dom=Dim(1), cod=Dim(2), array=[0.5019531, 0.49804688])

```python
postprocess = ClassicalGate('postprocess', 2, 0, array=[1, 0, 0, 0])
postprocessed_circuit = Ket(0, 0) >> H @ X >> CX >> Measure() @ Measure() >> postprocess
postprocessed_circuit.draw(aspect='auto')
```

```python
postprocessed_circuit.to_tk()
```

    tk.Circuit(2, 2).H(0).X(1).CX(0, 1).Measure(0, 0).Measure(0, 1).post_process(ClassicalGate('postprocess', bit @ bit, Ty(), array=[1, 0, 0, 0]))

```python
postprocessed_circuit.eval(backend)
```

    Tensor(dom=Dim(1), cod=Dim(1), array=[0.47851562])

## 2. ZX diagrams and PyZX interface

```python
from discopy.quantum.zx import *
from pyzx import draw

bialgebra = Z(1, 2, .25) @ Z(1, 2, .75) >> Id(1) @ SWAP @ Id(1) >> X(2, 1) @ X(2, 1, .5)
bialgebra.draw(aspect='equal')
draw(bialgebra.to_pyzx())
```

```python
from pyzx import generate, simplify

graph = generate.cliffordT(2, 5)
print("From DisCoPy:")
Diagram.from_pyzx(graph).draw()

print("To PyZX:")
draw(graph)
simplify.full_reduce(graph)
draw(graph)

print("And back!")
Diagram.from_pyzx(graph).draw()
```

## 3. Parametrised diagrams, formal sums and automatic gradients

```python
from sympy.abc import phi
from discopy import drawing
from discopy.quantum import *

circuit = sqrt(2) @ Ket(0, 0) >> H @ Rx(phi) >> CX >> Bra(0, 1)
drawing.equation(circuit, circuit.subs(phi, .5), symbol="|-->")
```

```python
gradient = scalar(1j) @ (circuit >> circuit[::-1]).grad(phi)
gradient.draw()
```

```python
import numpy as np

x = np.arange(0, 1, 0.05)
y = np.array([circuit.subs(phi, i).measure() for i in x])
dy = np.array([gradient.subs(phi, i).eval().array for i in x])
```

```python
from matplotlib import pyplot as plt

plt.subplot(2, 1, 1)
plt.plot(x, y)
plt.ylabel("Amplitude")

plt.subplot(2, 1, 2)
plt.plot(x, dy)
plt.ylabel("Gradient")
```

## 4. Learning functors, diagrammatically

```python
from discopy import *

s, n = Ty('s'), Ty('n')
Alice, loves, Bob = Word("Alice", n), Word("loves", n.r @ s @ n.l), Word("Bob", n)

grammar = Cup(n, n.r) @ Id(s) @ Cup(n.l, n)
parsing = {
    "{} {} {}".format(subj, verb, obj): subj @ verb @ obj >> grammar
    for subj in [Alice, Bob] for verb in [loves] for obj in [Alice, Bob]}
pregroup.draw(parsing["Alice loves Bob"], aspect='equal')

print("Our favorite toy dataset:")
corpus = {
    "{} {} {}".format(subj, verb, obj): int(obj != subj)
    for subj in [Alice, Bob] for verb in [loves] for obj in [Alice, Bob]}
for sentence, scalar in corpus.items():
    print("'{}' is {}.".format(sentence, "true" if scalar else "false"))
```

```python
from sympy import symbols

parameters = symbols("a0 a1 b0 b1 c00 c01 c10 c11")

F = TensorFunctor(
    ob={s: 1, n: 2},
    ar={Alice: symbols("a0 a1"),
        Bob: symbols("b0 b1"),
        loves: symbols("c00 c01 c10 c11")})
gradient = F(parsing["Alice loves Bob"]).grad(*parameters)
gradient
```

    Tensor(dom=Dim(8), cod=Dim(1), array=[1.0*b0*c00 + 1.0*b1*c01, 1.0*b0*c10 + 1.0*b1*c11, 1.0*a0*c00 + 1.0*a1*c10,
     1.0*a0*c01 + 1.0*a1*c11, 1.0*a0*b0, 1.0*a0*b1, 1.0*a1*b0, 1.0*a1*b1])

```python
gradient.subs(list(zip(parameters, 8 * [0])))
```

    Tensor(dom=Dim(8), cod=Dim(1), array=[0, 0, 0, 0, 0, 0, 0, 0])

```python
from discopy.quantum import *

gates = {
    Alice: ClassicalGate('Alice', 0, 1, symbols("a0 a1")),
    Bob: ClassicalGate('Bob', 0, 1, symbols("b0 b1")),
    loves: ClassicalGate('loves', 0, 2, symbols("c00 c01 c10 c11"))}

F = CircuitFunctor(ob={s: Ty(), n: bit}, ar=gates)
F(parsing["Alice loves Bob"]).draw()
```

```python
F(parsing["Alice loves Alice"]).grad(symbols("a0")).draw()
```

```python
F(parsing["Alice loves Alice"]).grad(symbols("a0")).eval()
```

    CQMap(dom=CQ(), cod=CQ(), array=[2.0*a0*c00 + 1.0*a1*c01 + 1.0*a1*c10])


# Unrolled generative adversarial networks on a toy dataset

This notebook demos a simple implementation of unrolled generative adversarial networks on a 2d mixture of Gaussians dataset. See the [paper](https://arxiv.org/abs/1611.02163) for a better description of the technique, experiments, results, and other good stuff. Note that the architecture and hyperparameters used in this notebook are not identical to the one in the paper.

## Motivation
The GAN learning problem is to find the optimal parameters $\theta_G^*$ for a generator function $G\left( z; \theta_G\right)$ in a minimax objective,
$$\begin{align}
    \theta_G^* &= \underset{\theta_G}{\text{argmin}} \underset{\theta_D}{\max} f\left(\theta_G, \theta_D\right) \\
&= \underset{\theta_G}{\text{argmin}} \;f\left(\theta_G, \theta_D^*\left(\theta_G\right)\right)\\
\theta_D^*\left(\theta_G\right) &= \underset{\theta_D}{\max}  \;f\left(\theta_G, \theta_D\right),
\end{align}$$
where the saddle objective $f$ is the standard GAN loss:
$$f\left(\theta_G, \theta_D\right) =
    \mathbb{E}_{x\sim p_{data}}\left[\mathrm{log}\left(D\left(x; \theta_D\right)\right)\right] +
    \mathbb{E}_{z \sim \mathcal{N}(0,I)}\left[\mathrm{log}\left(1 - D\left(G\left(z; \theta_G\right); \theta_D\right)\right)\right].
$$

In unrolled GANs, we approximate $\theta_D^*\left(\theta_G\right)$ using a few steps of gradient ascent:
$$\theta_D^*\left(\theta_G\right) \approx \hat{\theta}_D\left(\theta_G\right) \equiv\text{ a few steps of SGD maximizing}\;f\left(\theta_G, \theta_D\right).$$

We can then compute the update for the generator parameters, $\theta_G$, by computing the gradient of the saddle objective with respect to $\theta_G$ and the optimized discriminator parameters, $\hat{\theta}_D$:
$$\frac{d}{d \theta_G} f\left(\theta_G, \hat{\theta}_D\left(\theta_G\right)\right)$$.

## Implementation details
To backpropagate through the optimization process, we need to create a symbolic computational graph that includes all the operations from the initial weights to the optimized weights. TensorFlow's built-in optimizers use custom C++ code for efficiency, and do not construct a symbolic graph that is differentiable. For this notebook, we use the optimization routines from `keras` to compute updates.  Next, we use `tf.contrib.graph_editor.graph_replace` to build a copy of the graph containing the mapping from initial weights to updated weights after one optimization iteration, but replacing the initial weights with the last iteration's weights:

This yields a new graph that allows us to backprop from $\theta_D^2$ back to $\theta_D^0$. We can then plug $\theta_D^2$ into the loss function to get the final objective that the generator optimizes. Using the magic of `graph_replace` we can write the unrolled optimization procedure in just a few lines:
```python
# update_dict contains a dictionary mapping from variables (\theta_D^0)
# to their values after one step of optimization (\theta_D^1)
cur_update_dict = update_dict
for i in xrange(params['unrolling_steps'] - 1):
    # Compute variable updates given the previous iteration's updated variable
    cur_update_dict = graph_replace(update_dict, cur_update_dict)
# Final unrolled loss uses the parameters at the last time step
unrolled_loss = graph_replace(loss, cur_update_dict)
```

Note there are many other ways of implementing unrolled optimization that don't use graph rewriting. For example, if we created a function that takes weights as inputs and returns the updated weights, we could just iteratively call that function.

```python
%pylab inline
from collections import OrderedDict
import tensorflow as tf
ds = tf.contrib.distributions
slim = tf.contrib.slim

from keras.optimizers import Adam

try:
    from moviepy.video.io.bindings import mplfig_to_npimage
    import moviepy.editor as mpy
    generate_movie = True
except:
    print("Warning: moviepy not found.")
    generate_movie = False
```

    Using TensorFlow backend.
    WARNING:py.warnings:/usr/local/lib/python2.7/dist-packages/skimage/filter/__init__.py:6: skimage_deprecation: The `skimage.filter` module has been renamed to `skimage.filters`.  This placeholder module will be removed in v0.13.
      warn(skimage_deprecation('The `skimage.filter` module has been renamed '

    Populating the interactive namespace from numpy and matplotlib

`graph_replace` is broken in TensorFlow 1.0 (see this [issue](https://github.com/tensorflow/tensorflow/issues/9125)). We get around this issue with an ugly hack that removes the problematic attribute from all ops in the graph on every call to `graph_replace`.

```python
_graph_replace = tf.contrib.graph_editor.graph_replace

def remove_original_op_attributes(graph):
    """Remove _original_op attribute from all operations in a graph."""
    for op in graph.get_operations():
        op._original_op = None

def graph_replace(*args, **kwargs):
    """Monkey patch graph_replace so that it works with TF 1.0"""
    remove_original_op_attributes(tf.get_default_graph())
    return _graph_replace(*args, **kwargs)
```

### Utility functions

```python
def extract_update_dict(update_ops):
    """Extract variables and their new values from Assign and AssignAdd ops.

    Args:
        update_ops: list of Assign and AssignAdd ops, typically computed using Keras' opt.get_updates()

    Returns:
        dict mapping from variable values to their updated value
    """
    name_to_var = {v.name: v for v in tf.global_variables()}
    updates = OrderedDict()
    for update in update_ops:
        var_name = update.op.inputs[0].name
        var = name_to_var[var_name]
        value = update.op.inputs[1]
        if update.op.type == 'Assign':
            updates[var.value()] = value
        elif update.op.type == 'AssignAdd':
            updates[var.value()] = var + value
        else:
            raise ValueError("Update op type (%s) must be of type Assign or AssignAdd"%update_op.op.type)
    return updates
```

### Data creation

```python
def sample_mog(batch_size, n_mixture=8, std=0.01, radius=1.0):
    thetas = np.linspace(0, 2 * np.pi, n_mixture)
    xs, ys = radius * np.sin(thetas), radius * np.cos(thetas)
    cat = ds.Categorical(tf.zeros(n_mixture))
    comps = [ds.MultivariateNormalDiag([xi, yi], [std, std]) for xi, yi in zip(xs.ravel(), ys.ravel())]
    data = ds.Mixture(cat, comps)
    return data.sample(batch_size)
```

### Generator and discriminator architectures

```python
def generator(z, output_dim=2, n_hidden=128, n_layer=2):
    with tf.variable_scope("generator"):
        h = slim.stack(z, slim.fully_connected, [n_hidden] * n_layer, activation_fn=tf.nn.tanh)
        x = slim.fully_connected(h, output_dim, activation_fn=None)
    return x

def discriminator(x, n_hidden=128, n_layer=2, reuse=False):
    with tf.variable_scope("discriminator", reuse=reuse):
        h = slim.stack(x, slim.fully_connected, [n_hidden] * n_layer, activation_fn=tf.nn.tanh)
        log_d = slim.fully_connected(h, 1, activation_fn=None)
    return log_d
```

### Hyperparameters

```python
params = dict(
    batch_size=512,
    disc_learning_rate=1e-4,
    gen_learning_rate=1e-3,
    beta1=0.5,
    epsilon=1e-8,
    max_iter=25000,
    viz_every=5000,
    z_dim=256,
    x_dim=2,
    unrolling_steps=5,
)
```

## Construct model and training ops

```python
tf.reset_default_graph()

data = sample_mog(params['batch_size'])

noise = ds.Normal(tf.zeros(params['z_dim']),
                  tf.ones(params['z_dim'])).sample(params['batch_size'])
# Construct generator and discriminator nets
with slim.arg_scope([slim.fully_connected], weights_initializer=tf.orthogonal_initializer(gain=1.4)):
    samples = generator(noise, output_dim=params['x_dim'])
    real_score = discriminator(data)
    fake_score = discriminator(samples, reuse=True)

# Saddle objective
loss = tf.reduce_mean(
    tf.nn.sigmoid_cross_entropy_with_logits(logits=real_score, labels=tf.ones_like(real_score)) +
    tf.nn.sigmoid_cross_entropy_with_logits(logits=fake_score, labels=tf.zeros_like(fake_score)))

gen_vars = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, "generator")
disc_vars = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, "discriminator")

# Vanilla discriminator update
d_opt = Adam(lr=params['disc_learning_rate'], beta_1=params['beta1'], epsilon=params['epsilon'])
updates = d_opt.get_updates(disc_vars, [], loss)
d_train_op = tf.group(*updates, name="d_train_op")

# Unroll optimization of the discrimiantor
if params['unrolling_steps'] > 0:
    # Get dictionary mapping from variables to their update value after one optimization step
    update_dict = extract_update_dict(updates)
    cur_update_dict = update_dict
    for i in xrange(params['unrolling_steps'] - 1):
        # Compute variable updates given the previous iteration's updated variable
        cur_update_dict = graph_replace(update_dict, cur_update_dict)
    # Final unrolled loss uses the parameters at the last time step
    unrolled_loss = graph_replace(loss, cur_update_dict)
else:
    unrolled_loss = loss

# Optimize the generator on the unrolled loss
g_train_opt = tf.train.AdamOptimizer(params['gen_learning_rate'], beta1=params['beta1'], epsilon=params['epsilon'])
g_train_op = g_train_opt.minimize(-unrolled_loss, var_list=gen_vars)
```

## Train!

```python
sess = tf.InteractiveSession()
sess.run(tf.global_variables_initializer())
```

```python
from tqdm import tqdm
xmax = 3
fs = []
frames = []
np_samples = []
n_batches_viz = 10
viz_every = params['viz_every']
for i in tqdm(xrange(params['max_iter'])):
    f, _, _ = sess.run([[loss, unrolled_loss], g_train_op, d_train_op])
    fs.append(f)
    if i % viz_every == 0:
        np_samples.append(np.vstack([sess.run(samples) for _ in xrange(n_batches_viz)]))
        xx, yy = sess.run([samples, data])
        fig = figure(figsize=(5,5))
        scatter(xx[:, 0], xx[:, 1], edgecolor='none')
        scatter(yy[:, 0], yy[:, 1], c='g', edgecolor='none')
        axis('off')
        if generate_movie:
            frames.append(mplfig_to_npimage(fig))
        show()
```

## Visualize results

```python
import seaborn as sns
```

```python
np_samples_ = np_samples[::1]
cols = len(np_samples_)
bg_color  = sns.color_palette('Greens', n_colors=256)[0]
figure(figsize=(2*cols, 2))
for i, samps in enumerate(np_samples_):
    if i == 0:
        ax = subplot(1,cols,1)
    else:
        subplot(1,cols,i+1, sharex=ax, sharey=ax)
    ax2 = sns.kdeplot(samps[:, 0], samps[:, 1], shade=True, cmap='Greens', n_levels=20, clip=[[-xmax,xmax]]*2)
    ax2.set_axis_bgcolor(bg_color)
    xticks([]); yticks([])
    title('step %d'%(i*viz_every))
ax.set_ylabel('%d unrolling steps'%params['unrolling_steps'])
gcf().tight_layout()
```

```python
fs = np.array(fs)
plot(fs)
legend(('loss', 'unrolled loss'))
```

```python
plot(fs[:, 0] - fs[:, 1])
legend('optimized loss - initial loss')
```

```python
#clip = mpy.ImageSequenceClip(frames[::], fps=30)
#clip.ipython_display()
```


# sparse matrix の扱い

`scipy` で疎行列のクラスが扱える。

```python
from scipy import sparse
```

# 基本的な使い方

* `lil_matrix` 型の変数を用意して各要素に値を設定する
* 設定された `lil_matrix` を `csr_matrix` または `csc_matrix` に変換する
* 変換された疎行列について計算をする

```python
a = sparse.lil_matrix((4, 5))
```

```python
a.toarray()  # 最初は0うめ
```

    array([[0., 0., 0., 0., 0.],
           [0., 0., 0., 0., 0.],
           [0., 0., 0., 0., 0.],
           [0., 0., 0., 0., 0.]])

```python
a[0, 1] = 1
a[0, 3] = 2
a[2, 2] = 3
a[3, 4] = 4
```

```python
a.toarray()  # 値をセット
```

    array([[0., 1., 0., 2., 0.],
           [0., 0., 0., 0., 0.],
           [0., 0., 3., 0., 0.],
           [0., 0., 0., 0., 4.]])

```python
b = sparse.lil_matrix((5, 4))
b[0, 2] = 1
b[1, 2] = 2
b[2, 3] = 3
b[3, 3] = 4
b.toarray()
```

    array([[0., 0., 1., 0.],
           [0., 0., 2., 0.],
           [0., 0., 0., 3.],
           [0., 0., 0., 4.],
           [0., 0., 0., 0.]])

\begin{align}
C &= AB \\
&= \begin{pmatrix}
0& 1& 0& 2& 0 \\
0& 0& 0& 0& 0 \\
0& 0& 3& 0& 0 \\
0& 0& 0& 0& 4
\end{pmatrix}
\begin{pmatrix}
0& 0& 1& 0 \\
0& 0& 2& 0 \\
0& 0& 0& 3 \\
0& 0& 0& 4 \\
0& 0& 0& 0
\end{pmatrix} \\
&= \begin{pmatrix}
0& 0& 2& 8 \\
0& 0& 0& 0 \\
0& 0& 0& 9 \\
0& 0& 0& 0
\end{pmatrix}
\end{align}

```python
c = a.dot(b)
```

```python
c.toarray()
```

    array([[0., 0., 2., 8.],
           [0., 0., 0., 0.],
           [0., 0., 0., 9.],
           [0., 0., 0., 0.]])

# csr/csc matrix

* `csr_matrix` のほうが行を取り出すのが早い (cs 'row')
* `csc_matrix` のほうが列を取り出すのが早い (cs 'coolumn')

```python
a1 = a.tocsr()
b1 = b.tocsr()
```

```python
c1 = a1.dot(b1)
```

```python
c1.toarray()  # c と結果は同じ
```

    array([[0., 0., 2., 8.],
           [0., 0., 0., 0.],
           [0., 0., 0., 9.],
           [0., 0., 0., 0.]])

```python
a2 = a.tocsc()
b2 = b.tocsc()
```

```python
c2 = a2.dot(b2)
```

```python
c2.toarray()
```

    array([[0., 0., 2., 8.],
           [0., 0., 0., 0.],
           [0., 0., 0., 9.],
           [0., 0., 0., 0.]])

```python
a = sparse.lil_matrix((4, 4))
a[0, 1] = 1
a[1, 2] = 2
a[2, 3] = 3
a[3, 3] = 4
```

```python
a.toarray()
```

    array([[0., 1., 0., 0.],
           [0., 0., 2., 0.],
           [0., 0., 0., 3.],
           [0., 0., 0., 4.]])

```python
a1 = a.tocsr()
type(a1)
```

    scipy.sparse.csr.csr_matrix

```python
a2 = a.tocsc()
type(a2)
```

    scipy.sparse.csc.csc_matrix

```python
b1 = a1.getrow(1)
b1.toarray()
```

    array([[0., 0., 2., 0.]])

```python
b2 = a2.getcol(3)
b2.toarray()
```

    array([[0.],
           [0.],
           [3.],
           [4.]])

```python
type(a1.T)
```

    scipy.sparse.csc.csc_matrix

```python
type(a2.T)
```

    scipy.sparse.csr.csr_matrix


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

# Caracterización de Sistemas en el Dominio del Tiempo

*This Jupyter notebook is part of a [collection of notebooks](../index.ipynb) in the bachelors module Signals and Systems, Communications Engineering, Universität Rostock. Please direct questions and suggestions to [Sascha.Spors@uni-rostock.de](mailto:Sascha.Spors@uni-rostock.de).*

## Ejemplo RLC serie

Considere el circuito que se muestra en la figura. Suponga que la entrada es el voltaje $x(t)$ y la salida el voltaje $y(t)$. Suponga además que no hay energía almacenada en los elementos que almacenan energía en $t<0$. Además, considere que $x(t)=0 \forall t<0$.

Para ilustrar, considere que $L = 0.5$, $R = 1$, $C = 0.4$ .

### Ecuación diferencial (modelo)

Aplicando las [Leyes de Kirchhoff](https://en.wikipedia.org/wiki/Kirchhoff's_circuit_laws) al circuito se obtiene la siguiente ODE.

\begin{equation}
C L \frac{d^2 y(t)}{dt^2} + C R \frac{d y(t)}{dt} + y(t) = x(t)
\end{equation}

Defínala en `SymPy`

```python
t, L, R, C = sym.symbols('t L R C', real=True)
x = sym.Function('x')(t)
y = sym.Function('y')(t)

ode = sym.Eq(L*C*y.diff(t, 2) + R*C*y.diff(t) + y, x)
ode

```

Introduzca los valores de los elementos del circuito en un diccionario para usarlo después.

```python
RLC = {R: 1,
       L: 0.5,
       C: .4}
RLC
```

### Respuesta impulsional

Encuentre la respuesta impulsional del circuito resolviendo la ODE para $x(t)=\delta(t)$

```python
ode.subs(x, sym.DiracDelta(t)).subs(y, sym.Function('h')(t))
```

```python
solution_h = sym.dsolve(
    ode.subs(x, sym.DiracDelta(t)).subs(y, sym.Function('h')(t)))
solution_h
```

Las constantes $C_1$ y $C_2$ deben calcularse para satisfacer las condiciones iniciales  $y(t) = 0$ and $\frac{d y(t)}{dt} = 0$ para $t<0$.

```python
integration_constants = sym.solve(
    (solution_h.rhs.limit(t, 0, '-'),
     solution_h.rhs.diff(t).limit(t, 0, '-')),
    ['C1', 'C2'])
integration_constants
```

Remplace los valores de las constantes de integración $C_1$ y $C_2$ en la solución de la ODE obtenida antes para encontrar la respuesta impulsional completa.

```python
h = solution_h.subs(integration_constants)
h
```

Grafique la respuesta impulsional del sistema usando los parámetros almacenados en el diccionario RLC.

```python
plt.rcParams['figure.figsize'] = 7, 2
sym.plot(h.rhs.subs(RLC), (t,-1,10), ylabel=r'h(t)');
```

### Respuesta al escalón

La respuesta al escalón se obtiene al integrar la respuesta impulsional.

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

he = sym.integrate(h.rhs.subs(t, tau), (tau, 0, t))
he
```

$\displaystyle \frac{- \left(\begin{cases} \begin{cases} - \frac{2 C L}{C R - \sqrt{C^{2} R^{2} - 4 C L}} & \text{for}\: C R - \sqrt{C^{2} R^{2} - 4 C L} \neq 0 \\0 & \text{otherwise} \end{cases} & \text{for}\: C R - \sqrt{C^{2} R^{2} - 4 C L} = 0 \\- \frac{2 C L}{C R - \sqrt{C^{2} R^{2} - 4 C L}} & \text{otherwise} \end{cases}\right) \theta\left(t\right) + \left(\begin{cases} \begin{cases} - \frac{2 C L e^{- \frac{R t}{2 L} + \frac{t \sqrt{C^{2} R^{2} - 4 C L}}{2 C L}}}{C R - \sqrt{C^{2} R^{2} - 4 C L}} & \text{for}\: C R - \sqrt{C^{2} R^{2} - 4 C L} \neq 0 \\t & \text{otherwise} \end{cases} & \text{for}\: C R - \sqrt{C^{2} R^{2} - 4 C L} = 0 \\- \frac{2 C L e^{- \frac{R t}{2 L} + \frac{t \sqrt{C^{2} R^{2} - 4 C L}}{2 C L}}}{C R - \sqrt{C^{2} R^{2} - 4 C L}} & \text{otherwise} \end{cases}\right) \theta\left(t\right)}{\sqrt{C \left(C R^{2} - 4 L\right)}} - \frac{- \left(\begin{cases} \begin{cases} - \frac{2 C L}{C R + \sqrt{C^{2} R^{2} - 4 C L}} & \text{for}\: C R + \sqrt{C^{2} R^{2} - 4 C L} \neq 0 \\0 & \text{otherwise} \end{cases} & \text{for}\: C R + \sqrt{C^{2} R^{2} - 4 C L} = 0 \\- \frac{2 C L}{C R + \sqrt{C^{2} R^{2} - 4 C L}} & \text{otherwise} \end{cases}\right) \theta\left(t\right) + \left(\begin{cases} \begin{cases} - \frac{2 C L e^{- \frac{R t}{2 L} - \frac{t \sqrt{C^{2} R^{2} - 4 C L}}{2 C L}}}{C R + \sqrt{C^{2} R^{2} - 4 C L}} & \text{for}\: C R + \sqrt{C^{2} R^{2} - 4 C L} \neq 0 \\t & \text{otherwise} \end{cases} & \text{for}\: C R + \sqrt{C^{2} R^{2} - 4 C L} = 0 \\- \frac{2 C L e^{- \frac{R t}{2 L} - \frac{t \sqrt{C^{2} R^{2} - 4 C L}}{2 C L}}}{C R + \sqrt{C^{2} R^{2} - 4 C L}} & \text{otherwise} \end{cases}\right) \theta\left(t\right)}{\sqrt{C \left(C R^{2} - 4 L\right)}}$

Para facilitar la interpretación de la expresión, considere los valores almacenados en el diccionario.

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

he = sym.integrate(h.rhs.subs(RLC).subs(t, tau), (tau, 0, t))
he
```

Grafique la respuesta al escalón.

```python
sym.plot(he, (t,-1,10), ylabel=r'$h_\epsilon(t)$');
```

```python

```

.ipynb_checkpoints/3-4 Ejemplo circuito RLC serie-checkpoint.ipynb


```python
import numpy as np
import sympy as sym
import json
import sympy as sym
import pydae.buildfast as db
from pydae.grid_bpu import bpu
```

```python
file_path = 'k12p6_pss.json'
bpu_obj = bpu(data_input=file_path)
```

```python
g_list = bpu_obj.dae['g']
h_dict = bpu_obj.dae['h_dict']
f_list = bpu_obj.dae['f']
x_list = bpu_obj.dae['x']
params_dict = bpu_obj.dae['params_dict']

sys = {'name':'k12p6_pss',
       'params_dict':params_dict,
       'f_list':f_list,
       'g_list':g_list,
       'x_list':x_list,
       'y_ini_list':bpu_obj.dae['y_ini'],
       'y_run_list':bpu_obj.dae['y_run'],
       'u_run_dict':bpu_obj.dae['u_run_dict'],
       'u_ini_dict':bpu_obj.dae['u_ini_dict'],
       'h_dict':h_dict}

sys = db.system(sys)
db.sys2num(sys)
```

    {'real': True, 'extended_real': True, 'commutative': True, 'complex': True, 'imaginary': False, 'infinite': False, 'hermitian': True, 'finite': True}
    {'real': True, 'extended_real': True, 'commutative': True, 'complex': True, 'imaginary': False, 'infinite': False, 'hermitian': True, 'finite': True}
    <class 'sympy.core.symbol.Symbol'>

```python
A_sp.inv(method="LU")
```

```python
import re

import time

Fx_ini = sys['Fx_ini']
Fy_ini = sys['Fy_ini']
Gx_ini = sys['Gx_ini']
Gy_ini = sys['Gy_ini']

Fx_run = sys['Fx_run']
Fy_run = sys['Fy_run']
Gx_run = sys['Gx_run']
Gy_run = sys['Gy_run']

jac_ini = sym.Matrix([[Fx_ini,Fy_ini],[Gx_ini,Gy_ini]])
jac_run = sym.Matrix([[Fx_run,Fy_run],[Gx_run,Gy_run]])

matrix = lu
string_xy = ''
string_up = ''

string = ''

function_header = 'def lu_ini_eval(lu,x,y,u,p,xyup = 0):\n\n'
matrix_name = 'lu_ini'

tab = ' '
N_row,N_col = matrix.shape
t_0 = time.time()
for irow in range(N_row):
    for icol in range(N_col):
        str_element = sym2str(matrix[irow,icol],sys['x'],sys['y_ini'],sys['u_ini'],sys['params_dict'].keys())
        if str_element != 0:
            if 'x' in str_element or 'y' in str_element:
                string_xy += f'{" "*4}{matrix_name}[{irow},{icol}] = {str_element}\n'
            else:
                string_up += f'{" "*8}{matrix_name}[{irow},{icol}] = {str_element}\n'

string += function_header
string += string_xy
string += '\n'
string += f'{" "*4}if xyup == 1:\n\n'
string += string_up

t_1 = time.time()
print(t_1-t_0)
```

```python

Fx_ini = sys['Fx_ini']
Fy_ini = sys['Fy_ini']
Gx_ini = sys['Gx_ini']
Gy_ini = sys['Gy_ini']

Fx_run = sys['Fx_run']
Fy_run = sys['Fy_run']
Gx_run = sys['Gx_run']
Gy_run = sys['Gy_run']

jac_ini = sym.Matrix([[Fx_ini,Fy_ini],[Gx_ini,Gy_ini]])
jac_run = sym.Matrix([[Fx_run,Fy_run],[Gx_run,Gy_run]])

N_x = Fx_run.shape[0]
eye = sym.eye(N_x,real=True)
Dt = sym.Symbol('Dt',real=True)
jac_trap = sym.Matrix([[eye - 0.5*Dt*Fx_run, -0.5*Dt*Fy_run],[Gx_run,Gy_run]])

```

```python
import re

def sym2str(sym_exp,x,y,u,p,multi_eval=False):

    matrix_str = str(sym_exp)

    mev = ''
    if multi_eval:
        mev = 'i,'

    if sym_exp == 0:
        matrix_str = 0
        return matrix_str

    for it in range(len(x)):
        name = str(x[it])
        matrix_str = re.sub(r'\b' + name + r'\b',f'x[{mev}{it}]',matrix_str)
    for it in range(len(y)):
        name = str(y[it])
        matrix_str = re.sub(r'\b' + name + r'\b',f'y[{mev}{it}]',matrix_str)
    for it in range(len(u)):
        name = str(u[it])
        matrix_str = re.sub(r'\b' + name + r'\b',f'u[{mev}{it}]',matrix_str)
    it = 0
    for item in p:
        name = str(item)
        matrix_str = re.sub(r'\b' + name + r'\b',f'p[{mev}{it}]',matrix_str)
        it+=1

    return matrix_str

def vector2string(vector,function_header,vector_name,x,y,u,p,multi_eval=False):
    string = ''
    string_i = ''
    N  = len(vector)

    for it in range(N):
        str_element = sym2str(vector[it],x,y,u,p,multi_eval=multi_eval)
        if multi_eval:
            string_i += f'{" "*4}{vector_name}[i,{it}] = {str_element}\n'
        else:
            string_i += f'{" "*4}{vector_name}[{it}] = {str_element}\n'

    string += '\n'
    string += function_header
    string += string_i
    string += '\n'

    return string

def matrix2string(matrix,function_header,matrix_name):
    string = ''
    string_xy = ''
    string_up = ''
    tab = ' '
    N_row,N_col = matrix.shape

    for irow in range(N_row):
        for icol in range(N_col):
            str_element = sym2str(matrix[irow,icol],sys['x'],sys['y_ini'],sys['u_ini'],sys['params_dict'].keys())
            if str_element != 0:
                if 'x' in str_element or 'y' in str_element:
                    string_xy += f'{" "*4}{matrix_name}[{irow},{icol}] = {str_element}\n'
                else:
                    string_up += f'{" "*8}{matrix_name}[{irow},{icol}] = {str_element}\n'

    string += '\n'
    string += function_header
    string += string_xy
    string += '\n'
    string += f'{" "*4}if xyup == 1:\n\n'
    string += string_up
    string += '\n'

    return string

def spmatrix2string(spmatrix_list,function_header,matrix_name):

    data = spmatrix_list[0]
    string = ''
    string_xy = ''
    string_up = ''
    tab = ' '

    for irow in range(len(data)):
        str_element = sym2str(data[irow],sys['x'],sys['y_ini'],sys['u_ini'],sys['params_dict'].keys())
        if str_element != 0:
            if 'x' in str_element or 'y' in str_element:
                string_xy += f'{" "*4}{matrix_name}[{irow}] = {str_element}\n'
            else:
                string_up += f'{" "*8}{matrix_name}[{irow}] = {str_element}\n'

    string += '\n'
    string += function_header
    string += string_xy
    string += '\n'
    string += f'{" "*4}if xyup == 1:\n\n'
    string += string_up
    string += '\n'
    string += 'def {matrix_name_vectors}():\n\n'
    string += f'{" "*4}{matrix_name}_ia = ' + str(spmatrix_list[1]) + '\n'
    string += f'{" "*4}{matrix_name}_ja = ' + str(spmatrix_list[2]) + '\n'
    string += f'{" "*4}{matrix_name}_nia = ' + str(spmatrix_list[3][0])  + '\n'
    string += f'{" "*4}{matrix_name}_nja = ' + str(spmatrix_list[3][1])  + '\n'
    string += f'{" "*4}return {matrix_name}_ia, {matrix_name}_ja, {matrix_name}_nia, {matrix_name}_nja \n'

    return string

matrix = jac_trap
function_header = 'def jac_ini_eval(jac_ini,x,y,u,p,xyup = 0):\n\n'
matrix_name = 'jac_ini'

t_0 = time.time()
string = matrix2string(matrix,function_header,matrix_name)
t_1 = time.time()
print(t_1-t_0)

matrix = jac_trap
function_header = 'def jac_ini_sp_eval(jac_ini_sp,x,y,u,p,xyup = 0):\n\n'
matrix_name = 'jac_ini_sp'

t_0 = time.time()
spmatrix_list = _doktocsr(SparseMatrix(jac_ini))
string = spmatrix2string(spmatrix_list,function_header,matrix_name)
t_1 = time.time()
print(t_1-t_0)
```

```python
function_header = 'def f_ini_eval(f_ini,x,y,u,p,xyup = 0):\n\n'
string = vector2string(sys['f'],function_header,'f_ini',sys['x'],sys['y_ini'],sys['u_ini'],sys['params_dict'].keys(),multi_eval=False)
print(string)
```

    def f_ini_eval(f_ini,x,y,u,p,xyup = 0):

        f_ini[0] = -p[53]*x[0] + p[43]*(x[1] - y[58])
        f_ini[1] = (-p[51]*(x[1] - y[58]) - y[22]*(p[52]*y[22] + y[0]*sin(x[0] - y[1])) - y[23]*(p[52]*y[23] + y[0]*cos(x[0] - y[1])) + y[28])/(2*p[44])
        f_ini[2] = (-x[2] - y[22]*(-p[49] + p[47]) + y[26])/p[45]
        f_ini[3] = (-x[3] + y[23]*(-p[50] + p[48]))/p[46]
        f_ini[4] = (y[0] - x[4])/p[57]
        f_ini[5] = -p[60]*(p[55]*(-x[4] + y[30] + u[22]) + p[56]*x[5] - y[26]) - x[4] + y[30] + u[22]
        f_ini[6] = (y[27] - x[6])/p[62]
        f_ini[7] = (x[6] - x[7])/p[64]
        f_ini[8] = p[65]*(u[24] - y[24]) - 1.0e-6*x[8]
        f_ini[9] = (x[1] - x[9] - 1.0)/p[67]
        f_ini[10] = (-x[10] + y[29])/p[69]
        f_ini[11] = -p[83]*x[11] + p[73]*(x[12] - y[58])
        f_ini[12] = (-p[81]*(x[12] - y[58]) - y[31]*(p[82]*y[31] + y[2]*sin(x[11] - y[3])) - y[32]*(p[82]*y[32] + y[2]*cos(x[11] - y[3])) + y[37])/(2*p[74])
        f_ini[13] = (-x[13] - y[31]*(-p[79] + p[77]) + y[35])/p[75]
        f_ini[14] = (-x[14] + y[32]*(-p[80] + p[78]))/p[76]
        f_ini[15] = (y[2] - x[15])/p[87]
        f_ini[16] = -p[90]*(p[85]*(-x[15] + y[39] + u[26]) + p[86]*x[16] - y[35]) - x[15] + y[39] + u[26]
        f_ini[17] = (y[36] - x[17])/p[92]
        f_ini[18] = (x[17] - x[18])/p[94]
        f_ini[19] = p[95]*(u[28] - y[33]) - 1.0e-6*x[19]
        f_ini[20] = (x[12] - x[20] - 1.0)/p[97]
        f_ini[21] = (-x[21] + y[38])/p[99]
        f_ini[22] = -p[113]*x[22] + p[103]*(x[23] - y[58])
        f_ini[23] = (-p[111]*(x[23] - y[58]) - y[40]*(p[112]*y[40] + y[4]*sin(x[22] - y[5])) - y[41]*(p[112]*y[41] + y[4]*cos(x[22] - y[5])) + y[46])/(2*p[104])
        f_ini[24] = (-x[24] - y[40]*(-p[109] + p[107]) + y[44])/p[105]
        f_ini[25] = (-x[25] + y[41]*(-p[110] + p[108]))/p[106]
        f_ini[26] = (y[4] - x[26])/p[117]
        f_ini[27] = -p[120]*(p[115]*(-x[26] + y[48] + u[30]) + p[116]*x[27] - y[44]) - x[26] + y[48] + u[30]
        f_ini[28] = (y[45] - x[28])/p[122]
        f_ini[29] = (x[28] - x[29])/p[124]
        f_ini[30] = p[125]*(u[32] - y[42]) - 1.0e-6*x[30]
        f_ini[31] = (x[23] - x[31] - 1.0)/p[127]
        f_ini[32] = (-x[32] + y[47])/p[129]
        f_ini[33] = -p[143]*x[33] + p[133]*(x[34] - y[58])
        f_ini[34] = (-p[141]*(x[34] - y[58]) - y[49]*(p[142]*y[49] + y[6]*sin(x[33] - y[7])) - y[50]*(p[142]*y[50] + y[6]*cos(x[33] - y[7])) + y[55])/(2*p[134])
        f_ini[35] = (-x[35] - y[49]*(-p[139] + p[137]) + y[53])/p[135]
        f_ini[36] = (-x[36] + y[50]*(-p[140] + p[138]))/p[136]
        f_ini[37] = (y[6] - x[37])/p[147]
        f_ini[38] = -p[150]*(p[145]*(-x[37] + y[57] + u[34]) + p[146]*x[38] - y[53]) - x[37] + y[57] + u[34]
        f_ini[39] = (y[54] - x[39])/p[152]
        f_ini[40] = (x[39] - x[40])/p[154]
        f_ini[41] = p[155]*(u[36] - y[51]) - 1.0e-6*x[41]
        f_ini[42] = (x[34] - x[42] - 1.0)/p[157]
        f_ini[43] = (-x[43] + y[56])/p[159]
        f_ini[44] = 1 - y[58]

```python
import scipy
```

```python
vector_input = sys['f']

if type(vector_input.shape) == tuple:
    N_1,N_2 = vector_input.shape
    if N_1>N_2:
        N_rows = N_1
        vector = vector_input[:,0]
    else:
        N_rows = N_2
        vector = vector_input[0,:]

len(vector_input)
```

    45

```python
for irow in range(105):
    for icol in range(105):
        if jac_ini[irow,icol] != 0:
            a_i_j = sym.Symbol(f'a_{irow}_{icol}')
            jac_ini_a[irow,icol] = a_i_j

L, U, P = jac_ini_a.LUdecomposition()

```

```python
string = ''
icol = 5
for irow in range(80):
    for icol in range(80):
        if P[irow,icol] != 0:
            string += str(L[irow,icol]) + '\n'
```

```python
str(sym.simplify(L[irow,icol]))
```

```python
print(string)
```

    IOPub data rate exceeded.
    The notebook server will temporarily stop sending output
    to the client in order to avoid crashing it.
    To change this limit, set the config variable
    `--NotebookApp.iopub_data_rate_limit`.

    Current values:
    NotebookApp.iopub_data_rate_limit=1000000.0 (bytes/sec)
    NotebookApp.rate_limit_window=3.0 (secs)

```python
substitutions = {}
replacements = {}
it = 0
for item in sys['params_dict']:
    p     = sym.Symbol(f'p_{it:05}',real=True)
    param = sym.Symbol(item,real=True)
    substitutions.update({param:p})
    replacements.update({f'p_{it:05}':f'p[{it}]'})
    it+=1
for item in sys['x']:
    x_new = sym.Symbol(f'x_{it:05}',real=True)
    substitutions.update({item:x_new})
    replacements.update({f'x_{it:05}':f'x[{it}]'})
    it+=1
for item in sys['y_ini']:
    new = sym.Symbol(f'y_ini_{it:05}',real=True)
    substitutions.update({item:new})
    replacements.update({f'y_ini_{it:05}':f'y[{it}]'})
    it+=1
for item in sys['u_ini']:
    new = sym.Symbol(f'u_ini_{it:05}',real=True)
    substitutions.update({item:new})
    replacements.update({f'u_ini_{it:05}':f'u[{it}]'})
    it+=1
for item in sys['y_run']:
    new = sym.Symbol(f'y_run_{it:05}',real=True)
    substitutions.update({item:new})
    replacements.update({f'y_run_{it:05}':f'y[{it}]'})

    it+=1
for item in sys['u_run']:
    new = sym.Symbol(f'u_run_{it:05}',real=True)
    substitutions.update({item:new})
    replacements.update({f'u_run_{it:05}':f'u[{it}]'})
    it+=1
```

```python
string = str(sys['f'].xreplace(substitutions))

def mreplace(string,replacements):
    for item in replacements:
        string = string.replace(item,replacements[item])
    return string

mreplace(string,replacements)
```

    'Matrix([[p[43]*(x[165] - y[365]) - p[53]*x[164], (-p[51]*(x[165] - y[365]) - y[329]*(p[52]*y[329] + y[307]*sin(x[164] - y[308])) - y[330]*(p[52]*y[330] + y[307]*cos(x[164] - y[308])) + y[335])/(2*p[44]), (-x[166] - y[329]*(p[47] - p[49]) + y[333])/p[45], (-x[167] + y[330]*(p[48] - p[50]))/p[46], (-x[168] + y[307])/p[57], -p[60]*(p[55]*(u[389] + u[390] - x[168]) + p[56]*x[169] - y[333]) + u[389] + u[390] - x[168], (-x[170] + y[334])/p[62], (x[170] - x[171])/p[64], p[65]*(u[391] - y[331]) - 1.0e-6*x[172], (x[165] - x[173] - 1.0)/p[67], (-x[174] + y[336])/p[69], p[73]*(x[176] - y[365]) - p[83]*x[175], (-p[81]*(x[176] - y[365]) - y[338]*(p[82]*y[338] + y[309]*sin(x[175] - y[310])) - y[339]*(p[82]*y[339] + y[309]*cos(x[175] - y[310])) + y[344])/(2*p[74]), (-x[177] - y[338]*(p[77] - p[79]) + y[342])/p[75], (-x[178] + y[339]*(p[78] - p[80]))/p[76], (-x[179] + y[309])/p[87], -p[90]*(p[85]*(u[393] + u[394] - x[179]) + p[86]*x[180] - y[342]) + u[393] + u[394] - x[179], (-x[181] + y[343])/p[92], (x[181] - x[182])/p[94], p[95]*(u[395] - y[340]) - 1.0e-6*x[183], (x[176] - x[184] - 1.0)/p[97], (-x[185] + y[345])/p[99], p[103]*(x[187] - y[365]) - p[113]*x[186], (-p[111]*(x[187] - y[365]) - y[347]*(p[112]*y[347] + y[311]*sin(x[186] - y[312])) - y[348]*(p[112]*y[348] + y[311]*cos(x[186] - y[312])) + y[353])/(2*p[104]), (-x[188] - y[347]*(p[107] - p[109]) + y[351])/p[105], (-x[189] + y[348]*(p[108] - p[110]))/p[106], (-x[190] + y[311])/p[117], -p[120]*(p[115]*(u[397] + u[398] - x[190]) + p[116]*x[191] - y[351]) + u[397] + u[398] - x[190], (-x[192] + y[352])/p[122], (x[192] - x[193])/p[124], p[125]*(u[399] - y[349]) - 1.0e-6*x[194], (x[187] - x[195] - 1.0)/p[127], (-x[196] + y[354])/p[129], p[133]*(x[198] - y[365]) - p[143]*x[197], (-p[141]*(x[198] - y[365]) - y[356]*(p[142]*y[356] + y[313]*sin(x[197] - y[314])) - y[357]*(p[142]*y[357] + y[313]*cos(x[197] - y[314])) + y[362])/(2*p[134]), (-x[199] - y[356]*(p[137] - p[139]) + y[360])/p[135], (-x[200] + y[357]*(p[138] - p[140]))/p[136], (-x[201] + y[313])/p[147], -p[150]*(p[145]*(u[401] + u[402] - x[201]) + p[146]*x[202] - y[360]) + u[401] + u[402] - x[201], (-x[203] + y[361])/p[152], (x[203] - x[204])/p[154], p[155]*(u[403] - y[358]) - 1.0e-6*x[205], (x[198] - x[206] - 1.0)/p[157], (-x[207] + y[363])/p[159], 1 - y[365]]])'

```python
string
```

    'Matrix([[p[43]*(x[165] - y[365]) - p[53]*x[164], (-p[51]*(x[165] - y[365]) - y[329]*(p[52]*y[329] + y[307]*sin(x[164] - y[308])) - y[330]*(p[52]*y[330] + y[307]*cos(x[164] - y[308])) + y[335])/(2*p[44]), (-x[166] - y[329]*(p[47] - p[49]) + y[333])/p[45], (-x[167] + y[330]*(p[48] - p[50]))/p[46], (-x[168] + y[307])/p[57], -p[60]*(p[55]*(u[389] + u[390] - x[168]) + p[56]*x[169] - y[333]) + u[389] + u[390] - x[168], (-x[170] + y[334])/p[62], (x[170] - x[171])/p[64], p[65]*(u[391] - y[331]) - 1.0e-6*x[172], (x[165] - x[173] - 1.0)/p[67], (-x[174] + y[336])/p[69], p[73]*(x[176] - y[365]) - p[83]*x[175], (-p[81]*(x[176] - y[365]) - y[338]*(p[82]*y[338] + y[309]*sin(x[175] - y[310])) - y[339]*(p[82]*y[339] + y[309]*cos(x[175] - y[310])) + y[344])/(2*p[74]), (-x[177] - y[338]*(p[77] - p[79]) + y[342])/p[75], (-x[178] + y[339]*(p[78] - p[80]))/p[76], (-x[179] + y[309])/p[87], -p[90]*(p[85]*(u[393] + u[394] - x[179]) + p[86]*x[180] - y[342]) + u[393] + u[394] - x[179], (-x[181] + y[343])/p[92], (x[181] - x[182])/p[94], p[95]*(u[395] - y[340]) - 1.0e-6*x[183], (x[176] - x[184] - 1.0)/p[97], (-x[185] + y[345])/p[99], p[103]*(x[187] - y[365]) - p[113]*x[186], (-p[111]*(x[187] - y[365]) - y[347]*(p[112]*y[347] + y[311]*sin(x[186] - y[312])) - y[348]*(p[112]*y[348] + y[311]*cos(x[186] - y[312])) + y[353])/(2*p[104]), (-x[188] - y[347]*(p[107] - p[109]) + y[351])/p[105], (-x[189] + y[348]*(p[108] - p[110]))/p[106], (-x[190] + y[311])/p[117], -p[120]*(p[115]*(u[397] + u[398] - x[190]) + p[116]*x[191] - y[351]) + u[397] + u[398] - x[190], (-x[192] + y[352])/p[122], (x[192] - x[193])/p[124], p[125]*(u[399] - y[349]) - 1.0e-6*x[194], (x[187] - x[195] - 1.0)/p[127], (-x[196] + y[354])/p[129], p[133]*(x[198] - y[365]) - p[143]*x[197], (-p[141]*(x[198] - y[365]) - y[356]*(p[142]*y[356] + y[313]*sin(x[197] - y[314])) - y[357]*(p[142]*y[357] + y[313]*cos(x[197] - y[314])) + y[362])/(2*p[134]), (-x[199] - y[356]*(p[137] - p[139]) + y[360])/p[135], (-x[200] + y[357]*(p[138] - p[140]))/p[136], (-x[201] + y[313])/p[147], -p[150]*(p[145]*(u[401] + u[402] - x[201]) + p[146]*x[202] - y[360]) + u[401] + u[402] - x[201], (-x[203] + y[361])/p[152], (x[203] - x[204])/p[154], p[155]*(u[403] - y[358]) - 1.0e-6*x[205], (x[198] - x[206] - 1.0)/p[157], (-x[207] + y[363])/p[159], 1 - y[365]]])'

```python
sys.keys()
```

    dict_keys(['name', 'params_dict', 'f_list', 'g_list', 'x_list', 'y_ini_list', 'y_run_list', 'u_run_dict', 'u_ini_dict', 'h_dict', 'f', 'g', 'x', 'y_ini', 'y_run', 'u_ini', 'u_run', 'h', 'Fx_run', 'Fy_run', 'Gx_run', 'Gy_run', 'Fx_ini', 'Fy_ini', 'Gx_ini', 'Gy_ini', 'Fu_run', 'Gu_run', 'Hx_run', 'Hy_run', 'Hu_run', 'Fx_ini_rows', 'Fx_ini_cols', 'Fy_ini_rows', 'Fy_ini_cols', 'Gx_ini_rows', 'Gx_ini_cols', 'Gy_ini_rows', 'Gy_ini_cols', 'Fx_run_rows', 'Fx_run_cols', 'Fy_run_rows', 'Fy_run_cols', 'Gx_run_rows', 'Gx_run_cols', 'Gy_run_rows', 'Gy_run_cols'])

```python

```


```python
import ipywidgets as ipw
import json
import random
import time
import pandas as pd
import os
import webbrowser
import math
from IPython.display import display, Markdown

# set kinetic parameters
with open("rate_parameters.json") as infile:
    jsdata = json.load(infile)

params = jsdata["kin1"]
```

Copyright **Jacob Martin and Paolo Raiteri**, January 2021

## Numerical solution of chemical equilibrium problems #1
Imagine a simple dimerisation reaction
\begin{equation}
2A \to B
\end{equation}

whose equilibrium constant can be written as
\begin{equation}
K_{eq} = \frac{[B]}{[A]^2} = 0.156
\end{equation}

and wanting to calculate the equilibrium concentrations of $[A]_{eq}$ and $[B]_{eq}$ given their initial concentrations $[A]_{0}$ and $[B]_{0}$.
Although this is a simple problem that can be solved analytically, in this workshop we will learn how we can use an iterative method to numerically solve it.
We will use a relatively simple minimisation procedure that can be applied to a large number of problems, for which it is not possible or it is too complicated to get an analytic solution.

Imagine mixing the reagents and then to be able to monitor the concentration of all the species in the system at discrete time intervals (*timesteps*). What you will see is that the concentrations will change and tend to the equilibrium value. As you have learnt in first year, the reaction quotient, $Q$, can be used to decided which way to reaction will proceed, and that at equilibrium the reaction quotient is equal to the equilibrium constant. Hence, as we have discussed in class, the reaction quotient and the equilibrium constant can be use to define a *driving force* that pulls the system towards equilibrium.

This *driving force* can then be used in conjunction with an *ICE* table to numerically compute the equilibrium concentration of reactant and products.

|      | [A]        | [B]
| :--- | :--------: |:---------:
| *I*  | [A]$_0$    | [B]$_0$
| *C*  | -2x        | x
| *E*  | [A]$_0$-2x | [B]$_0$+x

Here below you can see the working principle of the minimisation procedure that we will emply

1. compute the reaction quotient at beginning of the experiment
\begin{equation}
Q = \dfrac{\mathrm{[B]}_0}{\mathrm{[A]}^2_0}
\end{equation}

2. compute the driving force.

\begin{equation}
F \propto \ln\bigg[\frac{K_{eq}}{Q}\bigg]
\end{equation}

If $K_{eq}/Q<1$ the reaction proceeds towards the reactants, which we can call the *negative* direction. While, if $K_{eq}/Q>1$ the reaction proceeds towards the products, *i.e.* in the positive direction.
You can indeed see that because the *driving force* is defined as the logarithm of the $K_{eq}/Q$ it correctly changes sign, *i.e.* direction, when $Q$ become smaller or larger than $K_{eq}$.

3. compute the new concentrations after a *timestep* has passed.
We now assume that for an arbitrarily short time interval the *driving force* doesn't change and compute the concentrations of all the species in the system aftern that small amount of time has passed, which corresponds to the $x$ in the *ICE* table above.

\begin{equation}
x = \delta\ln\bigg[\frac{K_{eq}}{Q}\bigg]
\end{equation}

There is no unique way to compute x, the only requirement being that it should be a comparatively small change in the system composition, otherwise the procedure will become unstable. This is because our assumption that the *driving force* will break down. In the formula above we have introduced a new parameter $\delta$ that will allow us to control how much the concentrations can change before we recompute the *driving force*.
A reasonable choice for delta could be a value around 20% of the smallest concentration we have in the system.
The Larger values of $\delta$ the faster our procedure will converge, until the calculation becomes unstable and the method will fail to converge.
On the contrary, small values of $\delta$ will always converge to the correct solutions, but it may take a longer number of cycles.

4. Repeat from step 1 until there are no more change in the concentrations of all the species.

Follow now the demonstrator explaining how to create an excel spreadsheet that implements those steps.

5. The calculation has converged when the concentration of the species don't change anymore, *i.e.* the *driving force* is zero and the reaction quotient is equal to the equilibrium constant.

Now try to solve this proble yourself using an excel spreadsheet.
This python program can be used to verify your result.

### Important note on the first cycle:
In some cases one (or more) of the species involved in the reaction may have a zero initial concentration.
Therefore, the calculation of the reaction quotient would give $Q=0$ or $Q=\infty$, which makes the calculation of the force, $\ln\ [K_{eq}Q]$, impossible. In order to circumvent that problem, you can perform a "manual" first step of the minimisation cycle using an arbitrary (small) value for the force; *e.g.* $F=1$ or smaller. If a reactant has a zero concentration, you would have to use a small negative force.
Henceforth, when the concentration of all the species is different from zero and positive, you can follow the procedure outlined above.

- Click `Download CSV` to export the data as a CSV file to verify your result.

```python
def initialise():
    global nPoints
    global concA, concB
    global Keq
    global delta

    nPoints = 20
    concA = 1
    concB = 0.1
    Keq = 0.156
    delta = 0.2

def addLine(t,x,y,res,Q):
    var_list = []
    var_list.append(t)
    var_list.append(x)
    var_list.append(y)
    var_list.append(Q)
    res.loc[len(res)] = var_list

initialise()

```

```python
respath = os.path.join(os.getcwd(), "..", "results.csv")

out_P = ipw.Output()
out_L = ipw.Output()

with out_L:
    display(Markdown("[Download CSV](../results.csv)"))

def force(Q,k):
    if (abs(Q) > 1.e-6):
        force = - math.log(Q/k)
    else:
        force = 1.

    return force

def calc1(btn):
    out_P.clear_output()

    if os.path.exists(respath):
        os.remove(respath)
    res = pd.DataFrame(columns=["step" , "[A]" , "[B]", "Q"])

    A = float(concA_text.value)
    B = float(concB_text.value)
    dx = float(delta_text.value)
    k = float(Keq_text.value)
    n = int(nPoints_text.value)

    Q = B / math.pow(A,2)
    addLine(0,A,B,res,Q)

    for i in range(0, n):
        f = force(Q,k)
        cc = min(A,B)
        A = A - 2 * dx * f * cc
        B = B + dx * f * cc

        Q = B / math.pow(A,2)
        addLine(i,A,B,res,Q)
        # Append result

    res.to_csv(respath, index=False)
    with out_P:
        display(res.tail(n))

btn_calc1 = ipw.Button(description="Get Data", layout=ipw.Layout(width="150px"))
btn_calc1.on_click(calc1)

rows = []

# Equilibrium constant
Keq_text = ipw.Text(str(Keq))

# Initial concentrations
concA_text = ipw.Text(str(concA))

concB_text = ipw.Text(str(concB))

# delta concentration
delta_text = ipw.Text(str(delta))

# Nmber of data points
nPoints_text = ipw.Text(str(nPoints))

rows.append(ipw.HBox([ipw.Label('Initial concentration of A   :  '),concA_text]))
rows.append(ipw.HBox([ipw.Label('Initial concentration of B   :  '),concB_text]))
rows.append(ipw.HBox([ipw.Label('Equilibrium constant         :  '),Keq_text]))
rows.append(ipw.HBox([ipw.Label('Delta concentration          :  '),delta_text]))
rows.append(ipw.HBox([ipw.Label('Number of data point required:  '),nPoints_text]))

rows.append(ipw.HBox([btn_calc1]))

rows.append(ipw.HBox([out_L]))
rows.append(ipw.HBox([out_P]))

ipw.VBox(rows)
```

```python

```