(EIGVALEIGVEC)=

# 2.2 Eigenvalores y eigenvectores

```{admonition} Notas para contenedor de docker:

Comando de docker para ejecución de la nota de forma local:

nota: cambiar `<ruta a mi directorio>` por la ruta de directorio que se desea mapear a `/datos` dentro del contenedor de docker y `<versión imagen de docker>` por la versión más actualizada que se presenta en la documentación.

`docker run --rm -v <ruta a mi directorio>:/datos --name jupyterlab_optimizacion -p 8888:8888 -d palmoreck/jupyterlab_optimizacion:<versión imagen de docker>`

password para jupyterlab: `qwerty`

Detener el contenedor de docker:

`docker stop jupyterlab_optimizacion`

Documentación de la imagen de docker `palmoreck/jupyterlab_optimizacion:<versión imagen de docker>` en [liga](https://github.com/palmoreck/dockerfiles/tree/master/jupyterlab/optimizacion).

```

---

Nota generada a partir de [liga](https://www.dropbox.com/s/s4ch0ww1687pl76/3.2.2.Factorizaciones_matriciales_SVD_Cholesky_QR.pdf?dl=0).

```{admonition} Al final de esta nota el y la lectora:
:class: tip

* Aprenderá las definiciones más relevantes en el tema de eigenvalores y eigenvectores para su uso en el desarrollo de algoritmos en el análisis numérico en la resolución de problemas del álgebra lineal numérica. En específico las definiciones de: diagonalizable o *non defective* y similitud son muy importantes.

* Comprenderá el significado geométrico de calcular los eigenvalores y eigenvectores de una matriz simétrica para una forma cuadrática que define a una elipse.

* Aprenderá cuáles problemas en el cálculo de eigenvalores y eigenvectores de una matriz son bien y mal condicionados.

```

En esta nota **asumimos** que $A \in \mathbb{R}^{n \times n}$.

## Eigenvalor (valor propio o característico)

```{admonition} Definición

El número $\lambda$ (real o complejo) se denomina *eigenvalor* de A si existe $v \in \mathbb{C}^n - \{0\}$ tal que $Av = \lambda v$. El vector $v$ se nombra eigenvector (vector propio o característico) de $A$ correspondiente al eigenvalor $\lambda$.
```

```{admonition} Observación
:class: tip

Observa que si $Av=\lambda v$ y $v \in \mathbb{C}^n-\{0\}$ entonces la matriz $A-\lambda I_n$ es singular por lo que su determinante es cero.

```

```{admonition} Comentarios

* Una matriz con componentes reales puede tener eigenvalores y eigenvectores con valores en $\mathbb{C}$ o $\mathbb{C}^n$ respectivamente.
* El conjunto de eigenvalores se le nombra **espectro de una matriz** y se denota como:

$$\lambda(A) = \{ \lambda | \det(A-\lambda I_n) = 0\}.$$

* El polinomio

$$p(z) = \det(A-zI_n) = (-1)^nz^n + a_{n-1}z^{n-1}+ \dots + a_1z + a_0$$

se le nombra **polinomio característico asociado a $A$** y sus raíces o ceros son los eigenvalores de $A$.

* La multiplicación de $A$ por un eigenvector es un reescalamiento y posible cambio de dirección del eigenvector.
* Si consideramos que nuestros espacios vectoriales se definen sobre $\mathbb{C}$ entonces siempre podemos asegurar que $A$ tiene un eigenvalor con eigenvector asociado. En este caso $A$ tiene $n$ eigenvalores y pueden o no repetirse.

* Se puede probar que el determinante de $A$: $\det(A) = \displaystyle \prod_{i=1}^n \lambda_i$ y la traza de $A$: $tr(A) = \displaystyle \sum_{i=1}^n \lambda_i$.
```

### Ejemplo

```python
import numpy as np
```

```python
np.set_printoptions(precision=3, suppress=True)
```

```python
A=np.array([[10,-18],[6,-11]])
```

```python
print(A)
```

    [[ 10 -18]
     [  6 -11]]

**En *NumPy* con el módulo [numpy.linalg.eig](https://docs.scipy.org/doc/numpy/reference/generated/numpy.linalg.eig.html) podemos obtener eigenvalores y eigenvectores**

```python
evalue, evector = np.linalg.eig(A)
```

```python
print('eigenvalores:')
print(evalue)
print('eigenvectores:')
print(evector)
```

    eigenvalores:
    [ 1. -2.]
    eigenvectores:
    [[0.894 0.832]
     [0.447 0.555]]

```{margin}

$Av_1 = \lambda_1 v_1$.
```

```python
print('matriz * eigenvector:')
print(A@evector[:,0])
print('eigenvalor * eigenvector:')
print(evalue[0]*evector[:,0])
```

    matriz * eigenvector:
    [0.894 0.447]
    eigenvalor * eigenvector:
    [0.894 0.447]

```{margin}

$Av_2 = \lambda_2 v_2$.
```

```python
print('matriz * eigenvector:')
print(A@evector[:,1])
print('eigenvalor * eigenvector:')
print(evalue[1]*evector[:,1])
```

    matriz * eigenvector:
    [-1.664 -1.109]
    eigenvalor * eigenvector:
    [-1.664 -1.109]

### Ejemplo

Si $v$ es un eigenvector entonces $cv$ es eigenvector donde: $c$ es una constante distinta de cero.

```python
const = -2
const_evector = const*evector[:,0]
print(const_evector)
```

    [-1.789 -0.894]

```{margin}

$cv$ es un eigenvector con eigenvalor asociado $c\lambda$ pues $A(cv) = \lambda(cv)$ se satisface si $Av = \lambda v$ y $c \neq 0$.

```

```python
print('matriz * (constante * eigenvector):')
print(A@const_evector)
print('eigenvalor * (constante * eigenvector):')
print(evalue[0]*const_evector)
```

    matriz * (constante * eigenvector):
    [-1.789 -0.894]
    eigenvalor * (constante * eigenvector):
    [-1.789 -0.894]

### Ejemplo

Una matriz con entradas reales puede tener eigenvalores y eigenvectores complejos:

```python
A=np.array([[3,-5],[1,-1]])
```

```python
print(A)
```

    [[ 3 -5]
     [ 1 -1]]

```python
evalue, evector = np.linalg.eig(A)
```

```{margin}

Para $A \in \mathbb{R}^{n \times n}$ se tiene: $\lambda \in \mathbb{C}$ si y sólo si $\bar{\lambda} \in \mathbb{C}$ con $\bar{\lambda}$ el conjugado de $\lambda$.

```

```python
print('eigenvalores:')
print(evalue)
print('eigenvectores:')
print(evector)
```

    eigenvalores:
    [1.+1.j 1.-1.j]
    eigenvectores:
    [[0.913+0.j    0.913-0.j   ]
     [0.365-0.183j 0.365+0.183j]]

```{admonition} Observación
:class: tip

En el ejemplo anterior cada eigenvalor tiene una multiplicidad simple y la multiplicidad geométrica de cada eigenvalor es $1$.

```

### Ejemplo

Los eigenvalores de una matriz diagonal son iguales a su diagonal y sus eigenvectores son los vectores canónicos $e_1, e_2, \dots e_n$.

```python
A = np.diag([2, 2, 2, 2])
```

```python
print(A)
```

    [[2 0 0 0]
     [0 2 0 0]
     [0 0 2 0]
     [0 0 0 2]]

```python
evalue, evector = np.linalg.eig(A)
```

```python
print('eigenvalores:')
print(evalue)
print('eigenvectores:')
print(evector)
```

    eigenvalores:
    [2. 2. 2. 2.]
    eigenvectores:
    [[1. 0. 0. 0.]
     [0. 1. 0. 0.]
     [0. 0. 1. 0.]
     [0. 0. 0. 1.]]

```{admonition} Definición

La **multiplicidad algebraica** de un eigenvalor es su multiplicidad considerado como raíz/cero del polinomio característico $p(z)$. Si no se repite entonces tal eigenvalor se le nombra de multiplicidad **simple**.

La **multiplicidad geométrica** de un eigenvalor es el número máximo de eigenvectores linealmente independientes asociados a éste.

```

### Ejemplo

Los eigenvalores de una matriz triangular son iguales a su diagonal.

```python
A=np.array([[10,0, -1],
            [6,10, 10],
            [3, 4, 11.0]])
A = np.triu(A)
```

```python
print(A)
```

    [[10.  0. -1.]
     [ 0. 10. 10.]
     [ 0.  0. 11.]]

```python
evalue, evector = np.linalg.eig(A)
```

```{margin}

Observa que el eigenvalor igual a $10$ está repetido dos veces (multiplicidad algebraica igual a $2$) y se tienen dos eigenvectores linealmente independientes asociados a éste (multiplicidad geométrica igual a $2$).
```

```python
print('eigenvalores:')
print(evalue)
print('eigenvectores:')
print(evector)
```

    eigenvalores:
    [10. 10. 11.]
    eigenvectores:
    [[ 1.     0.    -0.099]
     [ 0.     1.     0.99 ]
     [ 0.     0.     0.099]]

**Otro ejemplo:**

```python
A=np.array([[10,18, -1],
            [6,10, 10],
            [3, 4, 11.0]])
A = np.triu(A)
```

```python
print(A)
```

    [[10. 18. -1.]
     [ 0. 10. 10.]
     [ 0.  0. 11.]]

```python
evalue, evector = np.linalg.eig(A)
```

```{margin}

Observa que en este ejemplo el eigenvalor $10$ está repetido dos veces (multiplicidad algebraica es igual a $2$) y sus eigenvectores asociados son linealmente dependientes (multiplicidad geométrica es igual a $1$).
```

```python
print('eigenvalores:')
print(evalue)
print('eigenvectores:')
print(evector)
```

    eigenvalores:
    [10. 10. 11.]
    eigenvectores:
    [[ 1.    -1.     0.998]
     [ 0.     0.     0.056]
     [ 0.     0.     0.006]]

### Ejemplo

Un eigenvalor puede estar repetido y tener un sólo eigenvector linealmente independiente:

```python
A = np.array([[2, 1, 0],
              [0, 2, 1],
              [0, 0, 2]])
```

```python
evalue, evector = np.linalg.eig(A)
```

```{margin}

Observa que en este ejemplo el eigenvalor $2$ está repetido tres veces (multiplicidad algebraica es igual a $3$) y sus eigenvectores asociados son linealmente dependientes (multiplicidad geométrica es igual a $1$).
```

```python
print('eigenvalores:')
print(evalue)
print('eigenvectores:')
print(evector)
```

    eigenvalores:
    [2. 2. 2.]
    eigenvectores:
    [[ 1. -1.  1.]
     [ 0.  0. -0.]
     [ 0.  0.  0.]]

```{admonition} Definición

Si $(\lambda, v)$ es una pareja de eigenvalor-eigenvector de $A$ tales que $Av = \lambda v$ entonces $v$ se le nombra eigenvector derecho. Si $(\lambda, v)$ es una pareja de eigenvalor-eigenvector de $A^T$ tales que $A^Tv = \lambda v$ (que es equivalente a $v^TA=\lambda v^T$) entonces $v$ se le nombra eigenvector izquierdo.
```

```{admonition} Observaciones
:class: tip

* En todos los ejemplos anteriores se calcularon eigenvectores derechos.

* Los eigenvectores izquierdos y derechos para una matriz simétrica son iguales.

```

(DIAGONALIZABLE)=

## $A$ diagonalizable

```{admonition} Definición

Si $A$ tiene $n$ eigenvectores linealmente independientes entonces $A$ se nombra diagonalizable o *non defective*. En este caso si $x_1, x_2, \dots, x_n$ son eigenvectores de $A$ con $Ax_i = \lambda_i x_i$ para $i=1,\dots,n$ entonces la igualdad anterior se escribe en ecuación matricial como:

$$AX = X \Lambda$$

o bien:

$$A = X \Lambda X^{-1}$$

donde: $X$ tiene por columnas los eigenvectores de $A$ y $\Lambda$ tiene en su diagonal los eigenvalores de $A$.

A la descomposición anterior $A = X \Lambda X^{-1}$ para $A$ diagonalizable o *non defective* se le nombra ***eigen decomposition***.
```

```{admonition} Observación
:class: tip

* Si $A = X \Lambda X^{-1}$ entonces $X^{-1}A = \Lambda X^{-1}$ y los renglones de $X^{-1}$ (o equivalentemente las columnas de $X^{-T}$) son eigenvectores izquierdos.

* Si $A = X \Lambda X^{-1}$ y $b = Ax = (X \Lambda X^{-1}) x$ entonces:

$$\tilde{b} = X^{-1}b = X^{-1}  (Ax) = X^{-1} (X \Lambda X^{-1}) x = \Lambda X^{-1}x = \Lambda \tilde{x}.$$

Lo anterior indica que el producto matricial $Ax$ para $A$ diagonalizable es equivalente a multiplicar una matriz diagonal por un vector denotado como $\tilde{x}$ que contiene los coeficientes de la combinación lineal de las columnas de $X$ para el vector $x$ . El resultado de tal multiplicación es un vector denotado como $\tilde{b}$ que también contiene los coeficientes de la combinación lineal de las columnas de $X$ para el vector $b$. En resúmen, si $A$ es diagonalizable o *non defective* la multiplicación $Ax$ es equivalente a la multiplicación por una matriz diagonal $\Lambda \tilde{x}$ (salvo un cambio de bases, ver [Change of basis](https://en.wikipedia.org/wiki/Change_of_basis)).

* Si una matriz $A$ tiene eigenvalores distintos entonces es diagonalizable y más general: si $A$ tiene una multiplicidad geométrica igual a su multiplicidad algebraica de cada eigenvalor entonces es diagonalizable.

```

### Ejemplo

La matriz:

$$A = \left[
\begin{array}{ccc}
1 & -4 & -4\\
8 & -11 & -8\\
-8 & 8 & 5
\end{array}
\right]
$$

es diagonalizable.

```python
A = np.array([[1, -4, -4],
              [8, -11, -8],
              [-8, 8, 5.0]])
```

```python
print(A)
```

    [[  1.  -4.  -4.]
     [  8. -11.  -8.]
     [ -8.   8.   5.]]

```python
evalue, evector = np.linalg.eig(A)
```

```python
print('eigenvalores:')
print(evalue)
```

    eigenvalores:
    [ 1. -3. -3.]

```{margin}

Se verifica que los eigenvectores de este ejemplo es un conjunto linealmente independiente por lo que $A=X\Lambda X^{-1}$.

```

```python
print('eigenvectores:')
print(evector)
```

    eigenvectores:
    [[ 0.333 -0.474 -0.326]
     [ 0.667  0.339 -0.811]
     [-0.667 -0.813  0.485]]

```python
X = evector
Lambda = np.diag(evalue)
```

```{margin}

Observa que si $Z$ es desconocida y $X^T Z^T = \Lambda$ entonces $Z^T = X^{-T} \Lambda$ y por tanto $XZ =X\Lambda X^{-1}$.
```

```python
print(X@np.linalg.solve(X.T, Lambda).T)
```

    [[  1.  -4.  -4.]
     [  8. -11.  -8.]
     [ -8.   8.   5.]]

```python
print(A)
```

    [[  1.  -4.  -4.]
     [  8. -11.  -8.]
     [ -8.   8.   5.]]

$A$ es diagonalizable pues: $X^{-1} A X = \Lambda$

```{margin}

Observa que si $Z$ es desconocida y $XZ = A$ entonces $Z = X^{-1}A$ y por tanto $ZX = X^{-1} A X$.
```

```python
print(np.linalg.solve(X, A)@X)
```

    [[ 1.  0. -0.]
     [ 0. -3. -0.]
     [ 0.  0. -3.]]

```python
print(Lambda)
```

    [[ 1.  0.  0.]
     [ 0. -3.  0.]
     [ 0.  0. -3.]]

```{admonition} Observación
:class: tip

Observa que **no necesariamente** $X$ en la *eigen decomposition* es una matriz ortogonal.

```

```{margin}

Aquí se toma $X[1:3,1]$ como la primera columna de $X$ y se satisface $X[1:3,1]^TX[1:3,1] = 1$ en este ejemplo pero en general esto no se cumple.

```

```python
X[:,0].dot(X[:,0])
```

    0.9999999999999997

```{margin}

$X[1:3,1]^TX[1:3,2] \neq 0$ por lo que la primera y segunda columna de $X$ no son ortogonales.

```

```python
X[:,0].dot(X[:,1])
```

    0.6098650780191467

**Eigenvectores derechos:**

```{margin}

`x_1` es la primer columna de $X$: $X[1:3, 1]$ y `lambda_1` el eigenvalor asociado.
```

```python
x_1 = X[:,0]
lambda_1 = Lambda[0,0]
```

```python
print(A@x_1)
```

    [ 0.333  0.667 -0.667]

```{margin}

$Ax_1 = \lambda_1 x_1$.
```

```python
print(lambda_1*x_1)
```

    [ 0.333  0.667 -0.667]

```{margin}

`x_2` es la segunda columna de $X$: $X[1:3, 2]$ y `lambda_2` el eigenvalor asociado.
```

```python
x_2 = X[:,1]
lambda_2 = Lambda[1,1]
```

```python
print(A@x_2)
```

    [ 1.422 -1.017  2.438]

```{margin}

$Ax_2 = \lambda_2 x_2$.
```

```python
print(lambda_2*x_2)
```

    [ 1.422 -1.017  2.438]

**Eigenvectores izquierdos:**

```{admonition} Observación
:class: tip

Para los eigenvectores izquierdos se deben tomar los renglones de $X^{-1}$ (o equivalentemente las columnas de $X^{-T}$) sin embargo no se utiliza el método [inv](https://numpy.org/doc/stable/reference/generated/numpy.linalg.inv.html) de *NumPy* pues es más costoso computacionalmente y amplifica los errores por redondeo. En su lugar se utiliza el método [solve](https://numpy.org/doc/stable/reference/generated/numpy.linalg.solve.html) y se resuelve el sistema: $X^{-T} z  = e_i$ para $e_i$ $i$-ésimo vector canónico.

```

```python
e1 = np.zeros((X.shape[0],1))
```

```python
e1[0] = 1
```

```python
print(e1)
```

    [[1.]
     [0.]
     [0.]]

```{margin}

`x_inv_1` es el primer renglón de $X^{-1}$: $X^{-1}[1, 1:3]$.
```

```python
x_inv_1 = np.linalg.solve(X.T, e1)
```

```python
print(x_inv_1)
```

    [[ 3.]
     [-3.]
     [-3.]]

```python
print(A.T@x_inv_1)
```

    [[ 3.]
     [-3.]
     [-3.]]

```{margin}

$A^TX^{-T}[1:3,1] = \lambda_1 X^{-T}[1:3,1]$, `lambda_1` el eigenvalor asociado a `x_inv_1`.
```

```python
print(lambda_1*x_inv_1)
```

    [[ 3.]
     [-3.]
     [-3.]]

```python
e2 = np.zeros((X.shape[0],1))
```

```python
e2[1] = 1
```

```{margin}

`x_inv_2` es el segundo renglón de $X^{-1}$: $X^{-1}[2, 1:3]$.
```

```python
x_inv_2 = np.linalg.solve(X.T, e2)
```

```python
print(x_inv_2)
```

    [[-1.318]
     [ 0.337]
     [-0.321]]

```python
print(A.T@x_inv_2)
```

    [[ 3.953]
     [-1.012]
     [ 0.964]]

```{margin}

$A^TX^{-T}[1:3,2] = \lambda_2 X^{-T}[1:3,2]$, `lambda_2` el eigenvalor asociado a `x_inv_2`.
```

```python
print(lambda_2*x_inv_2)
```

    [[ 3.953]
     [-1.012]
     [ 0.964]]

```{admonition} Ejercicio
:class: tip

¿Es la siguiente matriz diagonalizable?

$$A = \left [
\begin{array}{ccc}
-1 & -1 & -2\\
8 & -11 & -8\\
-10 & 11 & 7
\end{array}
\right]
$$

si es así encuentra su *eigen decomposition* y diagonaliza a $A$.
```

(DESCESP)=

### Resultado: $A$ simétrica

Si A es simétrica entonces tiene eigenvalores reales. Aún más: $A$ tiene eigenvectores reales linealmente independientes, forman un conjunto ortonormal y se escribe como un producto de tres matrices nombrado **descomposición espectral o *symmetric eigen decomposition***:

$$A = Q \Lambda Q^T$$

donde: $Q$ es una matriz ortogonal cuyas columnas son eigenvectores de $A$ y $\Lambda$ es una matriz diagonal con eigenvalores de $A$.

```{admonition} Comentarios

* Por lo anterior una matriz simétrica es **ortogonalmente diagonalizable**, ver {ref}`A diagonalizable <DIAGONALIZABLE>`.

* Los eigenvalores de $A$ simétrica se pueden ordenar:

$$\lambda_n(A) \leq \lambda_{n-1}(A) \leq \dots \leq \lambda_1(A)$$

con:

$\lambda_{max}(A) = \lambda_1(A)$, $\lambda_{min}(A) = \lambda_n(A)$.

* Se prueba para $A$ simétrica:

$$\lambda_{max}(A) = \displaystyle \max_{x \neq 0} \frac{x^TAx}{x^Tx}$$

$$\lambda_{min}(A) = \displaystyle \min_{x \neq 0} \frac{x^TAx}{x^Tx}.$$

por lo tanto:

$$\lambda_{min}(A) \leq \frac{x^TAx}{x^Tx} \leq \lambda_{max}(A) \forall x \neq 0.$$

* $||A||_2 = \displaystyle \max\{|\lambda_1(A)|, |\lambda_n(A)|\}$.

* $||A||_F = \left( \displaystyle \sum_{i=1}^n \lambda_i ^2 \right)^{1/2}$.

* Los valores singulares de $A$ son el conjunto $\{|\lambda_1(A)|, \dots, |\lambda_{n-1}(A)|, |\lambda_n(A)|\}$.
```

### Ejemplo

Matriz simétrica y descomposición espectral de la misma:

```python
A=np.array([[5,4,2],[4,5,2],[2,2,2]])
```

```python
print(A)
```

    [[5 4 2]
     [4 5 2]
     [2 2 2]]

```python
evalue, evector = np.linalg.eigh(A)
```

```{margin}

Como $A$ es simétrica sus eigenvalores son reales y sus eigenvectores forman un conjunto linealmente independiente. Por lo anterior $A$ tiene descomposción espectral.
```

```python
print('eigenvalores:')
print(evalue)
print('eigenvectores:')
print(evector)
```

    eigenvalores:
    [ 1.  1. 10.]
    eigenvectores:
    [[ 0.67  -0.327  0.667]
     [-0.732 -0.14   0.667]
     [ 0.125  0.935  0.333]]

```{margin}

$A = Q \Lambda Q^T$
```

```python
print('descomposición espectral:')
Lambda = np.diag(evalue)
Q = evector
print('QLambdaQ^T:')
print(Q@Lambda@Q.T)
print('A:')
print(A)
```

    descomposición espectral:
    QLambdaQ^T:
    [[5. 4. 2.]
     [4. 5. 2.]
     [2. 2. 2.]]
    A:
    [[5 4 2]
     [4 5 2]
     [2 2 2]]

A es diagonalizable pues: $Q^T A Q = \Lambda$

```python
print(Q.T@A@Q)
```

    [[ 1. -0. -0.]
     [ 0.  1. -0.]
     [-0. -0. 10.]]

```python
print(Lambda)
```

    [[ 1.  0.  0.]
     [ 0.  1.  0.]
     [ 0.  0. 10.]]

Ver [numpy.linalg.eigh](https://docs.scipy.org/doc/numpy-1.14.0/reference/generated/numpy.linalg.eigh.html).

## Condición del problema del cálculo de eigenvalores y eigenvectores

La condición del problema del cálculo de eigenvalores y eigenvectores de una matriz, es la sensibilidad de los mismos ante perturbaciones en la matriz, ver {ref}`Condición de un problema y estabilidad de un algoritmo <CPEA>`. Diferentes eigenvalores o eigenvectores de una matriz no necesariamente son igualmente sensibles a perturbaciones en la matriz.

```{admonition} Observación
:class: tip

La condición del problema del cálculo de eigenvalores y eigenvectores de una matriz **no** es igual a la condición del problema de resolver un sistema de ecuaciones lineales, ver {ref}`Número de condición de una matriz <NCM>`.

```

Se prueba que la condición de un eigenvalor **simple** de una matriz $A$ está dado por $\frac{1}{|y^Tx|}$ con $x$ eigenvector derecho, $y$ eigenvector izquierdo de $A$ ambos asociados al eigenvalor simple y normalizados esto es: $x^Tx = y^Ty=1$.

```{admonition} Comentarios

* Para los casos en que: $\lambda$ eigenvalor de $A$ sea simple, $A$ sea diagonalizable, existen eigenvectores izquierdos y derechos asociados a un eigenvalor de $A$ tales que $y^Tx \neq 0$. En tales casos, el análisis del condicionamiento del problema del cálculo de eigenvalores y eigenvectores es más sencillo de realizar que para matrices no diagonalizables o eigenvalores con multiplicidad algebraica mayor a $1$. En particular, los eigenvalores de una matriz simétrica están muy bien condicionados: las perturbaciones en $A$ únicamente perturban a los eigenvalores en una magnitud medida con la norma de las perturbaciones y no depende de otros factores, por ejemplo del número de condición de $A$.

* La sensibilidad de un eigenvector depende de la sensibilidad de su eigenvalor asociado y de la distancia de tal eigenvalor de otros eigenvalores.

* Los eigenvalores que son "cercanos" o aquellos de multiplicidad mayor a $1$ pueden ser mal condicionados y por lo tanto difíciles de calcularse de forma exacta y precisa en especial si la matriz es defectuosa (no diagonalizable). Puede mejorarse el número de condición si se escala el problema por una matriz diagonal y similar a $A$, ver {ref}`similitud <SIMILITUD>`.
```

(SIMILITUD)=

## Similitud

```{admonition} Definición

Si existe $X \in \mathbb{R}^{n \times n}$ tal que $B = XAX^{-1}$ con $A, B \in \mathbb{R}^{n \times n}$ entonces $A$ y $B$ se nombran similares.

```

```{admonition} Observación
:class: tip

Las matrices que son similares tienen el mismo espectro, de hecho: $Ax = \lambda x$ si y sólo si $By = \lambda y$ para $y=Xx$. Lo anterior quiere decir que los eigenvalores de una matriz son **invariantes** ante cambios de bases o representación en coordenadas distintas.

```

### Ejemplo

Dada la matriz

$$A=
\left [
\begin{array}{cccc}
-1 & -1 & -1 & -1\\
0 & -5 & -16 & -22\\
0 & 3 & 10 & 14\\
4 & 8 & 12 & 14
\end{array}
\right ]
$$

Definir matrices $B_1, B_2$ similares a $A$ a partir de las matrices:

$$
\begin{array}{l}
X_1 =
\left [
\begin{array}{cccc}
2 & -1 & 0 & 0\\
-1 & 2 & -1 & 0\\
0 & -1 & 2 & -1\\
0 & 0 & -1 & 1
\end{array}
\right ],
X_2 = \left [
\begin{array}{cccc}
2 & -1 & 1 & 0\\
-1 & 2 & 0 & 0\\
0 & -1 & 0 & 0\\
0 & 0 & 0 & 1
\end{array}
\right ]
\end{array}
$$

y verificar que los eigenvalores de $A$ son los mismos que los de $B_1, B_2$, esto es, tienen el mismo espectro.

```python
A = np.array([[-1, -1 , -1, -1],
              [0, -5, -16, -22],
              [0, 3, 10, 14],
              [4, 8, 12, 14.0]])
```

```python
X1 = np.array([[2, -1, 0, 0],
              [-1, 2, -1, 0],
              [0, -1, 2, -1],
              [0, 0, -1, 1.0]])
```

$B_1 = X_1^{-1}AX_1$:

```{margin}

Calculamos $B1$ explícitamente para revisar qué forma tiene pero no es necesario.

```

```python
B1 = np.linalg.solve(X1, A)@X1

```

```python
print(B1)
```

    [[ 1.  2. -0.  0.]
     [ 3.  4. -0.  0.]
     [ 0.  0.  5.  6.]
     [ 0.  0.  7.  8.]]

```python
X2 = np.array([[2, -1, 1, 0],
               [-1, 2, 0, 0],
               [0, -1, 0, 0],
               [0, 0, 0, 1.0]])
```

$B_2 = X_2^{-1}AX_2$:

```{margin}

Calculamos $B2$ explícitamente para revisar qué forma tiene pero no es necesario.

```

```python
B2 = np.linalg.solve(X2, A)@X2

```

```python
print(B2)
```

    [[  1.   2.   0.  -6.]
     [  3.   4.   0. -14.]
     [ -0.  -0.  -1.  -3.]
     [  0.   0.   4.  14.]]

**$B1$ y $B2$ son similares a $A$ por tanto tienen los mismos eigenvalores:**

```python
evalue, evector = np.linalg.eig(A)
```

```{margin}

`evalue` son los eigenvalores de $A$.

```

```python
print(evalue)
```

    [13.152  5.372 -0.152 -0.372]

```python
evalue_B1, evector_B1 = np.linalg.eig(B1)
```

```{margin}

`evalue_B1` son los eigenvalores de $B_1$, obsérvese que son iguales a los de $A$ salvo el orden.

```

```python
print(evalue_B1)
```

    [-0.372  5.372 -0.152 13.152]

```python
evalue_B2, evector_B2 = np.linalg.eig(B2)
```

```{margin}

`evalue_B2` son los eigenvalores de $B_2$, obsérvese que son iguales a los de $A$ salvo el orden.

```

```python
print(evalue_B2)
```

    [-0.372  5.372 13.152 -0.152]

Los eigenvectores **no son los mismos** pero pueden obtenerse vía multiplicación de matrices:

```{margin}

Elegimos un eigenvalor de $A$.

```

```python
print(evalue[1])
```

    5.372281323269014

```{margin}

Y elegimos el mismo eigenvalor en el *array* `evalue_B1` para $B_1$ que para este ejemplo corresponde al índice $1$ (el mismo que en `evalue` pero podría haber sido otro).

```

```python
print(evalue_B1[1])
```

    5.3722813232690125

```{margin}

Su correspondiente eigenvector en el índice $1$ del *array* `evector_B1`.

```

```python
print(evector_B1[:,1])
```

    [-0.416 -0.909  0.     0.   ]

**$X^{-1}x$ es eigenvector de $B_1$ para $x$ eigenvector de $A$**:

```{margin}

`evector[:,1]` es el eigenvector de $A$ correspondiente al eigenvalor `evalue[1]`. En esta celda se hace el producto $X_1^{-1}x$ y `evector[:,1]` representa a $x$.

```

```python
X1_inv_evector = np.linalg.solve(X1, evector[:,1])
```

```python
print(X1_inv_evector)
```

    [ 0.249  0.543 -0.    -0.   ]

```python
print(B1@(X1_inv_evector))
```

    [ 1.335  2.919 -0.    -0.   ]

```{margin}

Se verifica que $B1(X_1^{-1}x) = \lambda (X_1^{-1}x)$ con $\lambda$ igual al valor `evalue_B1[1]`.

```

```python
print(evalue_B1[1]*(X1_inv_evector))
```

    [ 1.335  2.919 -0.    -0.   ]

```{admonition} Observación
:class: tip

Obsérvese que son los mismos eigenvectores salvo una constante distinta de cero.

```

```python
print(evector_B1)
```

    [[-0.825 -0.416  0.     0.   ]
     [ 0.566 -0.909 -0.     0.   ]
     [ 0.     0.    -0.759 -0.593]
     [ 0.     0.     0.651 -0.805]]

```{margin}

El valor `1.33532534` es la primera entrada de `X1_inv_evector` que es $X_1^{-1}x$ y $x$ eigenvector de $A$. El valor `2.91920903` es la segunda entrada de `X_1_inv_evector`. Las entradas restantes son cercanas a cero.
```

```python
print(1.33532534e+00/evector_B1[0,1])
```

    -3.2101207266138467

```python
print(2.91920903e+00/evector_B1[1,1])
```

    -3.2101207350977647

La constante es aproximadamente $-3.21$:

```{margin}

`evector_B1` fue calculado con la función `eig` pero en la siguiente celda se observa que no es necesario si se tiene un eigenvector de $A$.
```

```python
print(evector_B1[:,1]*(-3.21))
```

    [ 1.335  2.919 -0.    -0.   ]

```{margin}

Recuerda que `X_1_inv_evector` es $X_1^{-1}x$ con $x$ eigenvector de $A$ que en este caso se utilizó `evector[:,1]`.

```

```python
print(B1@(X1_inv_evector))
```

    [ 1.335  2.919 -0.    -0.   ]

```{margin}

Se comprueba que $X_1^{-1}x$ es eigenvector de $B$ si $x$ es eigenvector de $A$.
```

```python
print(evalue_B1[1]*(X1_inv_evector))
```

    [ 1.335  2.919 -0.    -0.   ]

Como $A$ tiene eigenvalores distintos entonces es diagonalizable, esto es existen $X_3, \Lambda$ tales que $X_3^{-1} A X_3 = \Lambda$.

```python
X_3 = evector
Lambda = np.diag(evalue)
```

```python
print(A)
```

    [[ -1.  -1.  -1.  -1.]
     [  0.  -5. -16. -22.]
     [  0.   3.  10.  14.]
     [  4.   8.  12.  14.]]

```python
print(np.linalg.solve(X_3, A)@X_3)
```

    [[13.152  0.    -0.    -0.   ]
     [ 0.     5.372 -0.    -0.   ]
     [ 0.    -0.    -0.152 -0.   ]
     [-0.     0.    -0.    -0.372]]

```python
print(Lambda)
```

    [[13.152  0.     0.     0.   ]
     [ 0.     5.372  0.     0.   ]
     [ 0.     0.    -0.152  0.   ]
     [ 0.     0.     0.    -0.372]]

```{admonition} Comentario

**$X_1$ diagonaliza a $A$ por bloques, $X_2$ triangulariza a $A$ por bloques y $X_3$ diagonaliza a $A$.** Las tres matrices representan al mismo operador lineal (que es una transformación lineal del espacio vectorial sobre sí mismo) pero en **coordenadas diferentes**. Un aspecto muy **importante** en el álgebra lineal es representar a tal operador lineal en unas coordenadas lo más simple posible. En el ejemplo la matriz $X_3$, que en sus columnas están los eigenvectores de $A$, ayuda a representarlo de forma muy simple.

```

```{admonition} Observación
:class: tip

$X_3$ es una matriz que diagonaliza a $A$ y tiene en sus columnas a eigenvectores de $A$, si el objetivo es diagonalizar a una matriz **no es necesario** resolver un problema de cálculo de eigenvalores-eigenvectores pues cualquier matriz $X$ no singular puede hacer el trabajo. Una opción es considerar una factorización para $A$ simétrica del tipo $LDL^T$ (que tiene un costo computacional bajo para calcularse), la matriz $L$ no es ortogonal y la matriz $D$ tiene los pivotes que se calculan en la eliminación Gaussiana, ver {ref}` Operaciones y transformaciones básicas del Álgebra Lineal Numérica <OTBALN>`.

```

```{admonition} Ejercicio
:class: tip

Considera

$$A=
\left [
\begin{array}{cccc}
-2 & -1 & -5 & 2\\
-9 & 0 & -8 & -2\\
2 & 3 & 11 & 5\\
3 & -5 & 13 & -7
\end{array}
\right ]
$$

Define $X_1$ tal que $X_1^{-1}AX_1$ sea diagonal.
```

### Ejemplo

```python
import sympy
import matplotlib.pyplot as plt
```

```{margin}

Equivalentemente la ecuación $1 = \frac{19}{192}x^2 - \frac{7 \sqrt{3}}{288}xy + \frac{43}{576}y^2$ representa a la misma elipse inclinada.

```

Considérese la siguiente ecuación cuadrática:

$$57x^2 - 14 \sqrt{3} xy + 43 y^2=576$$

Con Geometría Analítica sabemos que tal ecuación representa una elipse inclinada. El desarrollo que continúa mostrará que tal ecuación es equivalente a:

$$\frac{\tilde{x}^2}{16} + \frac{\tilde{y}^2}{9} = 1.$$

la cual representa a la misma elipse pero en los ejes coordenados $\tilde{x}\tilde{y}$ rotados un ángulo $\theta$.

Si:

```python
D = sympy.Matrix([[sympy.Rational(1,16), 0],
                  [0, sympy.Rational(1,9)]])
```

```python
sympy.pprint(D)
```

    ⎡1/16   0 ⎤
    ⎢         ⎥
    ⎣ 0    1/9⎦

entonces el producto

$$\left [ \begin{array}{c}
\tilde{x}\\
\tilde{y}
\end{array}
\right ] ^TD
\left [
\begin{array}{c}
\tilde{x}\\
\tilde{y}
\end{array}
\right ]
$$

es:

```python
x_tilde, y_tilde = sympy.symbols("x_tilde, y_tilde")
x_y_tilde = sympy.Matrix([x_tilde, y_tilde])
```

```python
sympy.pprint((x_y_tilde.T*D*x_y_tilde)[0])
```

           2          2
    x_tilde    y_tilde
    ──────── + ────────
       16         9

```{admonition} Definición

Al producto $x^TAx$ con $A$ simétrica se le nombra forma cuadrática y es un número en $\mathbb{R}$.

```

A partir de la ecuación:

$$\frac{\tilde{x}^2}{16} + \frac{\tilde{y}^2}{9} = 1$$

rotemos al [eje mayor de la elipse](https://en.wikipedia.org/wiki/Semi-major_and_semi-minor_axes) un ángulo de $\theta = \frac{\pi}{3}$ en **sentido contrario a las manecillas del reloj** con una {ref}`transformación de rotación <TROT>` que genera la ecuación matricial:

$$\begin{array}{l}
\left[
\begin{array}{c}
x\\
y
\end{array}
\right ]
=
\left [
\begin{array}{cc}
\cos(\theta) & -\sin(\theta)\\
\sin(\theta) & \cos(\theta)
\end{array}
\right ]
\left[
\begin{array}{c}
\tilde{x}\\
\tilde{y}
\end{array}
\right ]
=
\left [
\begin{array}{cc}
\frac{1}{2} & -\frac{\sqrt{3}}{2}\\
\frac{\sqrt{3}}{2} & \frac{1}{2}
\end{array}
\right ]
\left[
\begin{array}{c}
\tilde{x}\\
\tilde{y}
\end{array}
\right ]
=
Q\left[
\begin{array}{c}
\tilde{x}\\
\tilde{y}
\end{array}
\right ]
\end{array}
$$

donde: $Q$ es la matriz de rotación en sentido contrario a las manecillas del reloj por el ángulo $\theta$.

Esto es:

$$
\begin{eqnarray}
x =\frac{\tilde{x}}{2} - \frac{\tilde{y}\sqrt{3}}{2} \nonumber \\
y =\frac{\tilde{x}\sqrt{3}}{2} + \frac{\tilde{y}}{2} \nonumber
\end{eqnarray}
$$

Despejando $\tilde{x},\tilde{y}$:

$$\begin{array}{l}
\left[
\begin{array}{c}
\tilde{x}\\
\tilde{y}
\end{array}
\right ]
=
\left [
\begin{array}{cc}
\cos(\theta) & \sin(\theta)\\
-\sin(\theta) & \cos(\theta)
\end{array}
\right ]
\left[
\begin{array}{c}
x\\
y
\end{array}
\right ]
=
Q^T\left[
\begin{array}{c}
x\\
y
\end{array}
\right ]
\end{array}
$$

y sustituyendo en $\frac{\tilde{x}^2}{16} + \frac{\tilde{y}^2}{9} = 1$ resulta en la ecuación:

```python
theta = sympy.pi/3
Q = sympy.Matrix([[sympy.cos(theta), -sympy.sin(theta)],
                  [sympy.sin(theta), sympy.cos(theta)]])
x,y = sympy.symbols("x, y")
x_tilde = (Q.T*sympy.Matrix([x,y]))[0]
y_tilde = (Q.T*sympy.Matrix([x,y]))[1]

sympy.pprint((x_tilde**2/16 + y_tilde**2/9).expand()*576) #576 is the least common denominator

```

        2                   2
    57⋅x  - 14⋅√3⋅x⋅y + 43⋅y

```{margin}

Ecuación de una elipse inclinada.

```

$$57x^2 - 14 \sqrt{3} xy + 43 y^2=576$$

Que es equivalente a la forma cuadrática

$$\left [ \begin{array}{c}
x\\
y
\end{array}
\right ]^T A
\left [
\begin{array}{c}
x\\
y
\end{array}
\right ]
$$

```python
x_y = sympy.Matrix([x,y])
A = Q*D*Q.T
sympy.pprint(((x_y.T*A*x_y)[0]).expand()*576)
```

        2                   2
    57⋅x  - 14⋅√3⋅x⋅y + 43⋅y

con $A$ matriz dada por $A=QDQ^T$:

```{margin}

Observa que $A$ es **simétrica**.

```

```python
sympy.pprint(A)
```

    ⎡  19    -7⋅√3 ⎤
    ⎢ ───    ──────⎥
    ⎢ 192     576  ⎥
    ⎢              ⎥
    ⎢-7⋅√3     43  ⎥
    ⎢──────   ───  ⎥
    ⎣ 576     576  ⎦

En este ejemplo la matriz $Q$ de rotación es la matriz que diagonaliza ortogonalmente a $A$ pues: $Q^TAQ = D.$

Para realizar la **gráfica** de la elipse con *NumPy* observar que:

```{margin}

Estas ecuaciones nos indican que la misma elipse se puede representar en diferentes coordenadas. El cambio de coordenadas del vector $(x,y)^T$ (en coordenadas de la base canónica) al vector $(\tilde{x}, \tilde{y})$ (en coordenadas de los eigenvectores de $A$) se realiza con la matriz $Q^T$.

```

```python
sympy.pprint(((x_y.T*A*x_y)[0]).expand())
```

        2                  2
    19⋅x    7⋅√3⋅x⋅y   43⋅y
    ───── - ──────── + ─────
     192      288       576

$$
\begin{eqnarray}
1&=&\frac{19}{192}x^2 - \frac{7 \sqrt{3}}{288}xy + \frac{43}{576}y^2 \nonumber \\
&=& \left [ \begin{array}{c}
x\\
y
\end{array}
\right ]^T A
\left [
\begin{array}{c}
x\\
y
\end{array}
\right ] \nonumber \\
&=& \left [ \begin{array}{c}
x\\
y
\end{array}
\right ]^T QDQ^T \left [
\begin{array}{c}
x\\
y
\end{array}
\right ] \nonumber \\
&=& \left(Q^T \left [ \begin{array}{c}
x\\
y
\end{array}
\right ]\right)^TD\left(Q^T \left [ \begin{array}{c}
x\\
y
\end{array}
\right ]\right) \nonumber \\
&=& \left [ \begin{array}{c}
\tilde{x}\\
\tilde{y}
\end{array}
\right ] ^TD
\left [
\begin{array}{c}
\tilde{x}\\
\tilde{y}
\end{array}
\right ] \nonumber \\
&=& \frac{\tilde{x}^2}{16} + \frac{\tilde{y}^2}{9} \nonumber
\end{eqnarray}
$$

```python
sympy.pprint(Q)
```

    ⎡     -√3 ⎤
    ⎢1/2  ────⎥
    ⎢      2  ⎥
    ⎢         ⎥
    ⎢√3       ⎥
    ⎢──   1/2 ⎥
    ⎣2        ⎦

```python
Q_np = np.array(Q.evalf(), dtype=float)
```

```python
print(Q_np)
```

    [[ 0.5   -0.866]
     [ 0.866  0.5  ]]

```python
A_np = np.array(A.evalf(),dtype = float)
```

```{margin}

Usamos [eig](https://numpy.org/doc/stable/reference/generated/numpy.linalg.eig.html) para el cálculo numérico de eigenvalores, eigenvectores de $A$.

```

```python
evalue_np, evector_np = np.linalg.eig(A_np)
```

```python
print(evector_np)
```

    [[ 0.866  0.5  ]
     [-0.5    0.866]]

**Obsérvese que la función de `eig` nos devuelve ordenados los eigenvalores en forma decreciente.**

```python
print(evalue_np)
```

    [0.111 0.062]

**Para que coincida el orden con la matriz `Q_np` reordenamos las columnas de `evector`:**

```python
P1 = np.array([[0, 1],
             [1, 0.0]])
```

```python
evector_np_permuted = evector_np@P1
```

```python
print(Q_np)
```

    [[ 0.5   -0.866]
     [ 0.866  0.5  ]]

```{margin}

El signo de la segunda columna está intercambiado pero no es un problema para eigenvectores pues son invariantes ante multiplicaciones por escalares distintos de cero.

```

```python
print(evector_np_permuted)
```

    [[ 0.5    0.866]
     [ 0.866 -0.5  ]]

```python
d1_inv=float(sympy.sqrt(D[0,0]))
d2_inv=float(sympy.sqrt(D[1,1]))
```

```python
evector_1_rescaled = 1/d1_inv*evector_np_permuted[:,0]
evector_2_rescaled = 1/d2_inv*evector_np_permuted[:,1]
```

```python
small_value = 1e-4
density=1e-2 + small_value
x=np.arange(-1/d1_inv,1/d1_inv,density)
y1=1/d2_inv*np.sqrt(1-(d1_inv*x)**2)
y2=-1/d2_inv*np.sqrt(1-(d1_inv*x)**2)
#transform
x_y1_hat = np.column_stack((x,y1))
x_y2_hat = np.column_stack((x,y2))
apply_evector_np_permuted = lambda vec : np.transpose(evector_np_permuted@np.transpose(vec))
evector_np_permuted_to_vector_1 = apply_evector_np_permuted(x_y1_hat)
evector_np_permuted_to_vector_2 = apply_evector_np_permuted(x_y2_hat)
fig = plt.figure(figsize=(12, 7))
ax1 = fig.add_subplot(1,2,1)
ax2 = fig.add_subplot(1,2,2)
#first plot
ax1.plot(evector_np_permuted_to_vector_1[:,0],evector_np_permuted_to_vector_1[:,1],'g',
         evector_np_permuted_to_vector_2[:,0],evector_np_permuted_to_vector_2[:,1],'g')
ax1.set_title("$\\frac{19x^2}{192}-\\frac{7\\sqrt{3}xy}{288}+\\frac{43y^2}{576}=1$", fontsize=18)
ax1.set_xlabel("Ejes coordenados típicos")
ax1.axhline(color='r')
ax1.axvline(color='r')
ax1.grid()
#second plot
Evector_1 = np.row_stack((np.zeros(2), evector_1_rescaled))
Evector_2 = np.row_stack((np.zeros(2), evector_2_rescaled))
ax2.plot(evector_np_permuted_to_vector_1[:,0],evector_np_permuted_to_vector_1[:,1],
         color='g', label = "Elipse")
ax2.plot(evector_np_permuted_to_vector_2[:,0],evector_np_permuted_to_vector_2[:,1],
         color='g', label = "_nolegend_")
ax2.plot(Evector_1[:,0], Evector_1[:,1],
         color='b', label = "Eigenvector Q[:,0], define al semieje mayor principal de la elipse")
ax2.plot(-Evector_1[:,0], -Evector_1[:,1],
         color='b', label = "_nolegend_")
ax2.plot(Evector_2[:,0], Evector_2[:,1],
         color='m', label = "Eigenvector Q[:,1],  define al semieje menor principal de la elipse")
ax2.plot(-Evector_2[:,0], -Evector_2[:,1],
         color='m', label = "_nolegend_")
ax2.scatter(evector_np_permuted[0,0],
            evector_np_permuted[1,0], marker = '*', color='b', s=150)
ax2.scatter(Q_np[0,0], Q_np[1,0],
            marker='o', facecolors='none', edgecolors='b',
            s=150)
ax2.scatter(evector_1_rescaled[0], evector_1_rescaled[1],
            marker='o', facecolors='none', edgecolors='b',
            s=150)
ax2.scatter(evector_2_rescaled[0], evector_2_rescaled[1],
            marker='o', facecolors='none', edgecolors='m',
            s=150)
ax2.set_title("$\\frac{\\tilde{x}^2}{16} + \\frac{\\tilde{y}^2}{9}=1$", fontsize=18)
ax2.set_xlabel("Ejes coordenados rotados")
ax2.legend(bbox_to_anchor=(1, 1))
fig.suptitle("Puntos en el plano que cumplen $z^TAz=1$ y $\\tilde{z}^TD\\tilde{z}=1$")
ax2.grid()
plt.show()
```

```{margin}

Recuerda que $A = Q D Q^T$, $A$ es similar a $D$ matriz diagonal y $Q$ es ortogonal.

```

En la gráfica anterior se representa la rotación de los ejes coordenados definidos por los vectores canónicos $e_1, e_2$ y los rotados definidos por los eigenvectores de $A$. Los eigenvectores de $A$ están en las columnas de $Q$. La primera columna de $Q$ define al eje mayor principal de la elipse y la segunda columna al eje menor principal. La longitud de los semiejes están dados respectivamente por la raíz cuadrada de los recíprocos de los eigenvalores de $A$ que en este caso son: $\frac{1}{16}, \frac{1}{9}$, esto es: $4$ y $3$. Ver por ejemplo: [Principal_axis_theorem](https://en.wikipedia.org/wiki/Principal_axis_theorem), [Diagonalizable_matrix](https://en.wikipedia.org/wiki/Diagonalizable_matrix).

```python
print(evector_1_rescaled)
```

    [2.    3.464]

```{margin}

Longitud del eigenvector reescalado asociado al eigenvalor mínimo y representa la longitud del semieje mayor de la elipse.

```

```python
print(np.linalg.norm(evector_1_rescaled))
```

    4.0

```python
print(evector_2_rescaled)
```

    [ 2.598 -1.5  ]

```{margin}

Longitud del eigenvector reescalado asociado al eigenvalor máximo y representa la longitud del semieje menor de la elipse.

```

```python
print(np.linalg.norm(evector_2_rescaled))
```

    3.0000000000000004

```{admonition} Ejercicio
:class: tip

Rotar los ejes coordenados $45^o$ la ecuación de la elipse:

$$13x^2+10xy+13y^2=72$$

para representar tal ecuación alineando los ejes mayor y menor de la elipse a sus eigenvectores. Encontrar las matrices $Q, D$ tales que $A=QDQ^T$ con $Q$ ortogonal y $D$ diagonal.

```

## Algunos algoritmos para calcular eigenvalores y eigenvectores

Dependiendo de las siguientes preguntas es el tipo de algoritmo que se utiliza:

* ¿Se requiere el cómputo de todos los eigenvalores o de sólo algunos?

* ¿Se requiere el cómputo de únicamente los eigenvalores o también de los eigenvectores?

* ¿$A$ tiene entradas reales o complejas?

* ¿$A$ es de dimensión pequeña y es densa o grande y rala?

* ¿$A$ tiene una estructura especial o es una matriz general?

Para la última pregunta a continuación se tiene una tabla que resume las estructuras en las matrices que son relevantes para problemas del cálculo de eigenvalores-eigenvectores:

|Estructura|Definición|
|:---:|:---:|
|Simétrica|$A=A^T$|
|Ortogonal|$A^TA=AA^T=I_n$|
|Normal|$A^TA = AA^T$|

Ver {ref}`Ejemplos de matrices normales <EJMN>`.

(EJMN)=

### Una opción (inestable numéricamente respecto al redondeo): encontrar raíces del polinomio característico...

```{margin}

Como ejemplo que no es posible expresar las raíces o ceros por una fórmula cerrada que involucren a los coeficientes, operaciones aritméticas y raíces $\sqrt[n]{\cdot}$ para polinomios de grado mayor a $4$, considérese las raíces de $x^5 - x^2 + 1 = 0$.

```

Por definición, los eigenvalores de $A \in \mathbb{R}^{n \times n}$ son las raíces o ceros del polinomio característico $p(z)$ por lo que un método es calcularlas vía tal polinomio. Calcular los eigenvalores de matrices por tal método para una $n > 4$ necesariamente requiere que sea un **método iterativo** para matrices con dimensión $n >4$ pues [Abel](https://en.wikipedia.org/wiki/Abel%E2%80%93Ruffini_theorem) probó de forma teórica que las raíces en general no son posibles expresarlas por una fórmula cerrada que involucren los coeficientes, operaciones aritméticas y raíces $\sqrt[n]{\cdot}$ .

```{margin}

Como ejemplo de este enunciado considérese:

$$A=\left[
\begin{array}{cc}
1 & \epsilon\\
\epsilon & 1\\
\end{array}
\right]
$$

cuyos eigenvalores son $1 + \epsilon$, $1 - \epsilon$ con $\epsilon$ menor que $\epsilon_{maq}$. Usando aritmética en el SPF se prueba que las raíces del polinomio característico es $1$ de multiplicidad $2$.
```

Además de lo anterior, en ciertas bases de polinomios, por ejemplo $\{1, x, x^2, \dots, x^n\}$, los coeficientes de los polinomios  numéricamente no están bien determinados por los errores por redondeo y las raíces de los polinomios son muy sensibles a perturbaciones de los coeficientes, esto es, es un **problema mal condicionado**, ver {ref}`condición de un problema y estabilidad de un algoritmo <CPEA>` y [Wilkinson's polynomial](https://en.wikipedia.org/wiki/Wilkinson%27s_polynomial) para un ejemplo.

### Alternativas

Revisaremos en la nota {ref}`Algoritmos y aplicaciones de eigenvalores, eigenvectores de una matriz <AAEVALEVEC>` algunos algoritmos como:

* Método de la potencia y método de la potencia inversa o iteración inversa.

* Iteración por el cociente de Rayleigh.

* Algoritmo QR.

* Método de rotaciones de Jacobi.

---

## Ejemplos de matrices normales

```{sidebar} Descomposición espectral para matrices normales

Las matrices normales generalizan al caso de entradas en $\mathbb{C}$ la diagonalización ortogonal al ser **unitariamente diagonalizables**. $A \in \mathbb{C}^{n \times n}$ es normal si y sólo si $A = U \Lambda U^H$ con $U$ matriz unitaria (generalización de una matriz ortogonal a entradas $\mathbb{C}$), $U^H$ la conjugada transpuesta de $U$ y $\Lambda$ matriz diagonal. Para $A \in \mathbb{R}^{n \times n}$ lo anterior se escribe como: $A$ es simétrica si y sólo si es ortogonalmente diagonalizable: $A = Q \Lambda Q^T$ (ver {ref}`descomposición espectral <DESCESP>`).

```

$$\begin{array}{l}
\left[
\begin{array}{cc}
1 &-2 \\
2 &1
\end{array}
\right],
\left[
\begin{array}{ccc}
1 &2 & 0\\
0 & 1 & 2\\
2 & 0 & 1
\end{array}
\right]
\end{array}
$$

Otro ejemplo:

$$A =
\left[
\begin{array}{ccc}
1 &1 & 0\\
0 & 1 & 1\\
1 & 0 & 1
\end{array}
\right]
$$

```python
A = np.array([[1, 1, 0],
              [0, 1, 1],
              [1, 0, 1.0]])
```

```python
print(A.T@A)
```

    [[2. 1. 1.]
     [1. 2. 1.]
     [1. 1. 2.]]

```{margin}

Como $A$ es normal entonces se cumple que $AA^T=A^TA$.

```

```python
print(A@A.T)
```

    [[2. 1. 1.]
     [1. 2. 1.]
     [1. 1. 2.]]

```python
evalue, evector = np.linalg.eig(A)
```

```python
print('eigenvalores:')
print(evalue)
```

    eigenvalores:
    [0.5+0.866j 0.5-0.866j 2. +0.j   ]

```{margin}

Se verifica que los eigenvectores de este ejemplo forman un conjunto linealmente independiente pues $A$ es normal.

```

```python
print('eigenvectores:')
print(evector)
```

    eigenvectores:
    [[-0.289+0.5j -0.289-0.5j -0.577+0.j ]
     [-0.289-0.5j -0.289+0.5j -0.577+0.j ]
     [ 0.577+0.j   0.577-0.j  -0.577+0.j ]]

```{margin}

Para una matriz normal $A$ se cumple que es unitariamente diagonalizable y $A = Q \Lambda Q^H$ donde: $Q^H$ es la conjugada transpuesta de $Q$.

```

```python
print('descomposición espectral:')
Lambda = np.diag(evalue)
Q = evector

```

    descomposición espectral:

```python
print('QLambdaQ^H:')
print(Q@Lambda@Q.conjugate().T)

```

    QLambdaQ^H:
    [[ 1.+0.j  1.+0.j -0.+0.j]
     [ 0.+0.j  1.+0.j  1.+0.j]
     [ 1.+0.j -0.+0.j  1.+0.j]]

```python
print(A)
```

    [[1. 1. 0.]
     [0. 1. 1.]
     [1. 0. 1.]]

```{margin}

Observa que $Q^HQ=QQ^H = I_3$ donde: $Q^H$ es la conjugada transpuesta de $Q$.
```

```python
print(Q.conjugate().T@Q)
```

    [[1.+0.j 0.-0.j 0.+0.j]
     [0.+0.j 1.+0.j 0.-0.j]
     [0.-0.j 0.+0.j 1.+0.j]]

```{admonition} Observación
:class: tip

El problema del cálculo de eigenvalores para matrices normales es bien condicionado.

```

**Preguntas de comprehensión:**

1)¿Qué son los eigenvalores de una matriz y qué nombre recibe el conjunto de eigenvalores de una matriz?

2)¿Cuántos eigenvalores como máximo puede tener una matriz?

3)¿Qué característica geométrica tiene multiplicar una matriz por su eigenvector?

4)¿A qué se le nombra matriz diagonalizable o *non defective*?

5)¿Cuál es el número de condición del problema de cálculo de eigenvalores con multiplicidad simple para una matriz simétrica?

6)¿Verdadero o Falso?

a.Una matriz es diagonalizable entonces tiene eigenvalores distintos.

b.Una matriz con eigenvalores distintos es diagonalizable.

c.Si $A=XDX^{-1}$ con $X$ matriz invertible entonces en la diagonal de $D$ y en las columnas de $X$ encontramos eigenvalores y eigenvectores derechos de $A$ respectivamente.

7)Describe la descomposición espectral de una matriz simétrica.

8)¿Qué característica tienen las matrices similares?

**Referencias:**

1. M. T. Heath, Scientific Computing. An Introductory Survey, McGraw-Hill, 2002.

2.  G. H. Golub, C. F. Van Loan, Matrix Computations, John Hopkins University Press, 2013.

3. L. Trefethen, D. Bau, Numerical linear algebra, SIAM, 1997.

4. C. Meyer, Matrix Analysis and Applied Linear Algebra, SIAM, 2000.


# The Discrete Fourier Transform

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

## Fast Fourier Transform

The discrete Fourier transformation (DFT) can be implemented computationally very efficiently by the [fast Fourier transform (FFT)](https://en.wikipedia.org/wiki/Fast_Fourier_transform). Various algorithms have been developed for the FFT resulting in various levels of computational efficiency for a wide range of DFT lengths. The concept of the so called [radix-2 Cooley–Tukey algorithm](https://en.wikipedia.org/wiki/Cooley%E2%80%93Tukey_FFT_algorithm) is introduced in the following.

### Radix-2 Decimation-in-Time Algorithm

Let's first consider the straightforward implementation of the DFT $X[\mu] = \text{DFT}_N \{ x[k] \}$ by its definition

\begin{equation}
X[\mu] = \sum_{k=0}^{N-1} x[k] \, w_N^{\mu k}
\end{equation}

where $w_N = e^{-j \frac{2 \pi}{N}}$. Evaluation of the definition for $\mu = 0,1,\dots,N-1$ requires $N^2$ complex multiplications and $N \cdot (N-1)$ complex additions. The numerical complexity of the DFT scales consequently [on the order of](https://en.wikipedia.org/wiki/Big_O_notation) $\mathcal{O} (N^2)$.

The basic idea of the radix-2 decimation-in-time (DIT) algorithm is to decompose the computation of the DFT into two summations: one over the even indexes $k$ of the signal $x[k]$ and one over the odd indexes. Splitting the definition of the DFT for an even lengths $N$ and rearranging terms yields

\begin{align}
X[\mu] &= \sum_{\kappa = 0}^{\frac{N}{2} - 1} x[2 \kappa] \, w_N^{\mu 2 \kappa} +
\sum_{\kappa = 0}^{\frac{N}{2} - 1} x[2 \kappa + 1] \, w_N^{\mu (2 \kappa + 1)} \\
&= \sum_{\kappa = 0}^{\frac{N}{2} - 1} x[2 \kappa] \, w_{\frac{N}{2}}^{\mu \kappa} +
w_N^{\mu} \sum_{\kappa = 0}^{\frac{N}{2} - 1} \, x[2 \kappa + 1] w_{\frac{N}{2}}^{\mu \kappa} \\
&= X_1[\mu] + w_N^{\mu} \cdot X_2[\mu]
\end{align}

It follows from the last equality that the DFT can be decomposed into two DFTs $X_1[\mu]$ and $X_2[\mu]$ of length $\frac{N}{2}$ operating on the even and odd indexes of the signal $x[k]$. The decomposed DFT requires $2 \cdot (\frac{N}{2})^2 + N$ complex multiplications and $2 \cdot \frac{N}{2} \cdot (\frac{N}{2} -1) + N$ complex additions. For a length $N = 2^w$ with $w \in \mathbb{N}$ which is a power of two this principle can be applied recursively till a DFT of length $2$ is reached. This comprises then the radix-2 DIT algorithm. It requires $\frac{N}{2} \log_2 N$ complex multiplications and $N \log_2 N$ complex additions. The numerical complexity of the FFT algorithm scales consequently on the order of $\mathcal{O} (N \log_2 N)$. The notation DIT is due to the fact that the decomposition is performed with respect to the (time-domain) signal $x[k]$ and not its spectrum $X[\mu]$.

The derivation of the FFT following above principles for the case $N=8$ is illustrated using signal flow diagrams. The first decomposition results in

where $\underset{a}{\oplus}$ denotes the weigthed summation $g[k] + a \cdot h[k]$ of two signals whereby the weighted signal $h[k]$ is denoted by the arrow. The same decomposition is now applied to each of the DFTs of length $\frac{N}{2} = 4$ resulting in two DFTs of length $\frac{N}{4} = 2$

where $w_\frac{N}{2}^0 = w_N^0$, $w_\frac{N}{2}^1 = w_N^2$, $w_\frac{N}{2}^2 = w_N^4$ und $w_\frac{N}{2}^3 = w_N^6$. The resulting DFTs of length $2$ can be realized by

where $w_2^0 = 1$ und $w_2^1 = -1$. Combining the decompositions yields the overall flow diagram of the FFT for $N=8$

Further optimizations can be applied by noting that various common terms exist and that a sign reversal requires to swap only one bit in common representations of floating point numbers.

### Benchmark

The radix-2 DIT algorithm presented above can only be applied to lengths $N = 2^w$ which are are power of two. Similar and other principles can be applied to derive efficient algorithms for various other cases. A wide variety of implemented FFTs is available for many hardware platforms. Their computational efficiency depends heavily on the particular algorithm and hardware used. In the following the performance of the [`numpy.fft`](http://docs.scipy.org/doc/numpy/reference/generated/numpy.fft.fft.html#numpy.fft.fft) function is evaluated by comparing the execution times of a [DFT realized by matrix/vector multiplication](definition.ipynb#Matrix/Vector-Representation) with the FFT algorithm. Note that the execution of the following cell may some time.

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

n = np.arange(14)  # lengths = 2**n to evaluate
reps = 100  # number of repetitions per measurement

# measure execution times
gain = np.zeros(len(n))
for N in n:
    length = 2**N
    # setup environment for timeit
    tsetup = 'import numpy as np; from scipy.linalg import dft;  \
            x=np.random.randn(%d)+1j*np.random.randn(%d); F = dft(%d)' % (length, length, length)
    # DFT
    tc = timeit.timeit('np.matmul(F, x)', setup=tsetup, number=reps)
    # FFT
    tf = timeit.timeit('np.fft.fft(x)', setup=tsetup, number=reps)
    # gain by using the FFT
    gain[N] = tc/tf

# show the results
plt.figure(figsize = (15, 10))
plt.barh(n-.5, gain, log=True)
plt.plot([1, 1], [-1, n[-1]+1], 'r-')
plt.yticks(n, 2**n)
plt.xlabel('Gain of FFT')
plt.ylabel('Length $N$')
plt.title('Ratio of execution times between DFT and FFT')
plt.grid()
```

**Exercise**

* For which lengths $N$ is the FFT algorithm faster than the direct computation of the DFT?
* Why is it slower below a given length $N$?
* Does the trend of the gain follow the expected numerical complexity of the radix-2 FFT algorithm?

**Copyright**

The notebooks are provided as [Open Educational Resource](https://de.wikipedia.org/wiki/Open_Educational_Resources). Feel free to use the notebooks for your own educational purposes. The text is licensed under [Creative Commons Attribution 4.0](https://creativecommons.org/licenses/by/4.0/), the code of the IPython examples under the [MIT license](https://opensource.org/licenses/MIT). Please attribute the work as follows: *Lecture Notes on Signals and Systems* by Sascha Spors.


```python
%matplotlib inline
from matplotlib import pyplot as plt

import sympy as sp
import numpy as np
import math
```

### Exact solution using Sympy

```python
x = sp.Function("x")
K = sp.Function("K")
t = sp.symbols("t")
m = sp.symbols("m")
r = sp.symbols("r")
k = sp.symbols("k")

dgl = sp.Eq(m*x(t).diff(t).diff(t) + r*x(t).diff(t) + k*x(t), K(t))
dgl
```

$\displaystyle k x{\left(t \right)} + m \frac{d^{2}}{d t^{2}} x{\left(t \right)} + r \frac{d}{d t} x{\left(t \right)} = K{\left(t \right)}$

```python
sp.dsolve(dgl, x(t))
```

$\displaystyle x{\left(t \right)} = C_{1} e^{\frac{t \left(- r - \sqrt{- 4 k m + r^{2}}\right)}{2 m}} + C_{2} e^{\frac{t \left(- r + \sqrt{- 4 k m + r^{2}}\right)}{2 m}} - \frac{e^{\frac{t \left(- r - \sqrt{- 4 k m + r^{2}}\right)}{2 m}} \int K{\left(t \right)} e^{\frac{r t}{2 m}} e^{\frac{t \sqrt{- 4 k m + r^{2}}}{2 m}}\, dt}{\sqrt{- 4 k m + r^{2}}} + \frac{e^{\frac{t \left(- r + \sqrt{- 4 k m + r^{2}}\right)}{2 m}} \int K{\left(t \right)} e^{\frac{r t}{2 m}} e^{- \frac{t \sqrt{- 4 k m + r^{2}}}{2 m}}\, dt}{\sqrt{- 4 k m + r^{2}}}$

### Runge-Kutta solution using Scipy

```python
def oscillator(t, x, K_0, omega, m, r, k):

    x_1, x_2 = x # x: vector of x_1 and x_2

    dxdt = [x_2, 1/m*(K_0*math.sin(omega*t) - r*x_2 - k*x_1)]
    return dxdt
```

```python
from scipy.integrate import solve_ivp
init = [0, 0]
time = np.arange(0, 3, 0.1)
time_span = [0, 3]

K_0 = 1
omega = 0.2
m = 0.5
r = 0.1
k = 0.2

dgl_RK45 = solve_ivp(oscillator, time_span, init, method="RK45", t_eval=time, args = (K_0, omega, m, r, k))
```

```python
plt.figure()
plt.plot(time, dgl_RK45.y[0, :], dgl_RK45.y[1, :])
plt.legend(["$x_1$ Runge-Kutta", "$x_2$ Runge-Kutta"])
plt.xlabel("Time")
plt.ylabel("$y$")
plt.show()
```

```python
dgl_RK45
```

      message: 'The solver successfully reached the end of the integration interval.'
         nfev: 50
         njev: 0
          nlu: 0
          sol: None
       status: 0
      success: True
            t: array([0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1. , 1.1, 1.2,
           1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2. , 2.1, 2.2, 2.3, 2.4, 2.5,
           2.6, 2.7, 2.8, 2.9])
     t_events: None
            y: array([[0.00000000e+00, 6.63200869e-05, 5.27309831e-04, 1.76946532e-03,
            4.16797423e-03, 8.08480963e-03, 1.38672118e-02, 2.18506552e-02,
            3.23538823e-02, 4.56764372e-02, 6.20986655e-02, 8.18806121e-02,
            1.05259336e-01, 1.32462317e-01, 1.63694785e-01, 1.99137417e-01,
            2.38946337e-01, 2.83253116e-01, 3.32164771e-01, 3.85763766e-01,
            4.44108756e-01, 5.07242177e-01, 5.75185763e-01, 6.47938342e-01,
            7.25476244e-01, 8.07753300e-01, 8.94700842e-01, 9.86227707e-01,
            1.08222023e+00, 1.18254226e+00],
           [0.00000000e+00, 1.98600542e-03, 7.88292551e-03, 1.75874367e-02,
            3.09817736e-02, 4.79344570e-02, 6.83013237e-02, 9.19245793e-02,
            1.18635561e-01, 1.48256051e-01, 1.80598281e-01, 2.15465907e-01,
            2.52657766e-01, 2.91958231e-01, 3.33147574e-01, 3.76003879e-01,
            4.20303040e-01, 4.65818770e-01, 5.12322591e-01, 5.59583840e-01,
            6.07371908e-01, 6.55464606e-01, 7.03627220e-01, 7.51628873e-01,
            7.99245629e-01, 8.46260496e-01, 8.92463425e-01, 9.37651308e-01,
            9.81627978e-01, 1.02420421e+00]])
     y_events: None


### Solve Equations

* fit time-of-flight to mass spectrum

$ \text{time} = k \sqrt{\frac{\text{mass}}{\text{charge}}} + \text{offset}$

* numeric approx.:

$ \frac{\text{mass}}{\text{charge}} = (c_1 \text{time} + c_2 )^2 $

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

from sympy.solvers import solve
from sympy import symbols, Eq

```

```python
m1, tof1 = 198, 13.0184
m2, tof2 = 71, 8.18377
```

```python
c1, c2 = symbols('c1,c2')

eq1 = Eq(m1,(c1*tof1+c2)**2)
eq2 = Eq(m2,(c1*tof2+c2)**2)

solutions = solve((eq1 , eq2) ,c1,c2)

solutions
```

    [(-4.65338548195968, 46.5083862788736),
     (-1.16763795911868, 1.12953072752030),
     (1.16763795911868, -1.12953072752030),
     (4.65338548195968, -46.5083862788736)]

```python
c1, c2 = [each for each in solutions if abs(each[0]) > abs(each[1])][0]

x = np.arange(start= 0, stop=200, step=1)
y = (c1 * x + c2)**2

plt.plot(x,y)
```

```python

```


# Input Driven HMM

This notebook is a simple example of an HMM with exogenous inputs.  The inputs modulate the probability of discrete state transitions via a multiclass logistic regression. Let $z_t \in \{1, \ldots, K\}$ denote the discrete latent state at time $t$ and $u_t \in \mathbb{R}^U$ be the exogenous input at time~$t$.  The transition probability is given by,

$$
\begin{align}
\Pr(z_t = k \mid z_{t-1} = j, u_t) =
\frac{\exp\{\log P_{j,k} + w_k^\mathsf{T} u_t\}}
{\sum_{k'=1}^K \exp\{\log P_{j,k'} + w_{k'}^\mathsf{T} u_t\}}.
\end{align}
$$

The parameters of the transition model are $P \in \mathbb{R}_+^{K \times K}$, a baseline set of (unnormalized) transition weights, and $W \in \mathbb{R}^{K \times U}$, a set of input weights.

## 1. Setup
The line `import ssm` imports the package for use. Here, we have also imported a few other packages for plotting.

```python
import numpy as np
import numpy.random as npr
import matplotlib.pyplot as plt
import ssm
import seaborn as sns
from ssm.util import one_hot, find_permutation

%matplotlib inline

npr.seed(0)
sns.set(palette="colorblind")
```

## 2. Create an Input Driven HMM
SSM is designed to be modular, so that the user can easily mix and match different types of transitions and observations.
We create an input-driven HMM with the following line:
```python
true_hmm = ssm.HMM(num_states, obs_dim, input_dim,
               observations="categorical", observation_kwargs=dict(C=num_categories),
               transitions="inputdriven")
```

Let's look at what each of the arguments do. The first three arguments specify the number of states, and the dimensionality of the observations and inputs.

**Setting the observation model**
For this example, we have set `observations="categorical"`, which means each observation will take on one of a discrete set of values, i.e $y_t \in \{1, \ldots, C \}$.
For categorical observations, the observations are drawn from a multinomial distribution, with parameters depending on the current state. Assuming $z_t = k$,the observations are a vector $y \in \mathbb{R}^D$, where $y_i \sim \text{mult} (\lambda_{k,i})$, where $\lambda_{k,i}$ is the multinomal parameter associated with coordinate $i$ of the observations in state $k$. Note that each observation variable is independent from the others.

For categorical observations, we also specify the number of discrete observations possible (in this case 3). We do this by creating a dictionary where the keys are the keyword arguments which we want to pass to the observation model. For categorical observations, there is just one keyword argument, `C`, which specifies the number of categories. This is set using `observation_kwargs=dict(C=num_categories)`.

The observations keyword argument should be one of : `"gaussian", "poisson" "studentst", "exponential", "bernoulli", "autoregressive", "robust_autoregressive"`.
**NOTE:**
Setting the observations as "autoregressive" means that each observation will be dependent on the prior observation, as well as on the input (if the input is nonzero). By constrast, the standard "inputdriven" transitions are not affected by previous observations or directly by the inputs.

**Setting the transition model**
In order to create an HMM with exogenous inputs, we set ```transitions="inputdriven"```. This means that the baseline transition matrix $P$ is modified according to a Generalized Linear Model, as described at the top of the page.

SSM support many transition models, set by keyword argument to the constructor of the class. The keyword argument should be one of: `"standard", "sticky", "inputdriven", "recurrent", "recurrent_only", "rbf_recurrent", "nn_recurrent".` We're working on creating standalone documentation to describe these in more detail. For most users, the stationary and input driven transition classes should suffice.

**Creating inputs and sampling**
After creating our HMM object, we create an input array called `inpt` which is simply a jittered sine wave. We also increase the transition weights so that it will be clear (for demonstration purposes) that the input is changing the transition probabilities. In this case, we will actually increase the weights such that the transitions appear almost deterministic.

```python
# Set the parameters of the HMM
time_bins = 1000 # number of time bins
num_states = 2    # number of discrete states
obs_dim = 1    # data dimension
input_dim = 1    # input dimension
num_categories = 3    # number of output types/categories

# Make an HMM
true_hmm = ssm.HMM(num_states, obs_dim, input_dim,
               observations="categorical", observation_kwargs=dict(C=num_categories),
               transitions="inputdriven")

# Optionally, turn up the input weights to exaggerate the effect
true_hmm.transitions.Ws *= 3

# Create an exogenous input
inpt = np.sin(2 * np.pi * np.arange(time_bins) / 50)[:, None] + 1e-1 * npr.randn(time_bins, input_dim)

# Sample some data from the HMM
true_states, obs = true_hmm.sample(time_bins, input=inpt)

# Compute the true log probability of the data, summing out the discrete states
true_lp = true_hmm.log_probability(obs, inputs=inpt)

# By default, SSM returns categorical observations as a list of lists.
# We convert to a 1D array for plotting.
obs_flat = np.array([x[0] for x in obs])
```

```python
np.dot(inpt[1:], true_hmm.transitions.Ws.T)
```

    array([[ 0.77037179, -0.40312928],
           [ 1.64201046, -0.85925069],
           [ 2.2494287 , -1.17710771],
           ...,
           [-1.92855912,  1.00919927],
           [-1.95378084,  1.02239759],
           [ 0.23567837, -0.12332857]])

```python
true_hmm.log_Ps
```

```python
true_hmm.transitions.Ws.T.shape
```

    (1, 2)

```python
# Plot the data
plt.figure(figsize=(8, 5))

plt.subplot(311)
plt.plot(inpt)
plt.xticks([])
plt.xlim(0, time_bins)
plt.ylabel("input")

plt.subplot(312)
plt.imshow(true_states[None, :], aspect="auto")
plt.xticks([])
plt.xlim(0, time_bins)
plt.ylabel("discrete\nstate")
plt.yticks([])

# Create Cmap for visualizing categorical observations
plt.subplot(313)
plt.imshow(obs_flat[None,:], aspect="auto", )
plt.xlim(0, time_bins)
plt.ylabel("observation")
plt.grid(b=None)
plt.show()
```

### 2.1 Exercise: EM for the input-driven HMM
There are a few good references that derive the EM algorithm for the case of a vanilla HMM (e.g Machine Learning and Pattern Recognition, by Chris Bishop, and [this tutorial](https://www.ece.ucsb.edu/Faculty/Rabiner/ece259/Reprints/tutorial%20on%20hmm%20and%20applications.pdf) by Lawrence Rabiner. How should the EM updates change for the case of input-driven HMMs?

## 3. Fit an input-driven HMM to data
Below, we'll show to fit an input-driven HMM from data. We'll treat the samples generated above as a dataset, and try to learn the appropriate HMM parameters from this dataset.

We create a new HMM object here, with the same parameters as the HMM in Section 1:
```python
hmm = ssm.HMM(num_states, obs_dim, input_dim,
          observations="categorical", observation_kwargs=dict(C=num_categories),
          transitions="inputdriven")
```

We fit the dataset simply by calling the `fit` method:
```python
hmm_lps = hmm.fit(obs, inputs=inpt, method="em", num_em_iters=N_iters)
```
Here, the variable `hmm_lps` will be set to a list of log-probabilities at each step of the EM-algorithm, which we'll use to check convergence.

```python
# Now create a new HMM and fit it to the data with EM
N_iters = 100
hmm = ssm.HMM(num_states, obs_dim, input_dim,
          observations="categorical", observation_kwargs=dict(C=num_categories),
          transitions="inputdriven")

# Fit
hmm_lps = hmm.fit(obs, inputs=inpt, method="em", num_iters=N_iters)
```

      0%|          | 0/100 [00:00<?, ?it/s]

### 3.1 Permute the latent states, check convergence
As in the vanilla-HMM notebook, we need to find a permutation of the latent states from our new hmm such that they match the states from the true HMM above. SSM accomplishes this with two function calls: first, we call `find_permutation(true_states, inferred_states)` which returns a list of indexes into states.

Then, we call `hmm.permute(permuation)` with the results of our first function call. Finally, we set `inferred_states` to be the underlying states we predict given the data.

Below, we plot the results of the `fit` function in order to check convergence of the EM algorithm. We see that the log-probability from the EM algorithm approaches the true log-probability of the data (which we have stored as `lp_true`).

```python
# Find a permutation of the states that best matches the true and inferred states
hmm.permute(find_permutation(true_states, hmm.most_likely_states(obs, input=inpt)))
inferred_states = hmm.most_likely_states(obs, input=inpt)
```

```python
# Plot the log probabilities of the true and fit models
plt.plot(hmm_lps, label="EM")
plt.plot([0, N_iters], true_lp * np.ones(2), ':k', label="True")
plt.legend(loc="lower right")
plt.xlabel("EM Iteration")
plt.xlim(0, N_iters)
plt.ylabel("Log Probability")
plt.show()
```

### 3.3 Exercise: Change the Fitting Method
As an experiment, try fitting the same dataset using another fitting method. The two other fitting methods supported for HMMs are "sgd" and "adam", which you can set by passing `method="sgd"` and `method="adam"` respectively. For these methods, you'll probably need to increase the number of iteratations to around 1000 or so.

After fitting with a different method, re-run the two cells above to generate a plot. How does the convergence of these other methods converge to EM?

```python
# Plot the true and inferred states
plt.figure(figsize=(8, 3.5))

plt.subplot(211)
plt.imshow(true_states[None, :], aspect="auto")
plt.xticks([])
plt.xlim(0, time_bins)
plt.ylabel("true\nstate")
plt.yticks([])

plt.subplot(212)
plt.imshow(inferred_states[None, :], aspect="auto")
plt.xlim(0, time_bins)
plt.ylabel("inferred\nstate")
plt.yticks([])
plt.show()
```

## 4. Visualize the Learned Parameters
After calling `fit`, our new HMM object will have parameters updated according to the dataset. We can get a sense of whether we successfully learned these parameters by comparing them to the _true_ parameters which generated the data.

Below, we plot the baseline log transition probabilities (the log of the state-transition matrix) as well as the input weights $w$.

```python
# Plot the true and inferred input effects
plt.figure(figsize=(8, 4))

vlim = max(abs(true_hmm.transitions.log_Ps).max(),
           abs(true_hmm.transitions.Ws).max(),
           abs(hmm.transitions.log_Ps).max(),
           abs(hmm.transitions.Ws).max())

plt.subplot(141)
plt.imshow(true_hmm.transitions.log_Ps, vmin=-vlim, vmax=vlim, cmap="RdBu", aspect=1)
plt.xticks(np.arange(num_states))
plt.yticks(np.arange(num_states))
plt.title("True\nBaseline Weights")
plt.grid(b=None)

plt.subplot(142)
plt.imshow(true_hmm.transitions.Ws, vmin=-vlim, vmax=vlim, cmap="RdBu", aspect=num_states/input_dim)
plt.xticks(np.arange(input_dim))
plt.yticks(np.arange(num_states))
plt.title("True\nInput Weights")
plt.grid(b=None)

plt.subplot(143)
plt.imshow(hmm.transitions.log_Ps, vmin=-vlim, vmax=vlim, cmap="RdBu", aspect=1)
plt.xticks(np.arange(num_states))
plt.yticks(np.arange(num_states))
plt.title("Inferred\nBaseline Weights")
plt.grid(b=None)

plt.subplot(144)
plt.imshow(hmm.transitions.Ws, vmin=-vlim, vmax=vlim, cmap="RdBu", aspect=num_states/input_dim)
plt.xticks(np.arange(input_dim))
plt.yticks(np.arange(num_states))
plt.title("Inferred\nInput Weights")
plt.grid(b=None)
plt.colorbar()
plt.show()
```

```python
np.exp(true_hmm.transitions.log_Ps)
```

    array([[0.96470641, 0.03529359],
           [0.02991731, 0.97008269]])

```python
true_hmm.transitions.Ws
```

    array([[ 5.60267397],
           [-2.93183364]])

```python
hmm.transitions.Ws
```

    array([[ 3.40151154],
           [-5.75812834]])

```python

```


# Relevant IMSRG equations

Normal-ordered operators:

$$
\begin{equation}
E_0 = \sum_{i} n_i t_{ii} + \frac{1}{2}\sum_{ij}n_in_j V_{ijij} + \frac{1}{6} n_i n_j n_k V_{ijkijk}^{(3)}
\label{_auto1} \tag{1}
\end{equation}
$$

$$
\begin{equation}
f_{pq} = t_{pq} + \sum_{i}n_i V_{piqi} + \frac{1}{2} \sum_{ij}n_i n_j V_{pijqij}^{(3)}
\label{_auto2} \tag{2}
\end{equation}
$$

$$
\begin{equation}
\Gamma_{pqrs} = V_{pqrs} + \sum_{i}n_i V_{pqirsi}^{(3)}
\label{_auto3} \tag{3}
\end{equation}
$$

$$
\begin{equation}
W_{pqrstu} = V_{pqrstu}^{(3)}
\label{_auto4} \tag{4}
\end{equation}
$$

IMSRG flow equations:

$$
\begin{equation}
\frac{d}{ds}E_0 = \sum_{ab} n_a\bar{n}_b \left(\eta_{ab}f_{ba}-f_{ab}\eta_{ba} \right)
+\frac{1}{4} \sum_{abcd} n_a n_b \bar{n}_c\bar{n}_d \left( \eta_{abcd}\Gamma_{cdab} - \Gamma_{abcd}\eta_{cdab} \right)
\label{_auto5} \tag{5}
\end{equation}
$$

$$
\begin{equation}
\begin{aligned}
\frac{d}{ds} f_{ij} = \sum_{a} \left(\eta_{ia}f_{aj}-f_{ia}\eta_{aj}\right)
&+ \sum_{ab}(n_a\bar{n}_b-\bar{n}_a n_b) \left(\eta_{ab}\Gamma_{biaj}-f_{ab}\eta_{biaj} \right)
\\
&+ \frac{1}{2} \sum_{abc}(n_an_b\bar{n}_c + \bar{n}_a\bar{n}_bn_c) \left( \eta_{ciab}\Gamma_{abcj} - \Gamma_{ciab}\eta_{abcj} \right)
\end{aligned}
\label{_auto6} \tag{6}
\end{equation}
$$

$$
\begin{equation}
\begin{aligned}
\frac{d}{ds}\Gamma_{ijkl} = &\sum_{a}\left[ (1-P_{ij}\left( \eta_{ia}\Gamma_{ajkl}-f_{ia}\eta_{ajkl} \right) - ( 1-P_{kl}) \left( \eta_{ak}\Gamma_{kjal} - f_{ak}\eta_{ijal} \right) \right]
\\
&+ \frac{1}{2} \sum_{ab} (\bar{n}_a\bar{n}_b-n_a n_b)\left( \eta_{ijab}\Gamma_{abkl} - \Gamma_{ijab}\eta_{abkl} \right) \\
&+ (1-P_{ij})(1-P_{kl}) \sum_{ab}(n_a\bar{n}_b-\bar{n}_an_b) \eta_{aibk}\Gamma_{bjal}
\end{aligned}
\label{_auto7} \tag{7}
\end{equation}
$$

The White generator:

$$
\begin{equation}
\eta^{\text{Wh}}_{ai} = \frac{f_{ai}}{\Delta_{ai}}
\label{_auto8} \tag{8}
\end{equation}
$$

$$
\begin{equation}
\eta^{\text{Wh}}_{abij} = \frac{\Gamma_{abij}}{\Delta_{abij}}
\label{_auto9} \tag{9}
\end{equation}
$$

Energy denominators:

$$
\begin{equation}
\Delta_{ai} = f_{aa} - f_{ii}
\label{_auto10} \tag{10}
\end{equation}
$$

$$
\begin{equation}
\Delta_{abij} = f_{aa} + f_{bb} - f_{ii} - f_{jj}
\label{_auto11} \tag{11}
\end{equation}
$$

Simplification of flow equation for nuclear matter:

$$
\begin{equation}
\frac{d}{ds}E_0 = \frac{1}{4} \sum_{abcd} n_a n_b \bar{n}_c\bar{n}_d \left( \eta_{abcd}\Gamma_{cdab} - \Gamma_{abcd}\eta_{cdab} \right)
\label{_auto12} \tag{12}
\end{equation}
$$

$$
\begin{equation}
\frac{d}{ds} f_{ij} =
\frac{1}{2} \sum_{abc}(n_an_b\bar{n}_c + \bar{n}_a\bar{n}_bn_c) \left( \eta_{ciab}\Gamma_{abcj} - \Gamma_{ciab}\eta_{abcj} \right)
\label{_auto13} \tag{13}
\end{equation}
$$

$$
\begin{equation}
\begin{aligned}
\frac{d}{ds}\Gamma_{ijkl} &= (A_{ii} + A_{jj} - A_{kk} - A_{ll})B_{ijkl} - (B_{ii} + B_{jj} - B_{kk} - B_{ll})A_{ijkl}
\\
&+ \frac{1}{2} \sum_{ab} (\bar{n}_a\bar{n}_b-n_a n_b)\left( \eta_{ijab}\Gamma_{abkl} - \Gamma_{ijab}\eta_{abkl} \right) \\
&+ (1-P_{ij})(1-P_{kl}) \sum_{ab}(n_a\bar{n}_b-\bar{n}_an_b) \eta_{aibk}\Gamma_{bjal}
\end{aligned}
\label{_auto14} \tag{14}
\end{equation}
$$

# Benchmarking the pairing model
Pairing model with 4 particles, in 4 doubly degenerate levels, for $\delta=1$ and $g=+0.5$

Solving the IMSRG flow equation with a simple Euler step method with
step size $ds = 0.1$.  $E_0$ is the zero-body piece of the flowing
Hamiltonian $H(s)$.  EMBPT2 is the second order MBPT energy using
$H(s)$, and $dE/ds$ is the zero body part of $[\eta(s), H(s)]$.

$s$	  $E_0$ 	   EMBPT2  	  $dE/ds$

   0.0    	    1.50000    	    -0.0623932    	    0.0000000

   0.1    	    1.48752    	    -0.0531358    	    -0.1247860

   0.2    	    1.47689    	    -0.0453987    	    -0.1062720

   0.3    	    1.46781    	    -0.0388940    	    -0.0907975

   0.4    	    1.46004    	    -0.0333983    	    -0.0777880

   0.5    	    1.45336    	    -0.0287359    	    -0.0667967

The same numbers as before, but now the $[\eta_2, H_2]_2$
particle-hole commutator term is omitted.

$s$	  $E_0$ 	   EMBPT2  	  $dE/ds$

   0      	    1.5        	    -0.0623932    	    0

   0.1    	    1.48752    	    -0.0531358    	    -0.124786

   0.2    	    1.47689    	    -0.0453551    	    -0.106272

   0.3    	    1.46782    	    -0.0387878    	    -0.0907102

   0.4    	    1.46007    	    -0.0332251    	    -0.0775756

   0.5    	    1.45342    	    -0.0284994    	    -0.0664503

   0.6    	    1.44772    	    -0.0244746    	    -0.0569988

   0.7    	    1.44283    	    -0.0210395    	    -0.0489492

These are the same as before, but with the painful $[\eta_{2b},
H_{2b}]_{2b}$ particle-hole term omitted.

$s$	   $E0$ 	   EMBPT2  	  $dE/ds$

   0.0    	    1.50000    	    -0.0623932    	    0

   0.1    	    1.48752    	    -0.0531358    	    -0.124786

   0.2    	    1.47689    	    -0.0453551    	    -0.106272

   0.3    	    1.46782    	    -0.0387878    	    -0.0907102

   0.4    	    1.46007    	    -0.0332251    	    -0.0775756

   0.5    	    1.45342    	    -0.0284994    	    -0.0664503


# Euler Equation with CentPy in 2d

### Import packages

```python
# Install the centpy package
!pip install centpy
```

    Collecting centpy
      Downloading https://files.pythonhosted.org/packages/92/89/7cbdc92609ea7790eb6444f8a189826582d675f0b7f59ba539159c43c690/centpy-0.1-py3-none-any.whl
    Requirement already satisfied: numpy in /usr/local/lib/python3.6/dist-packages (from centpy) (1.18.5)
    Installing collected packages: centpy
    Successfully installed centpy-0.1

```python
# Import numpy and centpy for the solution
import numpy as np
import centpy
```

```python
# Imports functions from matplotlib and setup for the animation
import matplotlib.pyplot as plt
from matplotlib import animation
from IPython.display import HTML
```

### Equation

We solve the Euler equations in 2D

\begin{equation}
\partial_t
\begin{bmatrix} \rho \\ \rho u_x \\ \rho u_y \\ E \end{bmatrix}
+
\partial_x
\begin{bmatrix} \rho u_x \\ \rho u_x^2 + p \\  \rho u_x u_y \\ (E+p) u_x \end{bmatrix}
+
\partial_y
\begin{bmatrix} \rho u_y \\ \rho u_y u_x \\  \rho u_y^2 +p \\ (E+p) u_y \end{bmatrix}
= 0
\end{equation}

with the equation of state

\begin{equation}
p = (\gamma-1) \left(E-\frac{1}{2} \rho (u_x^2 - u_y^2) \right), \qquad \gamma=1.4
\end{equation}

on the domain $(x,y,t)\in([0,1]\times[0,1]\times[0,0.1])$ with initial data for a *2D Riemann problem*:

\begin{equation}
(\rho, v, p)_{t=0} =
\begin{cases}
(1,0,1) & \text{if} & 0<x\leq0.5 \\
(0.125, 0, 0.1) & \text{if} & 0.5<x<1
\end{cases}
\end{equation}

and Dirichlet boundary data set by initial data on each boundary. The solution is computed using a 200 $\times$ 200 mesh and CFL number 0.75.

```python
pars = centpy.Pars2d(
    x_init=0., x_final=1.,
    y_init=0., y_final=1.,
    J=200, K=200,
    t_final=0.4,
    dt_out=0.005,
    cfl=0.475,
    scheme="fd2",)
pars.gamma = 1.4
```

```python
# Euler equation
class Euler2d(centpy.Equation2d):

    # Helper functions and definitions for the equation

    def pressure(self, u):
        return (self.gamma - 1.0) * (
            u[:, :, 3] - 0.5 * (u[:, :, 1] ** 2 + u[:, :, 2] ** 2) / u[:, :, 0]
        )

    def euler_data(self):
        gamma = self.gamma

        p_one = 1.5
        p_two = 0.3
        p_three = 0.029
        p_four = 0.3

        upper_right, upper_left, lower_right, lower_left = np.ones((4, 4))

        upper_right[0] = 1.5
        upper_right[1] = 0.0
        upper_right[2] = 0.0
        upper_right[3] = (
            p_one / (gamma - 1.0)
            + 0.5 * (upper_right[1] ** 2 + upper_right[2] ** 2) / upper_right[0]
        )

        upper_left[0] = 0.5323
        upper_left[1] = 1.206 * upper_left[0]
        upper_left[2] = 0.0
        upper_left[3] = ( p_two / (gamma - 1.0)
            + 0.5 * (upper_left[1] ** 2 + upper_left[2] ** 2) / upper_left[0] )

        lower_right[0] = 0.5323
        lower_right[1] = 0.0
        lower_right[2] = 1.206 * lower_right[0]
        lower_right[3] = ( p_four / (gamma - 1.0)
            + 0.5 * (lower_right[1]**2 + lower_right[2]**2) / lower_right[0] )

        lower_left[0] = 0.138
        lower_left[1] = 1.206 * lower_left[0]
        lower_left[2] = 1.206 * lower_left[0]
        lower_left[3] = ( p_three / (gamma - 1.0)
          + 0.5 * (lower_left[1] ** 2 + lower_left[2] ** 2) / lower_left[0] )

        return upper_right, upper_left, lower_right, lower_left

    # Abstract class equation definitions

    def initial_data(self):
        u = np.empty((self.J + 4, self.K + 4, 4))
        midJ = int(self.J / 2) + 2
        midK = int(self.K / 2) + 2

        one_matrix = np.ones(u[midJ:, midK:].shape)
        upper_right, upper_left, lower_right, lower_left = self.euler_data()

        u[midJ:, midK:] = upper_right * one_matrix
        u[:midJ, midK:] = upper_left * one_matrix
        u[midJ:, :midK] = lower_right * one_matrix
        u[:midJ, :midK] = lower_left * one_matrix
        return u

    def boundary_conditions(self, u):

        upper_right, upper_left, lower_right, lower_left = self.euler_data()

        if self.odd:
            j = slice(1, -2)
            u[j, 0] = u[j, 1]
            u[j, -2] = u[j, -3]
            u[j, -1] = u[j, -3]

            u[0, j] = u[1, j]
            u[-2, j] = u[-3, j]
            u[-1, j] = u[-3, j]

            # one
            u[-2, -2] = upper_right
            u[-1, -2] = upper_right
            u[-2, -1] = upper_right
            u[-1, -1] = upper_right

            # two
            u[0, -2] = upper_left
            u[0, -1] = upper_left

            # three
            u[0, 0] = lower_left
            u[0, 1] = lower_left
            u[1, 0] = lower_left
            u[1, 1] = lower_left

            # four
            u[-2, 0] = lower_right
            u[-1, 0] = lower_right
            u[-2, 1] = lower_right
            u[-1, 1] = lower_right

        else:

            j = slice(2, -1)
            u[j, 0] = u[j, 2]
            u[j, 1] = u[j, 2]
            u[j, -1] = u[j, -2]

            u[0, j] = u[2, j]
            u[1, j] = u[2, j]
            u[-1, j] = u[-2, j]

            # one
            u[-1, -2] = upper_right
            u[-1, -1] = upper_right

            # two
            u[0, -2] = upper_left
            u[0, -1] = upper_left
            u[1, -2] = upper_left
            u[1, -1] = upper_left

            # three
            u[0, 0] = lower_left
            u[0, 1] = lower_left
            u[1, 0] = lower_left
            u[1, 1] = lower_left

            # four
            u[-1, 0] = lower_right
            u[-1, 1] = lower_right

    def flux_x(self, u):
        f = np.empty_like(u)

        p = self.pressure(u)

        f[:, :, 0] = u[:, :, 1]
        f[:, :, 1] = u[:, :, 1] ** 2 / u[:, :, 0] + p
        f[:, :, 2] = u[:, :, 1] * u[:, :, 2] / u[:, :, 0]
        f[:, :, 3] = (u[:, :, 3] + p) * u[:, :, 1] / u[:, :, 0]

        return f

    def flux_y(self, u):
        g = np.empty_like(u)

        p = self.pressure(u)

        g[:, :, 0] = u[:, :, 2]
        g[:, :, 1] = u[:, :, 1] * u[:, :, 2] / u[:, :, 0]
        g[:, :, 2] = u[:, :, 2] ** 2 / u[:, :, 0] + p
        g[:, :, 3] = (u[:, :, 3] + p) * u[:, :, 2] / u[:, :, 0]

        return g

    def spectral_radius_x(self, u):

        j0 = centpy._helpers.j0

        rho = u[j0, j0, 0]
        vx = u[j0, j0, 1] / rho
        vy = u[j0, j0, 2] / rho
        p = (self.gamma - 1.0) * (u[j0, j0, 3] - 0.5 * rho * (vx ** 2 + vy ** 2))
        c = np.sqrt(self.gamma * p / rho)
        return np.abs(vx) + c

    def spectral_radius_y(self, u):

        j0 = centpy._helpers.j0

        rho = u[j0, j0, 0]
        vx = u[j0, j0, 1] / rho
        vy = u[j0, j0, 2] / rho
        p = (self.gamma - 1.0) * (u[j0, j0, 3] - 0.5 * rho * (vx ** 2 + vy ** 2))
        c = np.sqrt(self.gamma * p / rho)
        return np.abs(vy) + c
```

### Solution

```python
eqn = Euler2d(pars)
soln = centpy.Solver2d(eqn)
soln.solve()
```

### Animation

```python
# Animation
fig = plt.figure()
ax = plt.axes(xlim=(soln.x_init,soln.x_final), ylim=(soln.y_init, soln.y_final))

ax.contour(soln.x[1:-1], soln.y[1:-1], soln.u_n[0,1:-1,1:-1,0])
def animate(i):
    ax.collections = []
    ax.contour(soln.x[1:-1], soln.y[1:-1], soln.u_n[i,1:-1,1:-1,0])

plt.close()
anim = animation.FuncAnimation(fig, animate, frames=soln.Nt, interval=100, blit=False);
HTML(anim.to_html5_video())
```

```python

```


#I sistemi lineari
###Esempio, nucleao di un'A.L.
$$
f(x, y, z) = (z+y, x-z) \\
\operatorname{ker} f =
\begin{cases}
z + y = 0 \\
x - z = 0
\end{cases}
$$
Le due equazioni formanon un sistema linare.
Questo insieme di equazioni possono essere arbitrariamente complesse.
Sistemi più complessi (più equazioni e più incognite) non sono facilmente risolvibili.

Alcuni sistemi sono più facilmente risolbili di altri.
$$
\begin{cases}
1x + 1y + 2z = 1 \\
0x + 2y - 1z = 0 \\
0x + 4y + 0z = 7
\end{cases}
$$
Questo sistema è facilmente risolvibile perchè ho molti coefficenti nulli.
Posso notare come la matrice relativa a questo sistema è una matrice ridotta.

#Rappresentazione dei sistemi lineari
I S.L. possono essere rappresentati da una sequenza di $m$ equazioni di cui la prima è di $n$ icognite
$$
a_{11}x_1 + a_{12}x_2 + \dots + a_{1n}x_n = b_1 \\
a_{21}x_a + a_{22}x_2 + \dots + a_{2n}x_n = b_n \\
\vdots \\
a_{m1}x_1 + a_{m2}x_2 + \dots + a_{mn}x_n = b_n
$$
Posso facilmente notare come posso rappresentare questo sistema tramite due matrici.
 1. La matrice $A, R^{mn}$ formata dai coefficenti $a_{mn}$.
 2. La matrice $B, R^{m1}$ formata dai termini noti.

Quindi posso rappresentare interamente con la matrice completa (A|B)
$$
(A|B) =
\left[
\begin{array}{ccc|c}
a_{11} & \dots & a_{1n} & b_1 \\
a_{21} & \dots & a_{2n} & b_2 \\
\dots  & \dots & \dots  & \dots \\
a_{m1} & \dots & a_{mn} & b_m
\end{array}
\right]
$$
Posso vedere che definendo una matrice colonna con le mie icognite
$$
X =
\begin{bmatrix}
x_1 \\ x_2 \\ \dots \\ x_m
\end{bmatrix}
$$
la dicitura $AX=B$ mi crea esattamente il mio sistema di equazioni.
Quindi analizzare S.L. equivale a studiare delle matrice.
#Sistema lineare ridotto
Un sistema è ridotto (quindi facilmente risolvibile) se la matrice $A$ dei coefficenti è ridotta per righe.
###Esempio
$$
\begin{cases}
2x - 1y + 1z = 1 \\
0x + 4y - 3z = 0 \\
0x + 5y + 0z = 10
\end{cases} \\
A =
\begin{bmatrix}
2 & -1 & 1 \\
0 & 4 & 3 \\
0 & 5 & 0
\end{bmatrix} \\
B =
\begin{bmatrix}
1 \\ 0 \\ 10
\end{bmatrix}
$$
###Tecnica di soluzione
La matrice A è ridotta per righe, quindi questo è un sistema ridotto.
Si parte dall'ultima equazione (che contiene più zeri e quindi il numero minimo di incognite) e si risale un'equazione alla volta.
$$
\begin{align}
5y = 10 & \rightarrow y = 2 \\
4y - 3z = 0 & \rightarrow z =  8/3 \\
2x - 2 + 8/3 = 1 & \rightarrow x = \frac{1 + 2 - 8/3}{2}
\end{align}
$$
###S.L. con infinite soluzioni
Se il numero di equazioni (righe) è minore del numero di incognite, nel cercare la soluzione mi rimarrano delle icognite.
$$
\left[
\begin{array}{cccc|c}
2 & 1 & 4 & 1 & 1 \\
0 & 2 & -1 & 1 & 1 \\
0 & 0 & 2 & 1 & 0
\end{array}
\right]
$$
Partendo dall'ultima riga ottengo
$$
2z + t = 0
$$
e scelgo di risolvere per $t$ per evitare una divisione
$$
t = -2z
$$
Ora salgo di una riga
$$
2y - z + y = 1
2y - 3z = 1
$$
Ora guardando la matrice mi accorgo che y, per la riga che sto analizzando, è l'elemento speciale, quello che sotto di se ha solo zeri.
Devo risolvere questa equazione per y
$$
y = \frac{3z +1}{2}
$$
Quindi vado ad analizzare la prima riga
$$
2x + y + 4z + t = 1 \\
2x + \frac{3z +1}{2} + 4z - 2z = 1
$$
Questo ci permette di avere tutte le mie incognite dipendenti solo da $z$
$$
\begin{align}
x & = \frac{1 - 2x - \frac{3z +1}{2}}{2} \\
y & = \frac{3z +1}{2} \\
t & = -2z
\end{align}
$$
Quindi $z$ posso sceglierla a mio piacere.
Quindi $z$ viene detta **incognita libera** quindi questo sistema ha infinite soluzioni.
La regola da ricordare è **quando devo scegliere per quale incognita risolvere una riga devo scegliere quella abbinata al coefficente speciale**.
##Sistema ridotto privo di soluzioni
Se nel ridurre la matrice di un sistma trovo una riga con tutti coefficenti nulli ma con termine noto non nullo allora il sistema è irrisolvibile (detto anche *incompatibile*)

##Analisi di un sistema generico AX=B
Devo eseguire i seguenti passaggi
 1. Identificare se il sistema è risolubile
 2. Trovare le soluzioni del sistema

Se il sistema è ridotto per righe si può lavorare direttamente sulla matrice per analizzare il sistema.
Se il sistema non è ridotto allora dobbiamo **trasformarlo** in un sistema **equivalente** (*con le stesse soluzioni*) ma **ridotto per righe**.

```

```

Algebra 25 Sistemi lineari I(risoluzione sistemi ridotti).ipynb