# **`PoissonGeometry`**

## **Instalando `PoissonGeometry`**

Para descargar e instalar el módulo `PoissonGeometry` simplemente ejecute:

```
!pip install poissongeometry==0.1.2
!pip install galgebra==0.4.3
```

Para verficar que el módulo fue instaldo correctamente ejecute:

```
def test_poissongeometry():
  """ Este metodo verifica si el modulo fue instalado correctamente """
  try:
    import poisson
    result = 'El módulo fue instalado correctamente'
  except:
    result = 'El módulo NO fue instalado correctamete'
  return result

test_poissongeometry()
```

**Más información:** en [Github](https://github.com/appliedgeometry/poissongeometry) o en la página [PyPi](https://pypi.org/project/poissongeometry/)

## **Preparando del entorno para trabajar con `PoissonGeometry`***.

### Obteniendo formato $\LaTeX$

Con este código puede imprimir los resultados de ciertas funciones en `PoissonGeometry` con tipografía $\LaTeX$:

```
import sympy
def custom_latex_printer(exp, **options):
    from google.colab.output._publish import javascript
    url = "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.3/latest.js?config=default"
    javascript(url=url)
    return sympy.printing.latex(exp, **options)
sympy.init_printing(use_latex="mathjax", latex_printer=custom_latex_printer)
```

### Bandera $\LaTeX$:

* Para imprimir el resultado de una función en código $\LaTeX$ sólo tenemos que añadir la bandera `latex_format=True`, cuyo valor por defecto es `False`:

        function_name(param_1, ..., param_n, latex_format=True)

### Sintaxis

* Una función escalar en `PoissonGeometry` se escribe usando *expresiones de cadena*.

    Por ejemplo, la función $f:\mathbb{R}_{x}^{3} \to \mathbb{R}$ dada por

    $$f(x_1,x_2,x_3) = ax_1^2 + bx_2^2 + cx_3^2, \quad a,b,c \in \mathbb{R}$$

    se debe escribir como:
```
"a*x1**2 + b*x2**2 + c*x3**2"
```
Aquí, `x1, x2, x3` son variables simbólicas que `PoissonGeometry` define por defecto y que representan en este caso las coordenadas $(x_1,x_2,x_3)$.

    **Observación.** Todos los caracteres que no son coordenadas locales se tratan como parámetros (simbólicos). Por ejemplo `a`, `b`, `c` en la expresión anterior.

    **Nota.** Python admite las siguientes operaciones básicas:

| Expression | Description  || Expression | Description    |
| :--------: | ------------ || :--------: | -------------- |
| +          | Suma         || *          | Multiplicación |
| -          | Resta        || **         | Potencia |
| /          | División     ||

* Un campo multivectorial o una forma diferencial en `Poisson Geometry` se escriben usando *diccionarios* con *tuplas de enteros* como **claves** y *expresiones de cadena* como **valores**.

    Por ejemplo, en $\mathbb{R}^{3}$:
    * El campo vectorial $x_1\frac{\partial}{\partial x_1} + x_2\frac{\partial}{\partial x_2} + x_3 \frac{\partial}{\partial x_3}$ debe escribirse como \\
```{(1,):'x1', (2,):'x2', (3,):'x3'}```

    * El campo bivectorial $x_1\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2} - x_2\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} + x_3 \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3}$ debe escribirse como \\
```{(1,2):'x1', (1,3):'-x2', (2,3):'x3'}```

    * El 3-campo multivectorial $x_1\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3}$ debe escribirse como \\
```{(1,2,3):'x1'}```

**Observaciones:**
1. En `Python`, a una variable \\
```{key_1: value_1, ..., key_n: value_n}``` \\
se le llama 'diccionario'. Cada `key_1,...,key_n` es llamada una *llave* del diccionario y cada `value_1,...,value_n` el *valor* de la correspondiente llave.

2. En nuestro caso, cada clave es una variable de tipo `tupla` y cada valor una variable de tipo `cadena (string)'.

    Si tenemos un multivector $A$ de grado $a$ en $\mathbb{R}^{m}$ $$A = \sum_{1 \leq i_1 < i_2 < \cdots < i_a \leq m} A^{i_1 i_2 \cdots i_a}\,\frac{\partial}{\partial{x_{i_1}}} \wedge \frac{\partial}{\partial{x_{i_2}}} \wedge \cdots \wedge \frac{\partial}{\partial{x_{i_a}}},$$ \\

    las llaves del diccionario deben ser tuplas $(i_1,i_2,\ldots,i_a)$ que corresponden a los índices ordenados $i_1 i_2 \cdots i_a$ de $A^{i_1 i_2 \cdots i_a}$ y los valores la correspondiente expressión de cadena del coeficiente (función escalar) $A^{i_1 i_2 \cdots i_a}$.

    **Nota.** Podemos escribir solamente las claves y valores de los *coeficientes no nulos*.

3. Podemos cambiar el orden de los índices en cada tupla añadiendo el signo menos en el correspondiente valor.

    Por ejemplo,

    * El campo bivectorial $x_1\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2}$ se puede escribir \\
    ```{(1,2): 'x1'}```

        o como

        ```{(2,1): '-x1'}```

     donde este último diccionario corresponde al campo bivectorial $-x_1\frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_1}$.

     **Nota.** Aunque tenemos la opción de ignorar el orden de los índices, recomendamos no hacerlo para evitar posibles errores de cálculo.

4. La sintaxis para formas diferenciales es la misma que para campos multivectoriales.

    Por ejemplo, la 2-forma diferencial en $\mathbb{R}_{x}^4$

    $$-\mathrm{d}x_{1} \wedge \mathrm{d}x_{2} - (x_1 + x_4)\mathrm{d}x_{3} \wedge \mathrm{d}x_{4}$$

    puede escribirse como \\
```{(1,2):'-1', (3,4): '-(x1 + x4)'}```

5. Finalmente, en `PoissonGeometry` se usa la siguiente notación:
    *   `Dxi` es equivalente a $\frac{\partial}{\partial x_{i}}$.
    *   `dxi` es qeuivalente a  $\mathrm{d}x_{i}$.

 Esta asignación se da porque en `SymPy` no es posible definir variables $\frac{\partial}{\partial x_{i}}$ o $\mathrm{d}x_{i}$.

## **Probando la clase `PoissonGeometry`**.

### Instanciación y conocimiento de la Clase

Primero, es necesario instanciar la clase. Para ello, debemos decirle a `PoissonGeometry` la dimensión y la variable simbólica que nombra a las coordenadas.

Por ejemplo, si queremos trabajar en la dimensión 4 y usar $z$ para nombrar a las coordenadas:

```
# Importamos la clase y le damos un nombre corto por simplicidad
from poisson.poisson import PoissonGeometry as pg

# Declaramos las variables y la dimensión
p = pg(4, variable="z")
```

**Observación:** Por defecto, `variable`=`"x"`.

Para conocer la dimensión en la que estamos trabajando escriba:

```
p.dim
```

Para conocer las coordenadas (actuales) escriba:

```
p.coordinates
```

Adicionalmente, `PoissonGeometry` construye una base $\left\{\frac{\partial}{\partial x_{1}},...,\frac{\partial}{\partial x_{n}}\right\}$ de campos vectoriales. En nuestro actual ejemplo, para $n=4$.

```
p.Dx_basis
```

Aún más, es posible operar sobre esta base. Por ejemplo, para calcular productos exteriores:

```
Dz1, Dz2, Dz3, Dz4 = p.Dx_basis
print(F'Wedge product of Dz1 with Dz2: {Dz1 ^ Dz2}') # El producto exterior en Galgebra se indica con el símbolo ^
print(F'Wedge product of Dz1 with Dz2: {Dz1 ^ Dz1}')
```

Las siguientes operaciones se pueden realizar en `Galgebra`:

| Expresión | Descripción | Expresión  | Descripción |
| :----------: | ----------  | :----------: | ----------  |
| *          | Multiplicación | ^         | Producto exterior |
| +          | Suma        | - | Resta |

## **Funciones de `PoissonGeometry`**

### Función: `bivector_to_matrix`

    CALCULA LA MATRIZ DE UN CAMPO BIVECTORIAL

Por ejemplo, la matriz del campo bivectorial en $\mathbb{R}^4$

$$\Pi = x_3\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2} - x_2\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} + x_1 \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3},$$

se puede calcular de la siguiente manera:

```
from poisson.poisson import PoissonGeometry
pg4 = PoissonGeometry(4)

# Ingresamos el campo bivectorial
bivector = {(1,2): 'x3', (1,3): '-x2', (2,3): 'x1'}

pg4.bivector_to_matrix(bivector)
```

**Recuerde.** Si queremos el código $\LaTeX$ de la matriz anterior sólo tenemos que añadir la bandera `latex_format=True`, cuyo valor por defecto es `False`:

```
print(pg4.bivector_to_matrix(bivector, latex_format=True))
```

* El 'poder' de `latex_format`: solo necesita copiar y pegar el resultado en un archivo `.tex`

###Función: `sharp_morphism`

        CALCULA LA IMAGEN DE UNA 1-FORMA DIFERENCIAL BAJO EL MORFISMO DE HACES VECTORIALES 'SHARP' INDUCIDO POR UN CAMPO BIVECTORIAL

Por ejemplo, considere el campo bivectorial de Lie-Poisson en
 $\mathbb{R}^{3}$ $$\Pi = x_3\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2} - x_2\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} + x_1 \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3},$$ \\

asociado al álgebra de Lie $\mathfrak{so}(3)$ [4], y la 1-forma diferencial $$\alpha = x_1
dx_{1} + x_{2} dx_{2} + x_{3} dx_{3}.$$ \\
Para calcular *$\Pi^{\natural}(\alpha)$*, ejecute:

```
from poisson.poisson import PoissonGeometry
pg3 = PoissonGeometry(3)

# Ingresamos el campo bivectorial y la 1-forma diferencial
bivector = {(1,2): 'x3', (1,3): '-x2', (2,3): 'x1'}
alpha = {(1,): 'x1', (2,): 'x2', (3,): 'x3'}

pg3.sharp_morphism(bivector, alpha)
```

Por lo tanto, $\Pi^{\natural}(\alpha)=0$.

###Función: `hamiltonian_vf`

        CALCULA EL CAMPO VECTORIAL HAMILTONIANO DE UNA FUNCIÓN ESCALAR CON RESPECTO A UN CAMPO BIVECTORIAL DE POISSON

Por ejemplo, considere el campo bivectorial de Poisson en $\mathbb{R}^{6}$
$$\Pi = \frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_4} + \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_5} + \frac{\partial}{\partial x_3}\wedge \frac{\partial}{\partial x_6},$$ y la función
$$h = -\frac{1}{x_{2}-x_{1}}-\frac{1}{x_{3}-x_{1}}-\frac{1}{x_{3}-x_{2}} + \frac{1}{2} (x_{4}^{2} + x_{5}^{2} + x_{6}^{2}).$$

El campo vectorial Hamiltoniano de $h$ con respecto a $\Pi$ es dado por
\begin{align}
    X_{h} &= - x_4\frac{\partial}{\partial{x_1}}- x_5\frac{\partial}{\partial{x_2}} - x_6\frac{\partial}{\partial{x_3}} + \left[ \frac{1}{(x_1-x_3)|x_1-x_3|} + \frac{1}{(x_1-x_2)|x_1-x_2|} \right]\frac{\partial}{\partial{x_4}} \\
    &+ \left[ \frac{1}{(x_2-x_3)|x_2-x_3|} + \frac{1}{(x_1-x_2)|x_1-x_2|} \right]\frac{\partial}{\partial{x_5}} -\left[ \frac{1}{(x_2-x_3)|x_2-x_3|} + \frac{1}{(x_1-x_3)|x_1-x_3|} \right]\frac{\partial}{\partial{x_6}}.
\end{align}

Este campo vectorial es el asociado al sistema Hamiltoniano de un caso particular del problema de los tres cuerpos[3].
Para calcular este campo vectorial con `Poisson Geometry`, ejecute:

```
# Este módulo es para una impresión más legible de Python
import pprint
pp = pprint.PrettyPrinter(indent=2)

# Instanciamos la clase
from poisson.poisson import PoissonGeometry
pg6 = PoissonGeometry(6)

bivector = {(1,4):  '1', (2,5):  '1', (3,6):  '1'}
h = '- 1/sqrt((x2 - x1)**2) - 1/sqrt((x3 - x1)**2) - 1/sqrt((x3 - x2)**2)+ 1/2*(x4**2 + x5**2 + x6**2)'

pp.pprint(pg6.hamiltonian_vf(bivector, h))
```

Debemos recordar que declaramos `Dxi` $\equiv \frac{\partial}{\partial x_{i}}$.

###Función: `lichnerowicz_poisson_operator`

            CALCULA LA IMAGEN DE UN CAMPO MULTIVECTORIAL BAJO EL OPERADOR DE COFRONTERA INDUCIDO POR UN CAMPO BIVECTORIAL DE POISSON,
            RELATIVO AL CORCHETE DE SCHOUTEN-NIJENHUIS PARA CAMPOS MULTIVECTORIALES

Consideremos el campo bivectorial en $\mathbb{R}^{3}$
$$\Pi = x_{1}\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2} - ax_{1}x_{3} \frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} + x_3(2x_{1} - ax_{2}) \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3},$$ el 3-campo multivectorial $$A = (bx_{2}^{2}x_{3} + c)\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3}.$$

En general, calcular el corchete de Schouten-Nijenhuis entre dos multivectores es una cuestión complicada.

La clase `PoissonGeometry` puede hacer este cálculo muy rápidamente!
:

```
from poisson.poisson import PoissonGeometry
pg3 = PoissonGeometry(3)

P = {(1,2): 'x1**2', (1,3): '-a*x1*x3', (2,3): 'x3*(2*x1 - a*x2)'}
A = {(1,2,3): 'b*x2**2*x3 + c'}
pg3.lichnerowicz_poisson_operator(P, A)
```

Por tanto, $A$ es un cociclo de $\Pi$. El grupo de cohomología formal de grado 3 del complejo de Lichnerowicz-Poisson de $\Pi$ es dado por (see [1]):

$$\mathscr{H}^{3}(\Pi) \simeq\ \mathbb{R} \cdot \frac{\partial}{\partial{x_{1}}} \wedge \frac{\partial}{\partial{x_{2}}} \wedge \frac{\partial}{\partial{x_{3}}} \bigoplus \mathbb{R} \cdot x_{2}^2x_{3}\,\frac{\partial}{\partial{x_{1}}} \wedge \frac{\partial}{\partial{x_{2}}} \wedge \frac{\partial}{\partial{x_{3}}}.$$

### Función: `curl_operator`

Considere el siguiente campo bivectorial de Poisson (Flaskcha-Ratiu) en $\mathbb{R}^6$

$$\Pi = x_{1}x_{2}\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2} - x_{1}x_{3} \frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} + x_2 x_3 \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3} + \frac{\partial}{\partial x_3}\wedge \frac{\partial}{\partial x_4} - \frac{\partial}{\partial x_3}\wedge \frac{\partial}{\partial x_5} + \frac{\partial}{\partial x_4}\wedge \frac{\partial}{\partial x_6}.$$

Con `curl_operator` podemos calcular la divergencia de $\Pi$ respecto a la forma de volumen $f\Omega_{0}$ en $\mathbb{R}^6$, donde $\Omega_{0}$ es la forma de volumen euclídea y $f$ es una función no nula:

```
from poisson.poisson import PoissonGeometry
pg6 = PoissonGeometry(6)

bivector = {(1,2): 'x1*x2', (1,3): '-x1*x3', (2,3): 'x2*x3', (3,4): '1', (3,5): '-1', (4,6): '1'}
pg6.curl_operator(bivector, 1)
```

Por tanto, la divergencia de $\Pi$ es trivial.

__Observación.__ El parámetro `1` en `curl_operator` significa que $f \equiv 1$, por lo tanto la divergencia es con respecto al volumen euclidiano $\Omega_{0}$ en $\mathbb{R}^6$.

### Función: `poisson_bracket`

            CALCULA EL CORCHETE DE POISSON, INDUCIDO POR UN CAMPO BIVECTORIAL DE POISSON, DE DOS FUNCIONES ESCALARES

Por ejemplo, considere el campo bivectorial de Lie-Poisson en $\mathbb{R}^{3}$,

$$\Pi = x_3\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2} - x_2\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} + x_1 \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3},$$

asociado al álgebra de Lie $\mathfrak{sl}(2)$ [7]. Es bien conocido que $\{x_{1},x_{2}\}_{\Pi} = -x_{3}$, $\{x_{2},x_{3}\}_{\Pi} = x_{1}$ y $\{x_{3},x_{1}\}_{\Pi} = x_{2}$ son las relaciones de conmutación de esta álgebra de Lie:

```
from poisson.poisson import PoissonGeometry
pg3 = PoissonGeometry(3)

bivector ={(1,2): '-x3', (1,3): '-x2', (2,3): 'x1'}
x1_x2 = pg3.poisson_bracket(bivector, 'x1', 'x2')
x2_x3 = pg3.poisson_bracket(bivector, 'x2', 'x3')
x3_x1 = pg3.poisson_bracket(bivector, 'x3', 'x1')

print(F'{{x1, x2}} = {x1_x2}')
print(F'{{x2, x3}} = {x2_x3}')
print(F'{{x3, x1}} = {x3_x1}')
```

###Función: `modular_vf`

Considere el campo bivectorial de Poisson en $\mathbb{R}^{4}$
$$\Pi = 2x_{4}\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} + 2x_{3} \frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_4} - 2x_{4} \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3} + 2x_{3} \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_4} + (x_{1}-x_{2}) \frac{\partial}{\partial x_3}\wedge \frac{\partial}{\partial x_4}.$$ Este campo bivectorial es un caso particular de una familia de campos bivectoriales de Poisson que surgen en el análisis de la estabilidad orbital del oscilador de Pais-Uhlenbeck.

* La función `modular_vf` calcula el campo modular de $\Pi$ respecto a una forma de volumen $f\Omega_{0}$ en $\mathbb{R}^4$ ( aquí $f$ es una función no nula y $\Omega_{0}$ es la forma de volumen euclidiana en $\mathbb{R}^4$ ):

```
from poisson.poisson import PoissonGeometry
pg4 = PoissonGeometry(4)

bivector ={(1,3):'2*x4', (1,4): '2*x3', (2,3): '-2*x4', (2,4): '2*x3', (3,4):'x1-x2'}
pg4.modular_vf(bivector, 1)
```

Por tanto, en este caso el campo modular de $\Pi$ respecto a la forma de volumen euclídea es trivial.

__Nota__: La segunda entrada para la función `curl_operator`, en este ejemplo `1`, puede tomar cualquier función real $f$ para modificar la forma del volumen.

Por ejemplo, para el campo bivectorial en $\mathbb{R}^3$, $$\Pi = x_{3}\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2} -x_{2} \frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} + x_{1}\frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3},$$

y la función $f(x_{1},x_{2},x_{3}) = \mathrm{exp}(x_1 + x_2 + x_3)$, calculemos el campo modular de $\Pi$ con respecto a la forma de volumen $f\Omega_{0}$:

```
from poisson.poisson import PoissonGeometry
pg3 = PoissonGeometry(3)

bivector = {(1,2): 'x3', (1,3): '-x2', (2,3): 'x1'}
function = 'exp(x1 + x2 + x3)'
pg3.modular_vf(bivector, function)
```

Por lo tanto, el campo modular de $\Pi$ respecto a la forma de volumen $f\Omega_{0}$ es dado por
    \begin{equation*}
        (x_{3} - x_{2})\frac{\partial}{\partial{x_{1}}} + (x_{1} - x_{3})\frac{\partial}{\partial{x_{2}}} + (x_{2} - x_{1})\frac{\partial}{\partial{x_{3}}}.
    \end{equation*}

### Función: `flashcka_ratiu_bivector`

            CALCULA EL CAMPO BIVECTORIAL DE FLASCHKA-RATIU Y LA FORMA SIMPLÉCTICA CORRESPONDIENTE DE UN CONJUNTO 'MÁXIMO' DE FUNCIONES ESCALARES

Por ejemplo, consideremos las funciones
$$f(x_1, x_2, x_3, x_4) = x_4$$
y
$$g(x_1, x_2, x_3, x_4) = −x_1^2 + x_2^2 + x_3^2$$

que describen localmente una singularidad quebrada de una foliación de Lefschetz en una variedad 4-dimensional [6].

Podemos construir un campo bivectorial de Poisson $\Pi$ de tal manera que las funciones $f$ y $g$ sean Casimires de $\Pi$:

```
from poisson.poisson import PoissonGeometry
pg4 = PoissonGeometry(4)

casimirs = ['x4', '-x1**2 + x2**2 + x3**2']
pg4.flaschka_ratiu_bivector(casimirs)
```

Para obtener la forma simpléctica de $\Pi$ en hojas bidimensionales, añadimos la bandera `symplectic_form=True`:

```
bivector, symplectic_form = pg4.flaschka_ratiu_bivector(casimirs, symplectic_form=True)
print(f'Poisson bivector field: {bivector}')
print(f'Symplectic form: {symplectic_form}')
```

###Función: `linear_normal_form_R3`

            CALCULA UNA FORMA NORMAL DE CUALQUIER CAMPO BIVECTORIAL DE LIE-POISSON EN R^3

Por ejemplo, considere el campo bivectorial de Lie-Poisson en $\mathbb{R}^{3}$,
$$\Pi = -10x_{3}\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2} +10x_{2} \frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} -10x_{1}\frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3}$$

Para calcular una forma normal de $\Pi$, ejecute:

```
from poisson.poisson import PoissonGeometry
pg3 = PoissonGeometry(3)

bivector = {(1,2): '-10*x3', (1,3): '10*x2', (2,3): '-10*x1'}
pg3.linear_normal_form_R3(bivector)
```

Notemos que $\Pi$ es un múltiplo ($-10$) del campo bivectorial de Lie-Poisson en $\mathbb{R}^{3}$ inducido por el álgebra de Lie $\mathfrak{so}(3)$. Por lo tanto, pertenecen a la misma clase de equivalencia.

### Función: `one_forms_bracket`

            CALCULA EL CORCHETE DE LIE DE DOS 1-FORMAS DIFERENCIALES INDUCIDO POR UN CAMPO BIVECTORIAL DE POISSON

Por ejemplo, considere el campo bivectorial de Poisson en $\mathbb{R}^{4}$
$$\Pi = \big( x_3^2 + x_4^2 \big)\frac{\partial}{\partial{x_{1}}} \wedge \frac{\partial}{\partial{x_{2}}}
        + \big( x_2x_3 - x_1x_4\big)\frac{\partial}{\partial{x_{1}}} \wedge \frac{\partial}{\partial{x_{3}}}
        - \big( x_1x_3 + x_2x_4\big)\frac{\partial}{\partial{x_{1}}} \wedge \frac{\partial}{\partial{x_{4}}}
        + \big( x_1x_3 + x_2x_4\big)\frac{\partial}{\partial{x_{2}}} \wedge \frac{\partial}{\partial{x_{3}}}
        + \big( x_2x_3 - x_1x_4\big)\frac{\partial}{\partial{x_{2}}} \wedge \frac{\partial}{\partial{x^{4}}}
        + \big( x_1^2 + x_2^2 \big)\frac{\partial}{\partial{x_{3}}} \wedge \frac{\partial}{\partial{x_{4}}},$$

y las 1-formas diferenciales
$$\alpha = x_1\mathrm{d}{x_1} - x_2\mathrm{d}{x_2} + x_3\mathrm{d}{x_3} - x_4\mathrm{d}{x_4} \quad \mathrm{y} \quad \beta = x_2\mathrm{d}{x_1} + x_1\mathrm{d}{x_2} + x_4\mathrm{d}{x_3} + x_3\mathrm{d}{x_4}.$$

Para calcular el corchete de Lie, inducido por $\Pi$, de $\alpha$ y $\beta$, ejecute:

```
from poisson.poisson import PoissonGeometry
pg4 = PoissonGeometry(4)

bivector ={(1,2): 'x3**2 + x4**2', (1,3): 'x2*x3 - x1*x4', (1,4): '-x1*x3 - x2*x4', (2,3): 'x1*x3 + x2*x4', (2,4): 'x2*x3 - x1*x4', (3,4): 'x1**2 + x2**2'}
alpha = {(1,): 'x1', (2,): '-x2', (3,): 'x3', (4,): '-x4'}
beta = {(1,): 'x2', (2,): 'x1', (3,): 'x4', (4,): 'x3'}
pg4.one_forms_bracket(bivector, alpha, beta)
```

So $\{\alpha, \beta\}_{\Pi} = 0$.

## **Applicaciones**

### Función: `gauge_transformations`

            CALCULA LA TRANSFORMACIÓN GAUGE DE UN CAMPO BIVECTORIAL CON RESPECTO A UNA 2-FORMA DIFERENCIAL

Por ejemplo, considere un campo bivectorial arbitrario en $\mathbb{R}^3$,

$$\Pi=\Pi_{12} \frac{\partial}{\partial x_{1}} \wedge \frac{\partial}{\partial x_{2}} + \Pi_{13} \frac{\partial}{\partial x_{1}} \wedge \frac{\partial}{\partial x_{3}} + \Pi_{23} \frac{\partial}{\partial x_{2}} \wedge \frac{\partial}{\partial x_{3}}$$

y una 2-forma diferencial,

$$\lambda = \lambda_{12} \mathrm{d}x_{1}\wedge \mathrm{d}x_{2} + \lambda_{13} \mathrm{d}x_{1}\wedge \mathrm{d}x_{3} + \lambda_{23} \mathrm{d}x_{2}\wedge \mathrm{d}x_{3}$$

Para calcular la transformación gauge de $\Pi$ inducida por $\lambda$, ejecute:

```
import pprint
pp = pprint.PrettyPrinter(indent=2)

from poisson.poisson import PoissonGeometry
pg3 = PoissonGeometry(3)

P = {(1,2):  'P12', (1,3):  'P13', (2,3):  'P23'}
L = {(1,2): 'L12', (1,3): 'L13', (2,3): 'L23'}
gauge_bivector, determinant = pg3.gauge_transformation(P, L)

print('L-gauge transformation of P:')
pp.pprint(gauge_bivector)
print(f'\nIt\'s well-defined on the open subset \n{{{determinant} != 0}}\nof R^3')
```

* De esta manera obtenemos [5]:

**Proposición 3.1** Sea $\Pi$ un campo bivectorial en una variedad 3-dimensional lisa $M$. Entonces, dado una 2-forma diferencial $\lambda$ en $M$, la transformación $\lambda$-gauge $\overline{\Pi}$ de $\Pi$ está bien definida en el subconjunto abierto de $M$
    \begin{equation*}
        \{F := \big\langle \lambda,\Pi \big\rangle + 1 \neq 0 \} \subseteq M.
    \end{equation*}
Aún más, $\overline{\Pi}$ es dado por
    \begin{equation*}
        \overline{\Pi} = \tfrac{1}{F}\Pi.
    \end{equation*}
Si $\Pi$ es un campo bivectorial de Poisson y $\lambda$ es cerrada a lo largo de las hojas de $\Pi$, entonces $\Pi$ es también de Poisson.

### Función: `jacobiator`

            CALCULA EL CORCHETE DE SCHOUTEN-NIJENHUIS DE UN CAMPO BIVECTORIAL CONSIGO MISMO

Por ejemplo, podemos modificar el siguiente campo bivectorial $4$-parametrizado en $\mathbb{R}^{4}$

$$ \Pi=a_1 x_2 \frac{\partial}{\partial x_{1}} \wedge \frac{\partial}{\partial x_{2}} + a_2 x_3 \frac{\partial}{\partial x_{1}} \wedge \frac{\partial}{\partial x_{3}} + a_3 x_4 \frac{\partial}{\partial x_{1}} \wedge \frac{\partial}{\partial x_{4}} + a_4 x_1 \frac{\partial}{\partial x_{2}} \wedge \frac{\partial}{\partial x_{3}},$$

auxiliándonos de la función `jacobiator` para construir una familia de campos bivectoriales de Poisson en $\mathbb{R}^{4}$:

```
from poisson.poisson import PoissonGeometry
pg4 = PoissonGeometry(4)

bivector = {(1,2): 'a1*x2', (1,3): 'a2*x3', (1,4): 'a3*x4', (2,3): 'a4*x1'}
pg4.jacobiator(bivector)
```

Por tanto,
    \begin{equation*}
        [\hspace{-0.065cm}[ \Pi,\Pi ]\hspace{-0.065cm}] = -2a_{4}(a_{1}+a_{2})x^1\,\frac{\partial}{\partial{x^{1}}} \wedge \frac{\partial}{\partial{x^{2}}} \wedge \frac{\partial}{\partial{x^{3}}} - 2a_{3}a_{4}x^4\frac{\partial}{\partial{x^{2}}} \wedge \frac{\partial}{\partial{x^{3}}} \wedge \frac{\partial}{\partial{x^{4}}}.
    \end{equation*}

Por lo tanto, tenemos dos casos, explicados en el siguiente lema [5].

**Lema 3.2** Si $a_{4}=0$, entonces $\Pi$ determina una familia de 3-parámetrizada de campos bivectoriales de Poisson en $\mathbb{R}^{4}_{x}$:
    \begin{equation}
        \Pi \,=\, a_{1}x^2\,\frac{\partial}{\partial{x^{1}}} \wedge \frac{\partial}{\partial{x^{2}}}
            + a_{2}x^3\,\frac{\partial}{\partial{x^{1}}} \wedge \frac{\partial}{\partial{x^{3}}}
            + a_{3}x^4\,\frac{\partial}{\partial{x^{1}}} \wedge \frac{\partial}{\partial{x^{4}}}.
    \end{equation}

Si $a_{2}=-a_{1}$ y $a_{3}=0$, entonces $\Pi$ determina una familia de 2-parametrizada de campos bivectoriales de Poisson en $\mathbb{R}^{4}_{x}$:
\begin{equation}
        \Pi \,=\, a_{1}x^2\,\frac{\partial}{\partial{x^{1}}} \wedge \frac{\partial}{\partial{x^{2}}} - a_{1}x^3\,\frac{\partial}{\partial{x^{1}}} \wedge \frac{\partial}{\partial{x^{3}}} + a_{4}x^1\,\frac{\partial}{\partial{x^{2}}} \wedge \frac{\partial}{\partial{x^{3}}}.
    \end{equation}

## **Funciones 'Test-Type'**

Permítanos verificar si un objeto geométrico dado en una variedad de Poisson satisface ciertas propiedades.

### Función: `is_homogeneous_unimodular`

            VERIFICA SI UN CAMPO BIVECTORIAL DE POISSON HOMOGÉNEO EN R^m ES UNIMODULAR, O NO

Por ejemplo, considere el campo bivectorial en $\mathbb{R}^{4}$
$$\Pi = 2x_{4}\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} + 2x_{3} \frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_4} - 2x_{4} \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3} + 2x_{3} \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_4} + (x_{1}-x_{2}) \frac{\partial}{\partial x_3}\wedge \frac{\partial}{\partial x_4}.$$

Este campo bivectorial de Poisson surge en el análisis de la estabilidad orbital del oscilador de Pais-Uhlenbeck en $\mathbb{R}^{4}$ [2]. Como vimos anteriormente, $\Pi$ tiene campo modular trivial relativo a la forma de volumen euclídea en $\mathbb{R}^{4}$, por tanto, es un campo bivectorial de Poisson unimodular en $\mathbb{R}^{4}$. Podemos verificar esto con:

```
from poisson.poisson import PoissonGeometry
pg4 = PoissonGeometry(4)

P ={(1,3): '-2*x4', (1,4): '2*x3', (2,3): '-2*x4', (2,4): '2*x3', (3,4): 'x1 + x2'}
pg4.is_homogeneous_unimodular(P)
```

### Función: `isomorphic_lie_poisson`

            VERIFICA SI DOS CAMPOS BIVECTORIALES DE LIE-POISSON EN R^3 SON ISOMORFOS, O NO

Por ejemplo, los campos bivectoriales en $\mathbb{R}^3$

$$\Pi_{1} = x_{3}\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2} -x_{2} \frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} + x_{1}\frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3},$$
y
$$\Pi_{2} = -x_{3}\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2} -x_{2} \frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} + x_{1}\frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3},$$ \\

que son inducidos por las álgebras de Lie $\mathfrak{so}(3)$ y $\mathfrak{sl}(2)$, respectivamente, NO son isomorfos:

```
from poisson.poisson import PoissonGeometry
pg3 = PoissonGeometry(3)

P1 ={(1,2): 'x3', (1,3): '-x2', (2,3): 'x1'}
P2 ={(1,2): '-x3', (1,3): '-x2', (2,3): 'x1'}
pg3.isomorphic_lie_poisson_R3(P1, P2)
```

### Función: `is_poisson_bivector`

            VERIFICA SI UN CAMPO BIVECTORIAL ES UN CAMPO BIVECTORIAL DE POISSON, O NO

Por ejemplo, podemos verificar que el campo bivectorial en $\mathbb{R}^{4}$,

$$\Pi = x_{2}\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2} + x_{3} \frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} + x_{4} \frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_4} + x_{1} \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3}$$

NO es un campo bivectorial de Poisson:

```
from poisson.poisson import PoissonGeometry
pg4 = PoissonGeometry(4)

bivector = {(1,2): 'x2', (1,3): 'x3', (1,4): 'x4', (2,3): 'x1'}
pg4.is_poisson_bivector(bivector)
```

###Función: `is_in_kernel`

            VERIFICA SI UNA 1-FORMA DIFERENCIAL PERTENECE AL KERNEL DE UN CAMPO BIVECTORIAL (DE POISSON)

Por ejemplo, para el campo bivectorial cuadrático de Flaschka-Ratiu en $\mathbb{R}^{4}$ [6]

\begin{align*}
\Pi &= (x_{3}^{2}+x_{4}^{2})\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2} + (x_{2}x_{3} - x_{1}x_{4}) \frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} - (x_{1}x_{3} + x_{2}x_{4}) \frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_4} + (x_{1}x_{3} + x_{2}x_{4}) \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3} \\ &+ (x_{2}x_{3} - x_{1}x_{4}) \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_4} + (x_{1}^{2} + x_{2}^{2}) \frac{\partial}{\partial x_3}\wedge \frac{\partial}{\partial x_4}
\end{align*}

podemos verificar que la 1-forma diferencial $\alpha = x_{1}\mathrm{d}x_{1} + x_{2}\mathrm{d}x_{2} + x_{3}\mathrm{d}x_{3} + x_{4}\mathrm{d}x_{4}$ pertenece al kernel de $\Pi$. En otras palabras, que $\Pi^{\#}(\alpha)=0$:

```
from poisson.poisson import PoissonGeometry
pg4 = PoissonGeometry(4)

bivector = {(1,2): 'x3**2 + x4**2', (1,3): 'x2*x3 - x1*x4', (1,4): '-x1*x3 - x2*x4',
            (2,3): 'x1*x3 + x2*x4', (2,4): 'x2*x3 - x1*x4', (3,4): 'x1**2 + x2**2'}
alpha = {(1,): 'x1', (2,): '-x2', (3,): 'x3', (4,): '-x4'}
pg4.is_in_kernel(bivector, alpha)
```

### Funciones: `is_casimir` and `is_poisson_vf`

                DADO UN CAMPO BIVECTORIAL DE POISSON P, CON ESTAS FUNCIONES PODEMOS VERIFICAR SI UNA FUNCIÓN ESCALAR ES UNA FUNCIÓN DE CASIMIR DE P
                O SI UN CAMPO VECTORIAL ES UN CAMPO VECTORIAL DE POISSON PARA P, RESPECTIVAMENTE

Consideremos el campo bivectorial de Lie-Poisson en $\mathbb{R}^{3}$

$$\Pi = -x_3\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2} - x_2\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} + x_1 \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3},$$

asociado al álgebra de Lie $\mathfrak{sl}(2)$ y una función de Casimir $K$ de $\Pi$ dada por

$$K = x_{1}^{2} + x_{2}^{2} + x_{3}^{2}$$:

```
from poisson.poisson import PoissonGeometry
pg3 = PoissonGeometry(3)

bivector = {(1,2): '-x3', (1,3): '-x2', (2,3): 'x1'}
K = 'x1**2 + x2**2 - x3**2'
pg3.is_casimir(bivector, K)
```

Esto verifica que en efecto $K$ es una función de Casimir de $\Pi$. Ahora, consideremos la función $f:\mathbb{R}\to\mathbb{R}$ definida por
$$f(t) := \left\{
            \begin{array}{ll}
              e^{-\frac{1}{t^2}} & \hbox{if} \quad t>0, \\
              0 & \hbox{en otro caso.}
            \end{array}
          \right.$$

Definamos una función suave $F$ by $F:=f\circ K$. Entonces, el campo vectorial
$$ W := \frac{x_1 F}{x_1^2 + x_2^2}\,\frac{\partial}{\partial{x_1}} \,+\, \frac{x_1 F}{x_2^2 + x_2^2} \frac{\partial}{\partial{x_2}}$$

es un campo vectorial de Poisson para $\Pi$:

```
from poisson.poisson import PoissonGeometry
pg3 = PoissonGeometry(3)

bivector = {(1,2): '-x3', (1,3): '-x2', (2,3): 'x1'}
W = {(1,): 'x1*exp(-1/(x1**2 + x2**2 - x3**2))/(x1**2 + x2**2)', (2,): 'x2*exp(-1/(x1**2 + x2**2 - x3**2))/(x1**2 + x2**2)', (3,): 0}
pg3.is_poisson_vf(bivector, W)
```

Notemos que $W$ **No** es un campo **Hamiltoniano** de $\Pi$ [8].

### Función: `is_poisson_pair`

            VERIFICA SI UN PAR DE CAMPOS BIVECTORIALES DE POISSON FORMAN UN PAR POISSON

Por ejemplo, sean
$$ \Pi = ax_1 x_2\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2} - b x_1 x_3\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} + b x_2 x_3 \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3}, $$
y
$$ \Psi = x_3^{2} \frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2}.$$

Entonces:

```
from poisson.poisson import PoissonGeometry
pg = PoissonGeometry(3)

Pi = {(1,2): 'a*x1*x2', (1,3): '-b*x1*x3', (2,3): 'b*x2*x3'}
Psi = {(1,2): 'x3**2'}
pg3.is_poisson_pair(Pi, Psi)
```

Por tanto, $\Pi$ y $\Psi$ forman un par de Poisson.

## __Bibliografía__

[1] M. Ammar, G. Kass, M. Masmoudi, N. Poncin, *Strongly R-Matrix Induced Tensors, Koszul Cohomology, and Arbitrary-Dimensional Quadratic Poisson Cohomology*, Pac. J. Math. 245, 1-23 (2010)

[2] M. Avendaño-Camacho, J. A. Vallejo and Yu. Vorobiev, *A Perturbation Theory Approach to the Stability of the Pais-Uhlenbeck Oscillator*, J. Math. Phys. 58, (2017)

[3] P. G. Breen, C. N. Foley, T. Boekholt, S. P. Zwart, *Newton vs the Machine: Solving the Chaotic Three-Body Problem Using Deep Neural Networks*, arXiv:1910.07291 [astro-ph.GA]

[4] J. P. Dufour and N. T. Zung, *Poisson Structures and their Normal Forms*, Progress in Mathematics, 242, Birkh\"auser Verlag, Basel, (2005)

[5] M. Evangelista-Alvarado, J. C. Ruíz-Pantaleón, P. Suárez-Serrato, *On Computational Poisson Geometry I: Symbolic Foundations*

[6] L. C. Garcia-Naranjo,  P.  Suárez-Serrato  and  R.  Vera, *Poisson  Structures  on  Smooth 4-manifolds*, Lett. Math. Phys. 105, 1533-1550 (2015)

[7] C. Laurent-Gengoux, A. Pichereau and P. Vanhaecke, *Poisson Structures*,
Grundlehren der mathematischen Wissenschaften, 347, Springer-Verlag Berlin Heidelberg, (2013)

[8] N. Nakanishi, *On the Structure of Infinitesimal Automorphisms of Linear Poisson Manifolds I*, J. Math. Kyoto Univ. 31, 71-82 (1991)


# Week 1

__Goals for this week__

We will talk about the organization of this course, including the weekly tasks and 3 "mini"-projects that await you during on during this semester.
We will introduce the frameworks you will use and a you will get a geniality quick course (rychlokurz geniality) in Python, required library and standard structures.

__What is this file?__

This is a Jupyter notebook. A file, that contains python scripts and also stores the binary results.

__How can I run it?__

Firs you have to run/configure your Jupyter server in your desired python environment.
Then just follow this notebook and run it cell by cell. Try to answer - code - the questions, so you'll get better grasp on python array/list/dictionary structures.

## Task for today
Your first task is to configure python environment, Tensorflow (CPU or GPU based on you HW), PyTorch, and to practice python concepts in the scripts below.
You should understand it fully, otherwise review your knowledge before you proceed. The gained knowledge will be helpful for the next weeks tasks and then data processing for your projects.

```python
# Line comment
"""
Block comment
"""

# input and output
name=input()
print("Hello, "+name)

```

    Hello, Neural Network

```python
# variables don't need explicit type declaration
var = 'Neural Networks rule!'
var = 2021
print(var)
var = 20.21
print(var)
var = True
print(var)
var = [29,1,2021]
print(var)
var = {'NN':'Rule!', 'year':2021}
print(var)

# Basic types
print(2022)                         # integer
print(13.07)                        # float
print('cool')                       # string
print(True, False)                  # boolean operands
print(None)                         # null-like operand
# python specific
print([12,34,'hello','world'])      # list
print({123:456,'DL':'NN'})          # dictionary

# Type conversion
print(float('36.5'))
print(int(36.5))
print(str(3.65), 'length:', len(str(3.65)))

# Basic operations
a = 2 + 5 - 2.5
a += 3
b = 2 ** 3  # exponentiation
print(a, b)
print(5 / 2)
print(5 // 2)  # Notice the difference between these two
print('DL' + 'NN')
# All compound assignment operators available
# including += -= *= **= /= //=
# pre/post in/decrementers not available (++ --)

# F-strings
print(f'1 + 2 = {1 + 2}, a = {a}')
print('1 + 2 = {1 + 2}, a = {a}')  # We need the f at the start for {} to work as expression wrappers.

```

    2021
    20.21
    True
    [29, 1, 2021]
    {'NN': 'Rule!', 'year': 2021}
    2022
    13.07
    cool
    True False
    None
    [12, 34, 'hello', 'world']
    {123: 456, 'DL': 'NN'}
    36.5
    36
    3.65 length: 4
    7.5 8
    2.5
    2
    DLNN
    1 + 2 = 3, a = 7.5
    1 + 2 = {1 + 2}, a = {a}

```python
# Conditions
a = 6
b = 2
if a > 4 and b < 3:  # and, or and not are the basic logical operators
    print('a')   # Indentation by spaces or tabs tells us where the statement belongs. print('a') is in the if.
elif b > 5:
    print('b')   # Indentation by spaces or tabs tells us where the statement belongs. print('b') is in the elif.
else:
    print('c')   # Indentation by spaces or tabs tells us where the statement belongs. print('c') is in the else.
print('d')       # But print('d') is outside, it will print every time.

# Loops
while a < 10:
    if b > 3:
        a += 1  # More indentation for code that is "deeper"
    else:
        a += 2
print(f'a = {a}')

# 'while' loops are not considered 'pythonic'. 'for' loops are more common
for char in 'string':
    print(char)

```

    a
    d
    a = 10
    s
    t
    r
    i
    n
    g

```python
# Lists - work like C arrays, but they are not dependent on element type
a = [1, 2.4, 10e-7, 0b1001, 'some "text"'] # embedded "" in '', works vice versa
len(a)  # Length
print(a[1])  # Second element
print(a[1:3])  # Second to third element
a.append(4)  # Adding at the end
del a[3]  # Removing at the index
print([])  # Empty array
print(a)

# This is why for loops are used more often
for el in a:
# but be careful with iterations over list which contain different var types - it is your responsibility
    print(el + 1)

```

```python
a = [1, 2.4, 10e-7, 0b1001]
# We can define lists with list comprehension statements
b = [el + 2 for el in a]
print(b)

```

    [3, 4.4, 2.000001, 11]

```python
# Dictionaries - key-based structures
a = {
    'layer_1': 'dense',
    5: 'five',
    4: 'four',
    'result': [1, 2, 3]
}
print(a['layer_1'])
a['layer_2'] = 'ReLU'
if 'result' in a:  # Does key exist?
    print(a['result'])
{}  # Empty
del a[5]  # Remove record

print()
print('Keys:')
for key in a:
    print(key)

print()
print('Keys and values:')
for key, value in a.items():
    print(key, ':', value)

# Dictionaries can be also defined via comprehension statement
a = {i: i**2 for i in [1, 2, 3, 4]}
print(a)

```

    dense
    [1, 2, 3]

    Keys:
    layer_1
    4
    result
    layer_2

    Keys and values:
    layer_1 : dense
    4 : four
    result : [1, 2, 3]
    layer_2 : ReLU
    {1: 1, 2: 4, 3: 9, 4: 16}

```python
# Most common and useful iterators
print('range(start, stop, step)')
for i in range(10,20,2):
    print(i)

print()
print('enumerate')
lowercase = ['a', 'b', 'c']
for i, el in enumerate(lowercase): # iterates over elements attaching the index order
    print(i, el)

print()
print('zip') # Like a zip - side-by-side merges lists together for iteration
uppercase = ['A', 'B', 'C']
numbers = [1,2,3]
for n, a, b in zip(numbers,lowercase, uppercase):
    print(n, a, b)

```

    range(start, stop, step)
    10
    12
    14
    16
    18

    enumerate
    0 a
    1 b
    2 c

    zip
    1 a A
    2 b B
    3 c C

```python
# Functions
def example_function(a, b=1, c=1):  # b and c have default values
    return a*b, a*c  # we return two values at the same time

a, b = example_function(1, 2, 3)  # and we can assign both values at the same time as well
print(a, b)
print(example_function(4))
print(example_function(5, 2))
print(example_function(5, c=2))  # Notice how do the arguments behave

# Classes
class A:

    def __init__(self, b):  # Constructor
        self.b = b  # Object variable

    def add_to_b(self, c):  # self is always the first argument and it references the object itself
        self.b += c

    def sub_from_b(self, c):
        self.add_to_b(-c)  # Calling object method

    def __str__(self): # every python class contain several default methods that start and end with __
        return f'Class A, b={self.b}'

    # be careful with naming and using underscores _
    # private, protected and public is expressed by underscores
    # default is public
    def foo(self):
        print("I'm public")

    def _bar(self):
        print("I'm protected")

    def __nope(self):
        print("You shall not print me, I'm private")

a = A(5)
a.add_to_b(1)
print(a.b)
a.sub_from_b(2)
print(a.b)
print(a)
a.foo()
a._bar()
a.__nope()
```

### Linear Algebra

Neural network models can be defined using vectors and matrices, i.e. concepts from linear algebra.
The _DeepLearningBook_ dedicates first pages for linear algebra, we will be using a bit of it during this semester, therefore you should know how basic linear operations work. Some of the concepts were covered during your _Algebra and Discrete Mathematics_ course. Read the provided links to review necessary topics (note that there are some questions at the end of each page) and solve the exercises in this notebook.

#### Vectors
- [On vectors](https://www.mathsisfun.com/algebra/vectors.html)
- [On dot product](https://www.mathsisfun.com/algebra/vectors-dot-product.html)

In these labs we use _DeepLearningBook_ notation: simple italic for scalars $x$, lowercase bold italic for vectors $\boldsymbol{x}$ and uppercase bold italics for matrices $\boldsymbol{X}$.
Please, keep this notation in mind.

### NumPy

[Numpy](https://numpy.org/) is a popular Python library for scientific computation. It provides a convenient way of working with vectors and matrices.
Use NumPy functions to solve these exercises.

```python
import numpy as np
```

```python
# Init vectors
a = np.array([0, 1, 3])
b = np.array([2, 4, 1])

# Other useful initializations
c = np.random.random(size=(3,1))
d = np.ones(shape=(1,3),dtype=float)
e = np.zeros(shape=(1,3),dtype=float)
print(c,d,e,sep='\n')
```

    [[0.29856034]
     [0.16234625]
     [0.12413145]]
    [[1. 1. 1.]]
    [[0. 0. 0.]]

__Exercise:__ Determine whether two vectors (e.g. $\boldsymbol{a}$ and $\boldsymbol{b}$) are orthogonal (perpendicular)?

Your TODO (use numpy):

```python
# Orthogonality of vectors
print(np.dot(a,b) == 0)

```

    False

```python
# Perpendicularity (similarity) of vectors
np.cross(a,b)
```

    array([-11,   6,  -2])

__Exercise:__ Compute which vector is longer, $\boldsymbol{a}$ or $\boldsymbol{b}$?

```python
# Length of vectors
l_a = np.linalg.norm(a)
l_b = np.linalg.norm(b)
print(f'a: {l_a}     b: {l_b}')
if l_a > l_b:
    print("a")
else:
    print("b")
```

    a: 3.1622776601683795     b: 4.58257569495584
    b

#### Matrices
- [On matrices](https://www.mathsisfun.com/algebra/matrix-introduction.html)
- [On matrix multiplication](https://www.mathsisfun.com/algebra/matrix-multiplying.html)

__INFO: NumPy array indexing__

```python
# Indexing, i.e. selecting elements from an array / vector / matrix

W = np.array([
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
])

W[1, 1]  # Element from second row of second column
W[0]  # First row
W[[0, 2]]  # First AND third row
W[:, 0]  # First column
W[1, [0, 2]]  # First AND third column of second row

# Access array slices by index
a = np.zeros([10,10])
a[:3] = 1
a[:, :3] = 2
a[:3, :3] = 3
rows = [4,6,7]
cols = [9,3,5]
a[rows, cols] = 4
print(a)

# transposition
a = np.arange(24).reshape(2,3,4)
print(a.shape)
print(a)
a=np.transpose(a, (2,1,0))
# swap 0th and 2nd axes
print(a.shape)
print(a)

```

    [[3. 3. 3. 1. 1. 1. 1. 1. 1. 1.]
     [3. 3. 3. 1. 1. 1. 1. 1. 1. 1.]
     [3. 3. 3. 1. 1. 1. 1. 1. 1. 1.]
     [2. 2. 2. 0. 0. 0. 0. 0. 0. 0.]
     [2. 2. 2. 0. 0. 0. 0. 0. 0. 4.]
     [2. 2. 2. 0. 0. 0. 0. 0. 0. 0.]
     [2. 2. 2. 4. 0. 0. 0. 0. 0. 0.]
     [2. 2. 2. 0. 0. 4. 0. 0. 0. 0.]
     [2. 2. 2. 0. 0. 0. 0. 0. 0. 0.]
     [2. 2. 2. 0. 0. 0. 0. 0. 0. 0.]]
    (2, 3, 4)
    [[[ 0  1  2  3]
      [ 4  5  6  7]
      [ 8  9 10 11]]

     [[12 13 14 15]
      [16 17 18 19]
      [20 21 22 23]]]
    (4, 3, 2)
    [[[ 0 12]
      [ 4 16]
      [ 8 20]]

     [[ 1 13]
      [ 5 17]
      [ 9 21]]

     [[ 2 14]
      [ 6 18]
      [10 22]]

     [[ 3 15]
      [ 7 19]
      [11 23]]]

```python
# Init matrices
# One way:
C = np.array([
    [0, 2, 4],
    [1, 2, 5]
])

d = np.array([1, 7])

# Other ways:
E = np.arange(4).reshape(2, 2)
F = np.ones(shape=(3,3),dtype=int)
G = np.zeros(shape=(3,3),dtype=int)
print(E,F,G,sep='\n\n')
```

    [[0 1]
     [2 3]]

    [[1 1 1]
     [1 1 1]
     [1 1 1]]

    [[0 0 0]
     [0 0 0]
     [0 0 0]]

```python
# There is a difference between a 1-D vector and a column matrix in numpy:
x1 = np.array([1, 2])  # This is a vector
x2 = np.array([  # This is a matrix
    [1],
    [2]
])

# First let's see the dimensions of these two
print(x1.shape)
print(x2.shape)

# Matrix - vector multiplication
# Then we can multiply them with E using np.matmul or @ matrix multiplication operator
print(E @ x1)

# TODO x_2^T \times E
np.transpose(x2)
```

    (2,)
    (2, 1)
    [2 8]

    array([[1, 2]])

### Loading files in python
#### Standard file types - image, csv, json, ...

```python
# imageio (or any other for image reading)
import imageio
import plotly.express as px

img_path = 'files/image.jpeg'
image = imageio.imread(img_path)
# image is a numpy array - you can do whatever you want to do with it as with an array
fig = px.imshow(image[0::50,0::50,:], title=img_path, width=800)
fig.show()

# there are many ways to import csv file
# if you are in doubt - use google - https://www.pythonpool.com/numpy-read-csv/
import csv
csv_path = 'files/iris.csv'
with open(csv_path, 'r') as f:
    # csv data can be considered as lines of a list
    csv_data = list(csv.reader(f, delimiter=','))
for line in range(10):
    print(csv_data[line])

# Other possibility to store data is json
import json
json_path = 'files/titanic.json'
with open(json_path, 'r') as f:
    json_data = json.load(f)
# json is stored as dictionary
print(json_data.keys())

```

    ['sepal.length', 'sepal.width', 'petal.length', 'petal.width', 'variety']
    ['5.1', '3.5', '1.4', '.2', 'Setosa']
    ['4.9', '3', '1.4', '.2', 'Setosa']
    ['4.7', '3.2', '1.3', '.2', 'Setosa']
    ['4.6', '3.1', '1.5', '.2', 'Setosa']
    ['5', '3.6', '1.4', '.2', 'Setosa']
    ['5.4', '3.9', '1.7', '.4', 'Setosa']
    ['4.6', '3.4', '1.4', '.3', 'Setosa']
    ['5', '3.4', '1.5', '.2', 'Setosa']
    ['4.4', '2.9', '1.4', '.2', 'Setosa']
    dict_keys(['metadata', 'data'])

### Derivatives

The final topic to cover are derivatives.
Almost all training algorithms of neural networks in practice are based on calculating the derivatives with respect to (w.r.t.) parameters of the model.
You should know the basics from your _Calculus course_ (Matematická analýza), but just in case, we recommend you to read the following to refresh your memory:

- [On derivatives](https://www.mathsisfun.com/calculus/derivatives-introduction.html)

You will need to use derivatives during this course only at the beginning, so you don't need to learn all the [derivative rules](https://www.mathsisfun.com/calculus/derivatives-rules.html), or you can help yourselves computationally.
However, we definitely need you to have an intuition about what derivatives are and what is their geometric interpretation. In essence, we need you to understand that a derivative tells us what is the slope of the tangent at given point. You should understand what is happening in the gif below:

```python
import plotly.graph_objects as go
x = np.linspace(-3,3,200)
fx = lambda x : x * np.sin(np.power(x, 2))
fdx = lambda x : np.sin(x**2)+ 2 * x**2 * np.cos(x**2)
y = fx(x)
A = -2
yd = fdx(A)
fig = go.Figure(data=go.Scatter(x=x,y=y, name='f(x)=x sin(x^2)+1'))
fig.add_trace(go.Scatter(x=[A],y=[fx(A)], name='A=f(-2),',mode='markers+text',text='A',textposition='middle right'))
line = lambda x,x1,y1: fdx(x1)*(x-x1)+y1
gx = np.linspace(A-0.2,A+0.2,20)
gy = line(gx, A, fx(A))
fig.add_trace(go.Scatter(x=gx,y=gy, line={'dash':'dash'}, name='Gradient in f(-2)\'=sin(x^2)+2x^2cos(x^2)'))
fig.show()

```

License: en:User:Dino, User:Lfahlberg CC BY-SA 3.0, via Wikimedia Commons

Partial derivatives are a concept you might have not head about before.
It is applied when we derive a function with more than one variable.
In such case we can actually derive a function in any direction.

Read the following link:

- [On partial derivatives](https://www.mathsisfun.com/calculus/derivatives-partial.html)

Function of one variable (1D) is a curve, and of two variables (2D) is a topographical relief.
2D function can be visualized by a 3D graph.
In this graph you can pick a point and then ask, what is a slope of the tangent in any direction.
Most commonly you would calculate the slope along axes of both variables (let's say $x,y$): $\frac{df}{dx}$ and $\frac{df}{dy}$.

Vector of derivatives w.r.t. all the parameters is called a _gradient_.
Generally for function $f$ with arbitrary number of parameters $x_1. x_2, ..., x_N = \boldsymbol{x}$, the gradient $\triangledown f$ is defined as:

\begin{equation}
\triangledown f(\boldsymbol{x}) = \frac{df}{d\boldsymbol{x}} = \begin{bmatrix}\frac{df}{dx_1} \\ \frac{df}{dx_2} \\ \vdots \\ \frac{df}{dx_N} \end{bmatrix}
\end{equation}

Gradient is the most important concept from this week's lab. The gradient is a vector quantity that tells us the _direction of steepest ascent_ at each point. This is a very important property, which we will often use in the following weeks. The magnitude of this vector tells us how steep this ascent is, i.e. what is the slope of the tangent in the direction of the gradient.

To compare _derivative_ and _gradient_:

- _Derivative_ is a quantity that tells us, what is the rate of change in given direction.
- _Gradient_ is a quantity that tells us what is the direction of the steepest rate of change, along with the rate of this change.

Observe the difference between these two concepts in two Figures below.
The first plot shows 2D function $f(x,y) = \sin(x) + \cos(y)$.

The next plot shows the values of derivatives of $f(x,y)$ as heatmap. Shows only the rate of change.

The partial derivatives would show arrows in the direction of $x$ or $y$ respectively according to the computed change. (You may plot them on your own)
The last plot shows gradients as blue arrows. Notice that they all point in the direction of the greatest change - as linear combination of partial derivatives.

```python
import plotly.figure_factory as ff

feature_x = np.arange(-np.pi, np.pi, np.pi/16)
feature_y = np.arange(-np.pi, np.pi, np.pi/16)

# Creating 2-D grid of features
[X, Y] = np.meshgrid(feature_x, feature_y)

Z = np.sin(X) + np.cos(Y)

fig = go.Figure(data = go.Heatmap(x = feature_x, y = feature_y, z = Z,))
fig.update_yaxes(scaleanchor='x',scaleratio=1)
fig.update_layout(height=600, width=800)
fig.show()
```

```python
Z = np.cos(X) - np.sin(Y)

fig = go.Figure(data = go.Heatmap(x = feature_x, y = feature_y, z = Z,))
fig.update_yaxes(scaleanchor='x',scaleratio=1)
fig.update_layout(height=600, width=800)
fig.show()

```

```python
U = np.cos(X)
V = np.sin(Y)

fig = ff.create_quiver(X,Y,U,V, scaleratio=1)
fig.update_layout(height=600, width=800)
fig.show()

```


```python

```

## Histograms of Oriented Gradients (HOG)

As we saw with the ORB algorithm, we can use keypoints in images to do keypoint-based matching to detect objects in images. These type of algorithms work great when you want to detect objects that have a lot of consistent internal features that are not affected by the background. For example, these algorithms work well for facial detection because faces have a lot of consistent internal features that don’t get affected by the image background, such as the eyes, nose, and mouth. However, these type of algorithms don’t work so well when attempting to do more general object recognition, say for example, pedestrian detection in images. The reason is that people don’t have consistent internal features, like faces do, because the body shape and style of every person is different (see Fig. 1). This means that every person is going to have a different set of internal features, and so we need something that can more generally describe a person.

Fig. 1. - Pedestrians.

One option is to try to detect pedestrians by their contours instead.  Detecting objects in images by their contours (boundaries) is very challenging because we have to deal with the difficulties brought about by the contrast between the background and the foreground. For example, suppose you wanted to detect a pedestrian in an image that is walking in front of a white building and she is wearing a white coat and black pants (see Fig. 2). We can see in Fig. 2, that since the background of the image is mostly white, the black pants are going to have a very high contrast, but the coat, since it is white as well, is going to have very low contrast. In this case, detecting the edges of pants is going to be easy but detecting the edges of the coat is going to be very difficult. This is where **HOG** comes in. HOG stands for **Histograms of Oriented Gradients** and it was first introduced by Navneet Dalal and Bill Triggs in 2005.

Fig. 2. - High and Low Contrast.

The HOG algorithm works by creating histograms of the distribution of gradient orientations in an image and then normalizing them in a very special way. This special normalization is what makes HOG so effective at detecting the edges of objects even in cases where the contrast is very low. These normalized histograms are put together into a feature vector, known as the HOG descriptor, that can be used to train a machine learning algorithm, such as a Support Vector Machine (SVM), to detect objects in images based on their boundaries (edges). Due to its great success and reliability, HOG has become one of the most widely used algorithms in computer vison for object detection.

In this notebook, you will learn:

* How the HOG algorithm works
* How to use OpenCV to create a HOG descriptor
* How to visualize the HOG descriptor.

# The HOG Algorithm

As its name suggests, the HOG algorithm, is based on creating histograms from the orientation of image gradients. The HOG algorithm is implemented in a series of steps:

1. Given the image of particular object, set a detection window (region of interest) that covers the entire object in the image (see Fig. 3).

2. Calculate the magnitude and direction of the gradient for each individual pixel in the detection window.

3. Divide the detection window into connected *cells* of pixels, with all cells being of the same size (see Fig. 3). The size of the cells is a free parameter and it is usually chosen so as to match the scale of the features that want to be detected. For example, in a 64 x 128 pixel detection window, square cells 6 to 8 pixels wide are suitable for detecting human limbs.

4. Create a Histogram for each cell, by first grouping the gradient directions of all pixels in each cell into a particular number of orientation (angular) bins; and then adding up the gradient magnitudes of the gradients in each angular bin (see Fig. 3). The number of bins in the histogram is a free parameter and it is usually set to 9 angular bins.

5. Group adjacent cells into *blocks* (see Fig. 3). The number of cells in each block is a free parameter and all blocks must be of the same size. The distance between each block (known as the stride) is a free parameter but it is usually set to half the block size, in which case you will get overlapping blocks (*see video below*).  The HOG algorithm has been shown empirically to work better with overlapping blocks.

6. Use the cells contained within each block to normalize the cell histograms in that block (see Fig. 3). If you have overlapping blocks this means that most cells will be normalized with respect to different blocks (*see video below*). Therefore, the same cell may have several different normalizations.

7. Collect all the normalized histograms from all the blocks into a single feature vector called the HOG descriptor.

8. Use the resulting HOG descriptors from many images of the same type of object to train a machine learning algorithm, such as an SVM, to detect those type of objects in images. For example, you could use the HOG descriptors from many images of pedestrians to train an SVM to detect pedestrians in images. The training is done with both positive a negative examples of the object you want detect in the image.

9. Once the SVM has been trained, a sliding window approach is used to try to detect and locate objects in images.  Detecting an object in the image entails finding the part of the image that looks similar to the HOG pattern learned by the SVM.

Fig. 3. - HOG Diagram.

Vid. 1. - HOG Animation.

# Why The HOG Algorithm Works

As we learned above, HOG creates histograms by adding the magnitude of the gradients in particular orientations in localized portions of the image called *cells*.  By doing this we guarantee that stronger gradients will contribute more to the magnitude of their respective angular bin, while the effects of weak and randomly oriented gradients resulting from noise are minimized. In this manner the histograms tell us the dominant gradient orientation of each cell.

### Dealing with contrast

Now, the magnitude of the dominant orientation can vary widely due to variations in local illumination and the contrast between the background and the foreground.

To account for the background-foreground contrast differences, the HOG algorithm tries to detect edges locally.  In order to do this, it defines groups of cells, called **blocks**, and normalizes the histograms using this local group of cells. By normalizing locally, the HOG algorithm can detect the edges in each block very reliably; this is called **block normalization**.

In addition to using block normalization, the HOG algorithm also uses overlapping blocks to increase its performance. By using overlapping blocks, each cell contributes several independent components to the final HOG descriptor, where each component corresponds to a cell being normalized with respect to a different block. This may seem redundant but, it has been shown empirically that by normalizing each cell several times with respect to different local blocks, the performance of the HOG algorithm increases dramatically.

### Loading Images and Importing Resources

The first step in building our HOG descriptor is to load the required packages into Python and to load our image.

We start by using OpenCV to load an image of a triangle tile. Since, the `cv2.imread()` function loads images as BGR we will convert our image to RGB so we can display it with the correct colors. As usual we will convert our BGR image to Gray Scale for analysis.

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

# Set the default figure size
plt.rcParams['figure.figsize'] = [17.0, 7.0]

# Load the image
image = cv2.imread('./images/triangle_tile.jpeg')

# Convert the original image to RGB
original_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

# Convert the original image to gray scale
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

# Print the shape of the original and gray scale images
print('The original image has shape: ', original_image.shape)
print('The gray scale image has shape: ', gray_image.shape)

# Display the images
plt.subplot(121)
plt.imshow(original_image)
plt.title('Original Image')
plt.subplot(122)
plt.imshow(gray_image, cmap='gray')
plt.title('Gray Scale Image')
plt.show()
```

# Creating The HOG Descriptor

We will be using OpenCV’s `HOGDescriptor` class to create the HOG descriptor. The parameters of the HOG descriptor are setup using the `HOGDescriptor()` function. The parameters of the `HOGDescriptor()` function and their default values are given below:

`cv2.HOGDescriptor(win_size = (64, 128),
                  block_size = (16, 16),
                  block_stride = (8, 8),
                  cell_size = (8, 8),
                  nbins = 9,
                  win_sigma = DEFAULT_WIN_SIGMA,
                  threshold_L2hys = 0.2,
                  gamma_correction = true,
                  nlevels = DEFAULT_NLEVELS)`

Parameters:

* **win_size** – *Size*
Size of detection window in pixels (*width, height*). Defines the region of interest. Must be an integer multiple of cell size.

* **block_size** – *Size*
Block size in pixels (*width, height*). Defines how many cells are in each block. Must be an integer multiple of cell size and it must be smaller than the detection window. The smaller the block the finer detail you will get.

* **block_stride** – *Size*
Block stride in pixels (*horizontal, vertical*). It must be an integer multiple of cell size. The `block_stride` defines the distance between adjecent blocks, for example, 8 pixels horizontally and 8 pixels vertically. Longer `block_strides` makes the algorithm run faster (because less blocks are evaluated) but the algorithm may not perform as well.

* **cell_size** – *Size*
Cell size in pixels (*width, height*). Determines the size fo your cell. The smaller the cell the finer detail you will get.

* **nbins** – *int*
Number of bins for the histograms. Determines the number of angular bins used to make the histograms. With more bins you capture more gradient directions. HOG uses unsigned gradients, so the angular bins will have values between 0 and 180 degrees.

* **win_sigma** – *double*
Gaussian smoothing window parameter. The performance of the HOG algorithm can be improved by smoothing the pixels near the edges of the blocks by applying a Gaussian spatial window to each pixel before computing the histograms.

* **threshold_L2hys** – *double*
L2-Hys (Lowe-style clipped L2 norm) normalization method shrinkage. The L2-Hys method is used to normalize the blocks and it consists of an L2-norm followed by clipping and a renormalization. The clipping limits the maximum value of the descriptor vector for each block to have the value of the given threshold (0.2 by default). After the clipping the descriptor vector is renormalized as described in *IJCV*, 60(2):91-110, 2004.

* **gamma_correction** – *bool*
Flag to specify whether the gamma correction preprocessing is required or not. Performing gamma correction slightly increases the performance of the HOG algorithm.

* **nlevels** – *int*
Maximum number of detection window increases.

As we can see, the  `cv2.HOGDescriptor()`function supports a wide range of parameters. The first few arguments (`block_size, block_stride, cell_size`, and `nbins`) are probably the ones you are most likely to change. The other parameters can be safely left at their default values and you will get good results.

In the code below, we will use the `cv2.HOGDescriptor()`function to set the cell size, block size, block stride, and the number of bins for the histograms of the HOG descriptor. We will then use `.compute(image)`method to compute the HOG descriptor (feature vector) for the given `image`.

```python
# Specify the parameters for our HOG descriptor

# Cell Size in pixels (width, height). Must be smaller than the size of the detection window
# and must be chosen so that the resulting Block Size is smaller than the detection window.
cell_size = (6, 6)

# Number of cells per block in each direction (x, y). Must be chosen so that the resulting
# Block Size is smaller than the detection window
num_cells_per_block = (2, 2)

# Block Size in pixels (width, height). Must be an integer multiple of Cell Size.
# The Block Size must be smaller than the detection window
block_size = (num_cells_per_block[0] * cell_size[0],
              num_cells_per_block[1] * cell_size[1])

# Calculate the number of cells that fit in our image in the x and y directions
x_cells = gray_image.shape[1] // cell_size[0]
y_cells = gray_image.shape[0] // cell_size[1]

# Horizontal distance between blocks in units of Cell Size. Must be an integer and it must
# be set such that (x_cells - num_cells_per_block[0]) / h_stride = integer.
h_stride = 1

# Vertical distance between blocks in units of Cell Size. Must be an integer and it must
# be set such that (y_cells - num_cells_per_block[1]) / v_stride = integer.
v_stride = 1

# Block Stride in pixels (horizantal, vertical). Must be an integer multiple of Cell Size
block_stride = (cell_size[0] * h_stride, cell_size[1] * v_stride)

# Number of gradient orientation bins
num_bins = 9

# Specify the size of the detection window (Region of Interest) in pixels (width, height).
# It must be an integer multiple of Cell Size and it must cover the entire image. Because
# the detection window must be an integer multiple of cell size, depending on the size of
# your cells, the resulting detection window might be slightly smaller than the image.
# This is perfectly ok.
win_size = (x_cells * cell_size[0] , y_cells * cell_size[1])

# Print the shape of the gray scale image for reference
print('\nThe gray scale image has shape: ', gray_image.shape)
print()

# Print the parameters of our HOG descriptor
print('HOG Descriptor Parameters:\n')
print('Window Size:', win_size)
print('Cell Size:', cell_size)
print('Block Size:', block_size)
print('Block Stride:', block_stride)
print('Number of Bins:', num_bins)
print()

# Set the parameters of the HOG descriptor using the variables defined above
hog = cv2.HOGDescriptor(win_size, block_size, block_stride, cell_size, num_bins)

# Compute the HOG Descriptor for the gray scale image
hog_descriptor = hog.compute(gray_image)
```

    The gray scale image has shape:  (250, 250)

    HOG Descriptor Parameters:

    Window Size: (246, 246)
    Cell Size: (6, 6)
    Block Size: (12, 12)
    Block Stride: (6, 6)
    Number of Bins: 9

# Number of Elements In The HOG Descriptor

The resulting HOG Descriptor (feature vector), contains the normalized histograms from all cells from all blocks in the detection window concatenated in one long vector. Therefore, the size of the HOG feature vector will be given by the total number of blocks in the detection window, multiplied by the number of cells per block, times the number of orientation bins:

\begin{equation}
\mbox{total_elements} = (\mbox{total_number_of_blocks})\mbox{ } \times \mbox{ } (\mbox{number_cells_per_block})\mbox{ } \times \mbox{ } (\mbox{number_of_bins})
\end{equation}

If we don’t have overlapping blocks (*i.e.* the `block_stride`equals the `block_size`), the total number of blocks can be easily calculated by dividing the size of the detection window by the block size. However, in the general case we have to take into account the fact that we have overlapping blocks. To find the total number of blocks in the general case (*i.e.* for any `block_stride` and `block_size`), we can use the formula given below:

\begin{equation}
\mbox{Total}_i = \left( \frac{\mbox{block_size}_i}{\mbox{block_stride}_i} \right)\left( \frac{\mbox{window_size}_i}{\mbox{block_size}_i} \right) - \left [\left( \frac{\mbox{block_size}_i}{\mbox{block_stride}_i} \right) - 1 \right]; \mbox{  for  } i = x,y
\end{equation}

Where Total$_x$, is the total number of blocks along the width of the detection window, and Total$_y$, is the total number of blocks along the height of the detection window. This formula for Total$_x$ and Total$_y$, takes into account the extra blocks that result from overlapping. After calculating Total$_x$ and Total$_y$, we can get the total number of blocks in the detection window by multiplying Total$_x$  $\times$ Total$_y$. The above formula can be simplified considerably because the `block_size`, `block_stride`, and `window_size`are all defined in terms of the `cell_size`. By making all the appropriate substitutions and cancelations the above formula reduces to:

\begin{equation}
\mbox{Total}_i = \left(\frac{\mbox{cells}_i - \mbox{num_cells_per_block}_i}{N_i}\right) + 1\mbox{  }; \mbox{  for  } i = x,y
\end{equation}

Where cells$_x$ is the total number of cells along the width of the detection window, and cells$_y$, is the total number of cells along the height of the detection window. And $N_x$ is the horizontal block stride in units of `cell_size` and $N_y$ is the vertical block stride in units of `cell_size`.

Let's calculate what the number of elements for the HOG feature vector should be and check that it matches the shape of the HOG Descriptor calculated above.

```python
# Calculate the total number of blocks along the width of the detection window
tot_bx = np.uint32(((x_cells - num_cells_per_block[0]) / h_stride) + 1)

# Calculate the total number of blocks along the height of the detection window
tot_by = np.uint32(((y_cells - num_cells_per_block[1]) / v_stride) + 1)

# Calculate the total number of elements in the feature vector
tot_els = (tot_bx) * (tot_by) * num_cells_per_block[0] * num_cells_per_block[1] * num_bins

# Print the total number of elements the HOG feature vector should have
print('\nThe total number of elements in the HOG Feature Vector should be: ',
      tot_bx, 'x',
      tot_by, 'x',
      num_cells_per_block[0], 'x',
      num_cells_per_block[1], 'x',
      num_bins, '=',
      tot_els)

# Print the shape of the HOG Descriptor to see that it matches the above
print('\nThe HOG Descriptor has shape:', hog_descriptor.shape)
print()
```

    The total number of elements in the HOG Feature Vector should be:  40 x 40 x 2 x 2 x 9 = 57600

    The HOG Descriptor has shape: (57600, 1)

# Visualizing The HOG Descriptor

We can visualize the HOG Descriptor by plotting the histogram associated with each cell as a collection of vectors. To do this, we will plot each bin in the histogram as a single vector whose magnitude is given by the height of the bin and its orientation is given by the angular bin that its associated with. Since any given cell might have multiple histograms associated with it, due to the overlapping blocks, we will choose to average all the histograms for each cell to produce a single histogram for each cell.

OpenCV has no easy way to visualize the HOG Descriptor, so we have to do some manipulation first in order to visualize it. We will start by reshaping the HOG Descriptor in order to make our calculations easier.  We will then compute the average histogram of each cell and finally we will convert the histogram bins into vectors. Once we have the vectors, we plot the corresponding vectors for each cell in an image.

The code below produces an interactive plot so that you can interact with the figure. The figure contains:
* the grayscale image,
* the HOG Descriptor (feature vector),
* a zoomed-in portion of the HOG Descriptor, and
* the histogram of the selected cell.

**You can click anywhere on the gray scale image or the HOG Descriptor image to select a particular cell**. Once you click on either image a *magenta* rectangle will appear showing the cell you selected. The Zoom Window will show you a zoomed in version of the HOG descriptor around the selected cell; and the histogram plot will show you the corresponding histogram for the selected cell. The interactive window also has buttons at the bottom that allow for other functionality, such as panning, and giving you the option to save the figure if desired. The home button returns the figure to its default value.

**NOTE**: If you are running this notebook in the Udacity workspace, there is around a 2 second lag in the interactive plot. This means that if you click in the image to zoom in, it will take about 2 seconds for the plot to refresh.

```python
%matplotlib notebook

import copy
import matplotlib.patches as patches

# Set the default figure size
plt.rcParams['figure.figsize'] = [9.8, 9]

# Reshape the feature vector to [blocks_y, blocks_x, num_cells_per_block_x, num_cells_per_block_y, num_bins].
# The blocks_x and blocks_y will be transposed so that the first index (blocks_y) referes to the row number
# and the second index to the column number. This will be useful later when we plot the feature vector, so
# that the feature vector indexing matches the image indexing.
hog_descriptor_reshaped = hog_descriptor.reshape(tot_bx,
                                                 tot_by,
                                                 num_cells_per_block[0],
                                                 num_cells_per_block[1],
                                                 num_bins).transpose((1, 0, 2, 3, 4))

# Print the shape of the feature vector for reference
print('The feature vector has shape:', hog_descriptor.shape)

# Print the reshaped feature vector
print('The reshaped feature vector has shape:', hog_descriptor_reshaped.shape)

# Create an array that will hold the average gradients for each cell
ave_grad = np.zeros((y_cells, x_cells, num_bins))

# Print the shape of the ave_grad array for reference
print('The average gradient array has shape: ', ave_grad.shape)

# Create an array that will count the number of histograms per cell
hist_counter = np.zeros((y_cells, x_cells, 1))

# Add up all the histograms for each cell and count the number of histograms per cell
for i in range (num_cells_per_block[0]):
    for j in range(num_cells_per_block[1]):
        ave_grad[i:tot_by + i,
                 j:tot_bx + j] += hog_descriptor_reshaped[:, :, i, j, :]

        hist_counter[i:tot_by + i,
                     j:tot_bx + j] += 1

# Calculate the average gradient for each cell
ave_grad /= hist_counter

# Calculate the total number of vectors we have in all the cells.
len_vecs = ave_grad.shape[0] * ave_grad.shape[1] * ave_grad.shape[2]

# Create an array that has num_bins equally spaced between 0 and 180 degress in radians.
deg = np.linspace(0, np.pi, num_bins, endpoint = False)

# Each cell will have a histogram with num_bins. For each cell, plot each bin as a vector (with its magnitude
# equal to the height of the bin in the histogram, and its angle corresponding to the bin in the histogram).
# To do this, create rank 1 arrays that will hold the (x,y)-coordinate of all the vectors in all the cells in the
# image. Also, create the rank 1 arrays that will hold all the (U,V)-components of all the vectors in all the
# cells in the image. Create the arrays that will hold all the vector positons and components.
U = np.zeros((len_vecs))
V = np.zeros((len_vecs))
X = np.zeros((len_vecs))
Y = np.zeros((len_vecs))

# Set the counter to zero
counter = 0

# Use the cosine and sine functions to calculate the vector components (U,V) from their maginitudes. Remember the
# cosine and sine functions take angles in radians. Calculate the vector positions and magnitudes from the
# average gradient array
for i in range(ave_grad.shape[0]):
    for j in range(ave_grad.shape[1]):
        for k in range(ave_grad.shape[2]):
            U[counter] = ave_grad[i,j,k] * np.cos(deg[k])
            V[counter] = ave_grad[i,j,k] * np.sin(deg[k])

            X[counter] = (cell_size[0] / 2) + (cell_size[0] * i)
            Y[counter] = (cell_size[1] / 2) + (cell_size[1] * j)

            counter = counter + 1

# Create the bins in degress to plot our histogram.
angle_axis = np.linspace(0, 180, num_bins, endpoint = False)
angle_axis += ((angle_axis[1] - angle_axis[0]) / 2)

# Create a figure with 4 subplots arranged in 2 x 2
fig, ((a,b),(c,d)) = plt.subplots(2,2)

# Set the title of each subplot
a.set(title = 'Gray Scale Image\n(Click to Zoom)')
b.set(title = 'HOG Descriptor\n(Click to Zoom)')
c.set(title = 'Zoom Window', xlim = (0, 18), ylim = (0, 18), autoscale_on = False)
d.set(title = 'Histogram of Gradients')

# Plot the gray scale image
a.imshow(gray_image, cmap = 'gray')
a.set_aspect(aspect = 1)

# Plot the feature vector (HOG Descriptor)
b.quiver(Y, X, U, V, color = 'white', headwidth = 0, headlength = 0, scale_units = 'inches', scale = 5)
b.invert_yaxis()
b.set_aspect(aspect = 1)
b.set_facecolor('black')

# Define function for interactive zoom
def onpress(event):

    #Unless the left mouse button is pressed do nothing
    if event.button != 1:
        return

    # Only accept clicks for subplots a and b
    if event.inaxes in [a, b]:

        # Get mouse click coordinates
        x, y = event.xdata, event.ydata

        # Select the cell closest to the mouse click coordinates
        cell_num_x = np.uint32(x / cell_size[0])
        cell_num_y = np.uint32(y / cell_size[1])

        # Set the edge coordinates of the rectangle patch
        edgex = x - (x % cell_size[0])
        edgey = y - (y % cell_size[1])

        # Create a rectangle patch that matches the the cell selected above
        rect = patches.Rectangle((edgex, edgey),
                                  cell_size[0], cell_size[1],
                                  linewidth = 1,
                                  edgecolor = 'magenta',
                                  facecolor='none')

        # A single patch can only be used in a single plot. Create copies
        # of the patch to use in the other subplots
        rect2 = copy.copy(rect)
        rect3 = copy.copy(rect)

        # Update all subplots
        a.clear()
        a.set(title = 'Gray Scale Image\n(Click to Zoom)')
        a.imshow(gray_image, cmap = 'gray')
        a.set_aspect(aspect = 1)
        a.add_patch(rect)

        b.clear()
        b.set(title = 'HOG Descriptor\n(Click to Zoom)')
        b.quiver(Y, X, U, V, color = 'white', headwidth = 0, headlength = 0, scale_units = 'inches', scale = 5)
        b.invert_yaxis()
        b.set_aspect(aspect = 1)
        b.set_facecolor('black')
        b.add_patch(rect2)

        c.clear()
        c.set(title = 'Zoom Window')
        c.quiver(Y, X, U, V, color = 'white', headwidth = 0, headlength = 0, scale_units = 'inches', scale = 1)
        c.set_xlim(edgex - cell_size[0], edgex + (2 * cell_size[0]))
        c.set_ylim(edgey - cell_size[1], edgey + (2 * cell_size[1]))
        c.invert_yaxis()
        c.set_aspect(aspect = 1)
        c.set_facecolor('black')
        c.add_patch(rect3)

        d.clear()
        d.set(title = 'Histogram of Gradients')
        d.grid()
        d.set_xlim(0, 180)
        d.set_xticks(angle_axis)
        d.set_xlabel('Angle')
        d.bar(angle_axis,
              ave_grad[cell_num_y, cell_num_x, :],
              180 // num_bins,
              align = 'center',
              alpha = 0.5,
              linewidth = 1.2,
              edgecolor = 'k')

        fig.canvas.draw()

# Create a connection between the figure and the mouse click
fig.canvas.mpl_connect('button_press_event', onpress)
plt.show()
```

    The feature vector has shape: (57600, 1)
    The reshaped feature vector has shape: (40, 40, 2, 2, 9)
    The average gradient array has shape:  (41, 41, 9)

    <IPython.core.display.Javascript object>

# Understanding The Histograms

Let's take a look at a couple of snapshots of the above figure to see if the histograms for the selected cell make sense. Let's start looking at a cell that is inside a triangle and not near an edge:

Fig. 4. - Histograms Inside a Triangle.

In this case, since the triangle is nearly all of the same color there shouldn't be any dominant gradient in the selected cell. As we can clearly see in the Zoom Window and the histogram, this is indeed the case. We have many gradients but none of them clearly dominates over the other.

Now let’s take a look at a cell that is near a horizontal edge:

Fig. 5. - Histograms Near a Horizontal Edge.

Remember that edges are areas of an image where the intensity changes abruptly. In these cases, we will have a high intensity gradient in some particular direction. This is exactly what we see in the corresponding histogram and Zoom Window for the selected cell.  In the Zoom Window, we can see that the dominant gradient is pointing up, almost at 90 degrees, since that’s the direction in which there is a sharp change in intensity. Therefore, we should expect to see the 90-degree bin in the histogram to dominate strongly over the others. This is in fact what we see.

Now let’s take a look at a cell that is near a vertical edge:

Fig. 6. - Histograms Near a Vertical Edge.

In this case we expect the dominant gradient in the cell to be horizontal, close to 180 degrees, since that’s the direction in which there is a sharp change in intensity. Therefore, we should expect to see the 170-degree bin in the histogram to dominate strongly over the others. This is what we see in the histogram but we also see that there is another dominant gradient in the cell, namely the one in the 10-degree bin. The reason for this, is because the HOG algorithm is using unsigned gradients, which means 0 degrees and 180 degrees are considered the same. Therefore, when the histograms are being created, angles between 160 and 180 degrees, contribute proportionally to both the 10-degree bin and the 170-degree bin. This results in there being two dominant gradients in the cell near the vertical edge instead of just one.

To conclude let’s take a look at a cell that is near a diagonal edge.

Fig. 7. - Histograms Near a Diagonal Edge.

To understand what we are seeing, let’s first remember that gradients have an *x*-component, and a *y*-component, just like vectors. Therefore, the resulting orientation of a gradient is going to be given by the vector sum of its components. For this reason, on vertical edges the gradients are horizontal, because they only have an x-component, as we saw in Figure 4. While on horizontal edges the gradients are vertical, because they only have a y-component, as we saw in Figure 3. Consequently, on diagonal edges, the gradients are also going to be diagonal because both the *x* and *y* components are non-zero.  Since the diagonal edges in the image are close to 45 degrees, we should expect to see a dominant gradient orientation in the 50-degree bin. This is in fact what we see in the histogram but, just like in Figure 4., we see there are two dominant gradients instead of just one. The reason for this is that when the histograms are being created, angles that are near the boundaries of bins, contribute proportionally to the adjacent bins.  For example, a gradient with an angle of 40 degrees, is right in the middle of the 30-degree and 50-degree bin. Therefore, the magnitude of the gradient is split evenly into the 30-degree and 50-degree bin. This results in there being two dominant gradients in the cell near the diagonal edge instead of just one.

Now that you know how HOG is implemented, in the workspace you will find a notebook named *Examples*. In there, you will be able set your own paramters for the HOG descriptor for various images. Have fun!


Computational Seismology

The Fourier Pseudospectral Method - Acoustic Waves in 2D

Seismo-Live: http://seismo-live.org

##### Authors:
* David Vargas ([@dvargas](https://github.com/davofis))
* Heiner Igel ([@heinerigel](https://github.com/heinerigel))

## Basic Equations
This notebook presents a Fourier Pseudospectral code for solving the 2D acoustic wave equation. Additionally, a solution using finite difference scheme is given for comparison.

The problem of solving the wave equation

\begin{equation}
\partial_t^2 p = c^2 (\partial_{x}^{2}p + \partial_{z}^{2}p) + s
\end{equation}

can be achieved using finite differeces in combination with spectral methods. Here, spatial partial derivatives with respect to $x$ and $z$ are computed via the Fourier method, i.e.

\begin{equation}
\partial_{x}^{2}p + \partial_{z}^{2}p = \mathscr{F}^{-1}[-k_{x}^{2}\mathscr{F}[p]] + \mathscr{F}^{-1}[-k_{z}^{2}\mathscr{F}[p]]
\end{equation}

where $\mathscr{F}$ represents the Fourier transform operator.

As it was the case in previous numerical solutions, we use a standard 3-point finite-difference operator to approximate the time derivatives. Then, the pressure field is extrapolated as

\begin{equation}
\frac{p_{j,k}^{n+1} - 2p_{j,k}^{n} + p_{j,k}^{n-1}}{dt^2}= c_{j,k}^{2} (\partial_{x}^{2}p + \partial_{z}^{2}p)_{j,k} + s_{j,k}^{n}
\end{equation}

```python
# Import all necessary libraries, this is a configuration step for the exercise.
# Please run it before the simulation code!
import numpy as np
import matplotlib.pyplot as plt
from ricker import ricker

# Show the plots in the Notebook
plt.switch_backend("nbagg")
```

### 1. Fourier derivative method

```python
def fourier_derivative_2nd(f, dx):
    # Length of vector f
    nx = np.size(f)
    # Initialize k vector up to Nyquist wavenumber
    kmax = np.pi / dx
    dk = kmax / (nx / 2)
    k = np.arange(float(nx))
    k[: int(nx/2)] = k[: int(nx/2)] * dk
    k[int(nx/2) :] = k[: int(nx/2)] - kmax

    # Fourier derivative
    ff = np.fft.fft(f)
    ff = (1j*k)**2 * ff
    df_num = np.real(np.fft.ifft(ff))
    return df_num
```

### 2. Initialization of setup

```python
# Basic parameters
# ---------------------------------------------------------------
nt    = 600     # number of time steps
nx    = 256     # number of grid points in x
nz    = nx      # number of grid points in z
c     = 343.    # acoustic velocity
eps   = .2      # stability limit
isnap = 600     # snapshot frequency
isx   = int(nx/2)   # source location
isz   = int(nz/2)
f0    = 200.    # Frequency (div by 5)
xmax  = 200
iplot = 20

# initialization of pressure fields
ap = np.zeros((nx, nz), dtype=float)
apnew = np.zeros((nx, nz), dtype=float)
apold = np.zeros((nx, nz), dtype=float)
ad2px = np.zeros((nx, nz), dtype=float)
ad2pz = np.zeros((nx, nz), dtype=float)

sp = np.zeros((nx, nz), dtype= float)
spnew = np.zeros((nx, nz), dtype=float)
spold = np.zeros((nx, nz), dtype=float)
sd2px = np.zeros((nx, nz), dtype=float)
sd2pz = np.zeros((nx, nz), dtype=float);

sp_sec = -np.abs(sp[1:int(nx/2), 1:int(nz/2)])
ap_sec = -np.abs(ap[int(nx/2):nx, 1:int(nz/2)].T)

dx = xmax/(nx-1)  # calculate space increment
x  = np.arange(0, nx)*dx # initialize space coordinates
z  = np.arange(0, nx)*dx # initialize space coordinates
dt = eps*dx/c # calculate tim step from stability criterion
```

### 3. Source Initialization

```python
# source time function
# ---------------------------------------------------------------
t   = np.arange(1, nt+1)*dt  # initialize time axis
T0  = 1./f0
tmp = ricker(dt, T0)
tmp = np.diff(tmp)
src = np.zeros(nt)
src[0:np.size(tmp)] = tmp
lam = c*T0

# spatial source function
# ---------------------------------------------------------------
sg = np.zeros((nx, nz), dtype= float)
sigma = 1.5*dx
x0 = x[isx-1]
z0 = z[isz-1]
for i in range(nx):
    for j in range(nz):
        sg[i,j] = np.exp(-1/sigma**2 * ((x[i]-x0)**2 +(z[j]-z0)**2))

sg = sg/np.amax(sg)
```

### 4. Time Extrapolation
The final solution for our 2D acoustic wave problem after taking into account the finite differences time extrapolation can be written as

\begin{equation}
p_{j,k}^{n+1} = dt^2c_{j,k}^{2} (\partial_{x}^{2}p + \partial_{z}^{2}p)_{j,k} + dt^2s_{j,k}^{n} + 2p_{j,k}^{n} - p_{j,k}^{n-1}
\end{equation}

In order to compare the above numerical solution, we implement a 5-point finite difference operator to compute spatial derivatives

\begin{equation}
\partial_x^2 p(x,t) = \frac{-p(x+2\mathrm{d}x,t) + 16p(x+\mathrm{d}x,t) - 30p(x,t) + 16p(x-\mathrm{d}x,t) - p(x-2\mathrm{d}x,t)}{12\mathrm{d}x^2}
\end{equation}

temporal derivative is done with a 3-point finite difference operator.

#### Numerical dispersion and anysotropy

One of the most significant characteristics of the fourier method is the low numerical dispersion in comparison with the finite difference method. The snapshots displayed below for both solutions allow us to brifly comment two significant observations:

1) There is strong anisotropic dispersion behaviour visible for the finite-difference solution. The most accurate direction occur at $\theta = \pi/4$

2) The Fourier solution do not exhibit significant dispersion, but the most importantly, it does not seem to be directionally dependent. In other words the error is isotropic.

```python
# Initialize animated plot
# ---------------------------------------------------------------
fig = plt.figure(figsize=(12,6))
ax1 = fig.add_subplot(1,2,1)
ax2 = fig.add_subplot(1,2,2)
line1 = ax1.imshow(sp_sec, interpolation="bicubic", cmap=plt.cm.RdBu)
line2 = ax2.imshow(ap_sec, interpolation="bicubic", cmap=plt.cm.RdBu)
ax1.set_title('Fourier Method', size=14)
ax2.set_title('Finite-Difference Method', size=14)

plt.ion()   # set interective mode
plt.show()

# ---------------------------------------------------------------
# Time extrapolation
# ---------------------------------------------------------------
for it in range(nt):
    # ----------------------------------------
    # Fourier Pseudospectral Method
    # ----------------------------------------
    # 2nd space derivative
    for j in np.arange(nz):
        sd2px[:,j] = fourier_derivative_2nd(sp[:,j].T, dx)
    for i in np.arange(nx):
        sd2pz[i,:] = fourier_derivative_2nd(sp[i,:], dx)

    # Time Extrapolation
    spnew = 2*sp - spold + c**2 * dt**2 * (sd2px + sd2pz)
    spnew = spnew + sg*src[it]*dt**2 # Add sources
    spold, sp = sp, spnew # Time levels

    # ----------------------------------------
    # Finite Differences Method 5pt
    # ----------------------------------------
    for i in range(2, nz-2):
        ad2px[i,:] = (-1./12*ap[i+2,:] + 4./3*ap[i+1,:] - 5./2*ap[i,:] \
                      + 4./3*ap[i-1,:] - 1./12*ap[i-2,:])/dx**2 # Space derivative
    for i in range(2, nx-2):
        ad2pz[:,i] = (-1./12*ap[:,i+2] + 4./3*ap[:,i+1] - 5./2*ap[:,i] \
                      + 4./3*ap[:,i-1] - 1./12*ap[:,i-2])/dx**2 # Space derivative

    apnew = 2*ap - apold + dt**2 * c**2 * (ad2px + ad2pz) # Time Extrapolation
    apnew = apnew + sg*src[it]*dt**2 # Add source
    apold, ap = ap, apnew # Time levels

    # Select Sections for plotting
    sp_sec = -np.abs(sp[1:int(nx/2), 1:int(nz/2)])
    ap_sec = -np.abs(ap[int(nx/2):nx, 1:int(nz/2)].T)

    # --------------------------------------
    # Animation plot. Display solution
    # --------------------------------------
    if not it % iplot:
        # Display Solution
        # --------------------------------------
        line1 = ax1.imshow(sp_sec, interpolation="bicubic", cmap=plt.cm.RdBu)
        line2 = ax2.imshow(ap_sec, interpolation="bicubic", cmap=plt.cm.RdBu)
        plt.gcf().canvas.draw()

```

```python

```