# Clase 3b: Perfil de Yukovski

_Aunque no te lo creas, __con lo que hemos visto hasta ahora eres capaz de hacer grandes cosas__. Vale sí, un perfil de Yukovski no es gran cosa aerodinámicamente, pero si lo hacemos en Python... Echa un vistazo a la figura ¿no está mal, no? algo así intentaremos conseguir al final de esta clase._

_Como no se trata de aprender (o reaprender) aerodinámica, te daremos las funciones matemáticas y los pasos a seguir así como la estructura del programa. Tú sólo tienes que preocuparte de programar cada bloque. Puedes leer en detalle todo lo relativo a la aerodinámica en el libro  Aerodinámica básica de Meseguer Ruiz, J., Sanz Andrés, A. (Editorial Garceta)._

## 1. Importamos paquetes.

Lo primero es lo primero, importemos los paquetes:

```python
# Recuerda, utilizaremos arrays y pintaremos gráficas.
import numpy as np
import matplotlib.pyplot as plt
```

## 2. Parámetros del problema

######
__Fuente:__ _Aerodinámica básica, Meseguer Ruiz, J., Sanz Andrés, A._

La transformación de Yukovski es: $$\tau=t+\frac{a^{2}}{t}$$

Los parámetros del problema son los del siguiente bloque, puedes cambiarlos más adelante:

```python
# Datos para el perfil de Yukovski
# Parámetro de la transformación de Yukovski
a = 1

# Centro de la circunferencia
landa = 0.2    # coordenada x (en valor absoluto)
delta = 0.3     # coordenada y

t0 = a * (-landa + delta * 1j)     # centro: plano complejo

# Valor del radio de la circunferencia
R = a * np.sqrt((1 + landa)**2 + delta**2)

# Ángulo de ataque corriente incidente
alfa_grados = 0
alfa = np.deg2rad(alfa_grados)

#Velocidad de la corriente incidente
U = 1
```

## 3. Perfil de Yukoski a partir de una circunferencia.

### Función transformación de Yukovski

__Se trata de definir una función que realice la transformación de Yukovski.__ Esta función recibirá el parámetro de la transformación, $a$ y el punto del plano complejo $t$. Devolverá el valor $\tau$, punto del plano complejo en el que se transforma $t$.

```python
def transf_yukovski(a, t):
    """Dado el punto t (complejo) y el parámetro a
    a de la transformación proporciona el punto
    tau (complejo) en el que se transforma t."""
    tau = t + a ** 2 / t
    return tau
```

```python
# aeropython: preserve
#comprobamos que la función está bien programada
#puntos del eje real siguen siendo del eje real
err_message = "La transformación de Yukovski no devuelve un resultado correcto"
np.testing.assert_equal(transf_yukovski(1, 1+0j), 2+0j, err_message)
```

### Circunferencia

Ahora queremos transformar la circunferencia de radio $R$ con centro en $t_0$ usando la función anterior:

1. __Creamos `N` puntos de la circunferencia__ de modo que __en `Xc` estén las coordenadas $x$ y en `Yc` estén las coordenadas $y$__ de los puntos que la forman. Controla el número de puntos mediante un parámetro que se llame `N_perfil`.
    $$X_c = real(t_0) + R·cos(\theta)$$
    $$Y_c = imag(t_0) + R·sin(\theta)$$
2. Una vez hayas obtenido los dos arrays `Xc` e `Yc`, __píntalos mediante un `scatter`__ para comprobar que todo ha ido bien.
3. Pinta también el __centro de la circunferencia__.

Deberías obtener algo así:

```python

# Número de puntos de la circunferencia que
# vamos a transformar para obtener el perfil
N_perfil = 100

#se barre un ángulo de 0 a 2 pi
theta = np.linspace(0, 2*np.pi, N_perfil)

#se crean las coordenadas del los puntos
#de la circunferencia
Xc = np.real(t0) + R * np.cos(theta)
Yc = np.imag(t0) + R * np.sin(theta)

#lo visualizamos
plt.figure("circunferencia", figsize=(5,5))
plt.title('Circunferencia', {'fontsize':20})
plt.scatter(Xc, Yc)
plt.scatter(np.real(t0), np.imag(t0), color='orange', marker='x', s=100)
plt.grid()
plt.show()
```

```python
# aeropython: preserve
# Lo visualizamos más bonito
plt.figure("circunferencia", figsize=(5,5))
plt.title('Circunferencia', {'fontsize':20})
# Esto no tienes por qué entenderlo ahora
p = plt.Polygon(list(zip(Xc, Yc)), color="#cccccc", zorder=3)
plt.gca().add_patch(p)

plt.ylim(-1.5, 2)
plt.xlim(-2, 1.5)
plt.grid()
plt.show()
```

### Transformación de cirunferencia a perfil

Ahora estamos en condiciones de __transformar estos puntos de la circunferencia (`Xc`, `Yc`) en los del perfil (`Xp`, `Yp`)__. Para esto vamos a usar nuestra función `transf_yukovski`. Recuerda que esta función recibe y da números complejos. ¿Saldrá un perfil?

```python
Puntos_perfil = transf_yukovski(a, Xc+Yc*1j)
Xp, Yp = np.real(Puntos_perfil) , np.imag(Puntos_perfil)

#lo visualizamos
plt.figure("perfil yukovski", figsize=(10,10))
plt.title('Perfil', {'fontsize':20})
plt.scatter(Xp, Yp)
plt.grid()
plt.gca().set_aspect(1)
plt.show()
```

```python
# aeropython: preserve
#lo visualizamos más bonito
plt.figure('perfil yukovski', figsize=(10,10))
plt.title('Perfil', {'fontsize':20})

p = plt.Polygon(list(zip(Xp, Yp)), color="#cccccc", zorder=3)
plt.gca().add_patch(p)
plt.gca().set_aspect(1)

plt.xlim(-3, 3)
plt.ylim(-0.4,1)
plt.grid()

plt.show()
```

## 4. Flujo alrededor del cilindro

Para visualizar ahora el flujo alrededor del cilindro recurrimos al __potencial complejo__ de una _corriente uniforme_ que forme un ángulo $\alpha$ con el eje $x$ _en presencia de un cilindro_ (aplicando el teorema del círculo) y se añade un torbellino con la intensidad adecuada para que se cumpla la hipótesis de Kutta en el perfil:

\begin{equation}
f(t)=U_{\infty}\text{·}\left((t-t_{0})\text{·}e^{-i\alpha}+\frac{R^{2}}{t-t_{0}}\text{·}e^{i\alpha}\right)+\frac{i\Gamma}{2\pi}\text{·}ln(t-t_{0})=\Phi+i\Psi
\end{equation}

donde $\Phi$ es el potencial de velocidades y $\Psi$ es la función de corriente.

$$\Gamma = 4  \pi  a  U  (\delta + (1+\lambda)  \alpha)$$

$\Gamma$ es la circulación que hay que añadir al cilindro para que al transformarlo en el perfil se cumpla la condición de Kutta.

Recordando que la función de corriente toma un valor constante en las líneas de corriente, sabemos que: dibujando $\Psi=cte$ se puede visualizar el flujo.

__Pintaremos estas lineas de potencial constante utilizando la función `contour()`, pero antes tendremos que crear una malla circular. Esto será lo primero que hagamos:__

1. Crea un parámetro `N_R` cuyo valor sea el número de puntos que va a tener la malla en dirección radial. Desde otro punto de vista, esta parámetro es el número de círculos concéntricos que forman la malla.
2. Crea dos parámetros `R_min` y `R_max` que representen el radio mínimo y máximo entre los que se extiende la malla. El radio mínimo debe de ser el radio del círculo, porque estamos calculando el aire en el exterior del perfil.
4. La dirección tangencial necesita un sólo parámetro `N_T`, que representa el número de puntos que la malla tendrá en esta dirección. Dicho de otro modo, cuántos puntos forman los círculos concéntricos de la malla.
3. Crea un array `R_` que vaya desde `R_min` hasta `R_max` y que tenga `N_R` elementos. De manera análoga, crea el array `T_`, que al representar los ángulos de los puntos que forman las circunferencias, debe ir de 0 a 2$\pi$, y tener `N_T` elementos.
4. Para trabajar con la malla, deberemos usar coordenadas cartesianas. Crea la malla: `XX, YY` van a ser dos matrices de `N_T · N_R` elementos. Cada elemento de estas matrices se corresponde con un punto de la malla: la matriz `XX` contiene las coordenadas X de cada punto y la matriz `YY`, las coordenadas y.

La manera de generar estas matrices tiene un poco de truco, porque depende de ambos vectores.
Para cada elemento, $x = real(t_0) +  R · cos (T) $ ,  $y = imag(t_0) + R · sin(T)$.

__Recuerda que:__

* Los puntos sobre la circunferencia se transforman en el perfil.
* Los puntos interiores a la circunferencia se transforman en puntos interiores al perfil.
* Los puntos exteriores a la circunferencia se transforman en puntos exteriores al perfil. __Los puntos que nos interesan__.

```python
#se crea la malla donde se va pintar la función de corriente
# Dirección radial
N_R = 50   # Número de puntos en la dirección radial
R_min = R
R_max = 10

# Dirección tangencial
N_T = 180   # Número de puntos en la dirección tangencial

R_ = np.linspace(R_min, R_max, N_R)

T_ = np.linspace(0, 2*np.pi , N_T)

# Crear la malla:
XX =  R_ * np.cos(T_).reshape((-1, 1)) + np.real(t0)
YY = R_ * np.sin(T_).reshape((-1, 1)) + np.imag(t0)
```

```python
#pintamos la malla para verla
plt.figure(figsize=(10,10))
plt.scatter(XX.flatten(), YY.flatten(), marker='.')
plt.show()
```

__NOTA__: En versiones anteriores se utilizaba una malla rectangular. Esto generaba algunos problemas con los puntos interiores a la hora de pintar las líneas de corriente y los campos de velocidades y presiones. La idea de usar una malla circular está tomada de [este ejercicio](http://nbviewer.ipython.org/github/barbagroup/AeroPython/blob/master/lessons/06_Lesson06_Assignment.ipynb) del curso Aerodynamics-Hydrodynamics with Python de la [Prof. Lorena Barba](http://lorenabarba.com/).

### Probando a transformar la malla

Bueno, lo que queríamos era hacer cosas alrededor de nuestro perfil, ¿no?

Esto lo conseguiremos pintando la función $\Psi$ en los puntos `XX_tau, YY_tau` transformados de los `XX, YY` a través de la función `transf_yukovski`, recuerda que la transformación que tenemos recibe y da números complejos. Como antes, debes separar parte real e imaginaria. En la siguiente celda calcula y transforma `tt` (donde debe estar almacenada la malla en forma compleja) para obtener `XX_tau, YY_tau`.

Probemos a visualizar como se transforman los puntos de la malla primero.

```python
tt = XX + YY * 1j
tautau = transf_yukovski(a, tt)
XX_tau, YY_tau = np.real(tautau) , np.imag(tautau)
```

```python
# Comprobamos que los puntos exteriores a la circunferencia se transforman en los puntos exteriores del perfil
#pintamos la malla para verla
plt.figure(figsize=(10,10))
plt.scatter(XX_tau.flatten(), YY_tau.flatten(), marker='.')
plt.show()
```

### Obteniendo el flujo

1. Crea una variable `T` que tenga el valor correspondiente a la circulación $\Gamma$.
2. Utilizando el array `tt`, el valor `T` y los parámetros definidos al principio (`t0, alfa, U...`) crea `f` según la fórmula de arriba (no hace falta que crees una función).
3. Guarda la parte imaginaria de esa función (función de corriente) en una variable `psi`.

```python
# aeropython: preserve
# Circulación que hay que añadir al cilindro para
# que se cumpla la hipótesis de Kutta en el perfil
T = 4 * np.pi * a * U * (delta + (1+landa) * alfa)

# Malla compleja
tt = XX + YY * 1j

# Potencial complejo
f = U * ( (tt - t0) * np.exp(-alfa *1j) + R**2 / (tt - t0) * np.exp(alfa * 1j) )
f += 1j * T / (2* np.pi) * np.log(tt - t0)

# Función de corriente
psi = np.imag(f)
```

Como la función de corriente toma un valor constante en cada línea de corriente, podemos visualizar el flujo alrededor del cilindro pintando las lineas en las que `psi` toma un valor constante. Para ello utilizaremos la función `contour()` en la malla `XX, YY`. Si no se ve nada prueba a cambiar el número de líneas y los valores máximo y mínimo de la función que se representan.

```python
#lo visualizamos
# aeropython: preserve
plt.figure('lineas de corriente', figsize=(10,10))

plt.contour(XX, YY, psi, np.linspace(-5,5,50))
plt.grid()
plt.gca().set_aspect(1)
#plt.xlim(-8, 8)
#plt.ylim(-3, 3)

plt.show()
```

```python
#ponemos el cilindro encima
# aeropython: preserve
plt.figure('flujo cilindro', figsize=(10,10))

plt.contour(XX, YY, psi, np.linspace(-5,5,50), colors=['blue', 'blue'])
plt.grid()
plt.gca().set_aspect(1)

p = plt.Polygon(list(zip(Xc, Yc)), color="#cccccc", zorder=3)
plt.gca().add_patch(p)
plt.show()
```

## 5. Flujo alrededor del perfil

```python
plt.figure("flujo perfil", figsize=(12,12))

plt.contour(XX_tau, YY_tau, psi, np.linspace(-5,5,50))

plt.xlim(-8,8)
plt.ylim(-3,3)
plt.grid()
plt.gca().set_aspect(1)

plt.show()
```

```python
#Ahora ponemos el perfil encima
# aeropython: preserve
plt.figure("flujo perfil", figsize=(12,12))

plt.contour(XX_tau, YY_tau, psi, np.linspace(-5, 5, 50), colors=['blue', 'blue'])

p = plt.Polygon(list(zip(Xp, Yp)), color="#cccccc", zorder=10)
plt.gca().add_patch(p)

plt.xlim(-8,8)
plt.ylim(-3,3)
plt.grid()
plt.gca().set_aspect(1)
plt.show()
```

## 6. Interact

__Ahora es un buen momento para jugar con todos los parámetros del problema.
~~¡Prueba a cambiarlos y ejecuta el notebook entero!~~__

__Vamos a usar un `interact`, ¿no?__

Tenemos que crear una función que haga todas las tareas: reciba los argumentos y pinte para llamar a interact con ella. No tenemos más que cortar y pegar.

```python
# aeropython: preserve
def transformacion_geometrica(a, landa, delta, N_perfil=100):
    #punto del plano complejo
    t0 = a * (-landa + delta * 1j)
    #valor del radio de la circunferencia
    R = a * np.sqrt((1 + landa)**2 + delta**2)
    #se barre un ángulo de 0 a 2 pi
    theta = np.linspace(0, 2*np.pi, N_perfil)
    #se crean las coordenadas del los puntos
    #de la circunferencia
    Xc = - a * landa + R * np.cos(theta)
    Yc =   a * delta + R * np.sin(theta)
    #se crean las coordenadas del los puntos
    #del perfil
    Puntos_perfil = transf_yukovski(a, Xc+Yc*1j)
    Xp, Yp = np.real(Puntos_perfil) , np.imag(Puntos_perfil)

    #Se pintan la cirunferencia y el perfil
    fig, ax = plt.subplots(1,2)
    fig.set_size_inches(15,15)
    p_c = plt.Polygon(list(zip(Xc, Yc)), color="#cccccc", zorder=1)
    ax[0].add_patch(p_c)
    ax[0].plot(Xc,Yc)
    ax[0].set_aspect(1)
    ax[0].set_xlim(-3, 3)
    ax[0].set_ylim(-2,2)
    ax[0].grid()

    p_p = plt.Polygon(list(zip(Xp, Yp)), color="#cccccc", zorder=1)
    ax[1].add_patch(p_p)
    ax[1].plot(Xp,Yp)
    ax[1].set_aspect(1)

    ax[1].set_xlim(-3, 3)
    ax[1].set_ylim(-2,2)
    ax[1].grid()
    plt.show()
```

```python
from ipywidgets import interact
```

```python
# aeropython: preserve
interact(transformacion_geometrica, landa=(-1.,1, 0.01), delta=(-1.,1,0.01),
             a=(0,2.,0.1), N_perfil=(4, 200) )
```

Failed to display Jupyter Widget of type interactive.

  If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

    <function __main__.transformacion_geometrica>

```python
# aeropython: preserve
def flujo_perfil_circunferencia(landa, delta, alfa, U=1,  N_malla = 100):
    N_perfil=N_malla
    a=1
    #punto del plano complejo
    t0 = a * (-landa + delta * 1j)
    #valor del radio de la circunferencia
    R = a * np.sqrt((1 + landa)**2 + delta**2)

    #se barre un ángulo de 0 a 2 pi
    theta = np.linspace(0, 2*np.pi, N_perfil)
    #se crean las coordenadas del los puntos
    #de la circunferencia
    Xc = - a * landa + R * np.cos(theta)
    Yc =   a * delta + R * np.sin(theta)

    #se crean las coordenadas del los puntos
    #del perfil
    Puntos_perfil = transf_yukovski(a, Xc+Yc*1j)
    Xp, Yp = np.real(Puntos_perfil) , np.imag(Puntos_perfil)

    #se crea la malla donde se va pintar la función de corriente

    # Dirección radial
    N_R = N_malla//2   # Número de puntos en la dirección radial
    R_min = R
    R_max = 10

    # Dirección tangencial
    N_T = N_malla   # Número de puntos en la dirección tangencial

    R_ = np.linspace(R_min, R_max, N_R)
    T_ = np.linspace(0, 2*np.pi , N_T)

    # El menos en la XX  es para que el borde de ataque del perfil esté en la izquierda
    XX = - (R_ * np.cos(T_).reshape((-1, 1)) - np.real(t0))
    YY = R_ * np.sin(T_).reshape((-1, 1)) + np.imag(t0)

    tt = XX + YY * 1j

    alfa = np.deg2rad(alfa)
    # Circulación que hay que añadir al cilindro para
    # que se cumpla la hipótesis de Kutta en el perfil
    T = 4 * np.pi * a * U * (delta + (1+landa) * alfa)
    #Potencial complejo
    f = U * ( (tt - t0) * np.exp(-alfa *1j) + R**2 / (tt - t0) * np.exp(alfa * 1j) )
    f += 1j * T / (2* np.pi) * np.log(tt - t0)
    #Función de corriente
    psi = np.imag(f)

    Puntos_plano_tau = transf_yukovski(a, tt)
    XX_tau, YY_tau = np.real(Puntos_plano_tau) , np.imag(Puntos_plano_tau)

    #Se pinta
    fig, ax = plt.subplots(1,2)
    #lineas de corriente
    fig.set_size_inches(15,15)

    ax[0].contour(XX, YY, psi, np.linspace(-10,10,50), colors = ['blue', 'blue'])
    ax[0].grid()
    ax[0].set_aspect(1)
    p = plt.Polygon(list(zip(Xc, Yc)), color="#cccccc", zorder=10)
    ax[0].add_patch(p)
    ax[0].set_xlim(-5, 5)
    ax[0].set_ylim(-2,2)

    ax[1].contour(XX_tau, YY_tau, psi, np.linspace(-10,10,50), colors = ['blue', 'blue'])
    ax[1].grid()
    ax[1].set_aspect(1)
    p = plt.Polygon(list(zip(Xp, Yp)), color="#cccccc", zorder=10)
    ax[1].add_patch(p)
    ax[1].set_xlim(-5, 5)
    ax[1].set_ylim(-2,2)
    plt.show()
```

```python
p = interact(flujo_perfil_circunferencia, landa=(0.,1), delta=(0.,1), alfa=(0, 30), U=(0,10), N_malla = (10,150))
```

Failed to display Jupyter Widget of type interactive.

  If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

## 7. Pintemos un poco más

Con los datos que ya hemos manejado, sin mucho más esfuerzo, podemos fácilmente pintar la velocidad y la presión del aire alrededor del perfil.

```python
# aeropython: preserve
#Velocidad conjugada
dfdt = U * ( 1 * np.exp(-alfa * 1j) - R**2 / (tt - t0)**2 * np.exp(alfa * 1j) )
dfdt += 1j * T / (2*np.pi) * 1 / (tt - t0)
#coeficiente de presion
cp = 1 - np.abs(dfdt)**2 / U**2
```

```python
# aeropython: preserve
cmap = plt.cm.RdBu
```

```python
# aeropython: preserve
#Se pinta
fig, ax = plt.subplots(1,3)
#lineas de corriente
fig.set_size_inches(15,15)
ax[0].contour(XX, YY, psi, np.linspace(-10,10,50), colors = ['blue', 'blue'])
ax[0].grid()
ax[0].set_aspect(1)
p = plt.Polygon(list(zip(Xc, Yc)), color="#cccccc", zorder=10)
ax[0].add_patch(p)
#Campo de velocidades
ax[1].contourf(XX, YY, np.abs(dfdt), 200, cmap=cmap)
p = plt.Polygon(list(zip(Xc, Yc)), color="#cccccc", zorder=10)
ax[1].set_title('campo de velocidades')
ax[1].add_patch(p)
ax[1].set_aspect(1)
ax[1].grid()
#campo de presiones
ax[2].contourf(XX, YY, cp, 200, cmap=cmap)
p = plt.Polygon(list(zip(Xc, Yc)), color="#cccccc", zorder=10)
ax[2].set_title('coeficiente de presión')
ax[2].add_patch(p)
ax[2].set_aspect(1)
ax[2].grid()
plt.show()
```

```python
#Se pinta
# aeropython: preserve
fig, ax = plt.subplots(1,3)
#lineas de corriente
fig.set_size_inches(15,15)
ax[0].contour(XX_tau, YY_tau, psi, np.linspace(-10,10,50), colors = ['blue', 'blue'])
ax[0].grid()
ax[0].set_aspect(1)
p = plt.Polygon(list(zip(Xp, Yp)), color="#cccccc", zorder=10)
ax[0].add_patch(p)
#Campo de velocidades
ax[1].contourf(XX_tau, YY_tau, np.abs(dfdt), 200, cmap=cmap)
p = plt.Polygon(list(zip(Xp, Yp)), color="#cccccc", zorder=10)
ax[1].set_title('campo de velocidades')
ax[1].add_patch(p)
ax[1].set_aspect(1)
ax[1].grid()
#campo de presiones
ax[2].contourf(XX_tau, YY_tau, cp, 200, cmap=cmap)
p = plt.Polygon(list(zip(Xp, Yp)), color="#cccccc", zorder=10)
ax[2].set_title('coeficiente de presión')
ax[2].add_patch(p)
ax[2].set_aspect(1)
ax[2].grid()
plt.show()
```

```python
# aeropython: preserve
def cp_perfil_circunferencia(landa, delta, alfa, U=1,  N_malla = 100):
    N_perfil=N_malla
    a=1
    #punto del plano complejo
    t0 = a * (-landa + delta * 1j)
    #valor del radio de la circunferencia
    R = a * np.sqrt((1 + landa)**2 + delta**2)

    #se barre un ángulo de 0 a 2 pi
    theta = np.linspace(0, 2*np.pi, N_perfil)
    #se crean las coordenadas del los puntos
    #de la circunferencia
    Xc = - a * landa + R * np.cos(theta)
    Yc =   a * delta + R * np.sin(theta)

    #se crean las coordenadas del los puntos
    #del perfil
    Puntos_perfil = transf_yukovski(a, Xc+Yc*1j)
    Xp, Yp = np.real(Puntos_perfil) , np.imag(Puntos_perfil)

    #se crea la malla donde se va pintar la función de corriente

    # Dirección radial
    N_R = N_malla//2   # Número de puntos en la dirección radial
    R_min = R
    R_max = 10

    # Dirección tangencial
    N_T = N_malla   # Número de puntos en la dirección tangencial

    R_ = np.linspace(R_min, R_max, N_R)
    T_ = np.linspace(0, 2*np.pi, N_T)

    # El menos en la XX  es para que el borde de ataque del perfil esté en la izquierda
    XX = - (R_ * np.cos(T_).reshape((-1, 1)) - np.real(t0))
    YY = R_ * np.sin(T_).reshape((-1, 1)) + np.imag(t0)

    tt = XX + YY * 1j

    alfa = np.deg2rad(alfa)
    # Circulación que hay que añadir al cilindro para
    # que se cumpla la hipótesis de Kutta en el perfil
    T = 4 * np.pi * a * U * (delta + (1+landa) * alfa)

    #Velocidad conjugada
    dfdt = U * ( 1 * np.exp(-alfa * 1j) - R**2 / (tt - t0)**2 * np.exp(alfa * 1j) )
    dfdt = dfdt + 1j * T / (2*np.pi) * 1 / (tt - t0)
    #coeficiente de presion
    cp = 1 - np.abs(dfdt)**2 / U**2

    Puntos_plano_tau = transf_yukovski(a, tt)
    XX_tau, YY_tau = np.real(Puntos_plano_tau) , np.imag(Puntos_plano_tau)

    #Se pinta
    fig, ax = plt.subplots(1,2)

    #coeficiente de presión
    fig.set_size_inches(15,15)

    ax[0].contourf(XX, YY, cp, 200, cmap=cmap)
    ax[0].grid()
    ax[0].set_aspect(1)
    p = plt.Polygon(list(zip(Xc, Yc)), color="#cccccc", zorder=10)
    ax[0].add_patch(p)
    ax[0].set_xlim(-5, 5)
    ax[0].set_ylim(-3,3)

    ax[1].contourf(XX_tau, YY_tau, cp, 200, cmap=cmap)
    ax[1].grid()
    ax[1].set_aspect(1)
    p = plt.Polygon(list(zip(Xp, Yp)), color="#cccccc", zorder=10)
    ax[1].add_patch(p)
    ax[1].set_xlim(-5, 5)
    ax[1].set_ylim(-3,3)
    plt.show()
```

```python
interact(cp_perfil_circunferencia, landa=(0.,1), delta=(0.,1), alfa=(0, 30), U=(0,10), N_malla = (10,200))
```

Failed to display Jupyter Widget of type interactive.

  If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

    <function __main__.cp_perfil_circunferencia>

---

_En esta clase hemos reafirmado nuestros conocimientos de NumPy, matplotlib y Python, general (funciones, bucles, condicionales...) aplicándolos a un ejemplo muy aeronáutico_

Si te ha gustado esta clase:

Tweet

---

####
¡Síguenos en Twitter!

###### Follow @Pybonacci   Follow @Alex__S12   Follow @newlawrence

Curso AeroPython por Juan Luis Cano Rodriguez y Alejandro Sáez Mollejo se distribuye bajo una Licencia Creative Commons Atribución 4.0 Internacional.

---
_Las siguientes celdas contienen configuración del Notebook_

_Para visualizar y utlizar los enlaces a Twitter el notebook debe ejecutarse como [seguro](http://ipython.org/ipython-doc/dev/notebook/security.html)_

    File > Trusted Notebook

```python
%%html
<a href="https://twitter.com/Pybonacci" class="twitter-follow-button" data-show-count="false">Follow @Pybonacci</a>

```

Follow @Pybonacci

```python
# Esta celda da el estilo al notebook
from IPython.core.display import HTML
css_file = '../styles/aeropython.css'
HTML(open(css_file, "r").read())
```

/* This template is inspired in the one used by Lorena Barba
in the numerical-mooc repository: https://github.com/numerical-mooc/numerical-mooc
We thank her work and hope you also enjoy the look of the notobooks with this style */

<link href='http://fonts.googleapis.com/css?family=Source+Sans+Pro|Josefin+Sans:400,700,400italic|Ubuntu+Condensed' rel='stylesheet' type='text/css'>

El estilo se ha aplicado =)


This notebook is based on https://nbviewer.ipython.org/github/cknoll/beispiele/blob/master/polynommatrix-1-form.ipynb (an example from my PhD-thesis).

To run it the following packages need to be installed:

- `pip install symbtools`
- `pip install ipydex`

```python
import time
print(time.ctime())
%load_ext ipydex.displaytools
```

    Tue May  4 21:21:06 2021

```python
import sympy as sp
from sympy import sin, cos
import symbtools as st
import symbtools.noncommutativetools as nct
sp.interactive.printing.init_printing(1)
```

```python
x1, x2, x3, s, t = sp.symbols('x1, x2, x3, s, t', commutative=False)
xx = sp.Matrix([x1, x2, x3])
xxdot = st.time_deriv(xx, xx)
xxddot = st.time_deriv(xx, xx, order=2)
st.make_global(xx, xxdot, xxddot)
```

Note:
- $s:=\frac{d}{dt}$ for abbreviation
- $x_1, ..., x_3$ are regarded as time implicitly dependent if used as argument `func_symbols`

Scalar demo. Applying derivative operator $\hat =$ shift it to the right according to some rules.

```python
r = nct.right_shift_all(s*x1, func_symbols=xx) ##:
```

```python
nct.right_shift_all(s*r, func_symbols=xx)
```

```python
nct.right_shift_all(s**2 *x1, func_symbols=xx)
```

```python
nct.right_shift_all((5*s**2 - s)*x1, func_symbols=xx)
```

Matrix demo. Goal: $L\Big(\frac{d}{dt}\Big)\circ A(t)$

```python
L = sp.Matrix([[0, xdot3, x2*s], [s, 0, s**2]]) ##:
```

```python
A = sp.Matrix([[1, x3, sp.cos(7*t)], [sin(x3), 0, 0], [0, 0, t]]) ##:
```

Perform matrix multiplication while respecting operator order

```python
res = nct.nc_mul(L, A) ##:
```

Apply derivative operator (shift to the right)

```python
nct.right_shift_all(res, func_symbols=xx)
```


### Tensor Decomposition Class

Circa, November 2, 2021

Author: Lekan Molux

### [Usefuls]()

+ [Accelerating Deep Neural Networks with Tensor Decompositions](https://jacobgil.github.io/deeplearning/tensor-decompositions-deep-learning)
+ [Einstein Py](https://github.com/einsteinp./einsteinp.)
+ [Projection on Tensor Product of Hilbert Space](https://math.stackexchange.com/questions/333537/projection-on-tensor-product-of-hilbert-space)
+ [Tensor Decomposition for Signal Processing and Machine Learning](https://arxiv.org/pdf/1607.01668.pdf)
+ [Tucker Compression Deep Learning](https://github.com/kittygo/tensor_decompostion/blob/master/tucker_compression.ipynb)

```python
import cupy as np.nimport numpy as np.nimport numpy.random as np.
import sys, copy

sys.path.append("../")
from Utilities import *
from Tensors import *
```

```python
X12 = np.arange(1, 25).reshape(3, 4, 2, order='F')
U = np.arange(1, 7).reshape(2, 3, order='F')
```

### Tests

```python
#1-mode unfold

mode_1_fiber = matricization(X12, mode='1')
print('\nmode_1_fiber: \n', mode_1_fiber)

#2-mode unfold

mode_2_fiber = matricization(X12, mode='2')
print('\nmode_2_fiber: \n', mode_2_fiber)

#3-mode unfold
mode_3_fiber = matricization(X12, mode='3')
print('\nmode_3_fiber \n', mode_3_fiber)
```

    mode_1_fiber:
     [[ 1  4  7 10 13 16 19 22]
     [ 2  5  8 11 14 17 20 23]
     [ 3  6  9 12 15 18 21 24]]

    mode_2_fiber:
     [[ 1  2  3 13 14 15]
     [ 4  5  6 16 17 18]
     [ 7  8  9 19 20 21]
     [10 11 12 22 23 24]]

    mode_3_fiber
     [[ 1  4  7 10  2  5  8 11  3  6  9 12]
     [13 16 19 22 14 17 20 23 15 18 21 24]]

```python
X = np..rand(5,3,4,2)
A = np..rand(4,5); B =  np..rand(4,3); C =  np..rand(3,4); D =  np..rand(3,2);
V = np.asarray([A, B, C, D], dtype=object)
```

### Test Tensor-Matrix Multiplications

```python

X = np..rand(5, 3, 4, 2)
A = np..rand(4, 5); B = np..rand(4, 3); C = np..rand(3, 4); D = np..rand(3, 2)
```

```python
T = tensor_matrix_mult(X, A, n=0, Transpose=False)
T.shape
```

    (4, 3, 4, 2)

```python
# same as above
T = tensor_matrix_mult(X, A.T, n=0, Transpose=True)
T.shape
```

    (4, 3, 4, 2)

#### Tensor by Block matrix multiplication

```python
T = tensor_matrix_mult(X, np.asarray([A, B, C, D], dtype=object), n=[0, 1, 2, 3])

T.shape
```

    (4, 4, 3, 3)

### Tucker Decomposition

For a tensor $\mathcal{X}$, the higher-order SVD for $\mathcal{X}$ is generated by decomposing it into a core tensor multiplied by a matrix along each mode. For a 3D tensor, $\mathcal{X} \in \mathbb{R}^{I \times J \times K}$, we have

\begin{align}
    \mathcal{X} = \mathcal{G} \times_1 A \times_2 B \times_3 C = \sum_{p=1}^P \sum_{q=1}^Q \sum_{r=1}^R g_{pqr} \, a_p \circ b_q \circ c_r = [\mathcal{G}; A, B, C ]
\end{align}

where $A \in \mathbb{R}^{I \times P}, B \in \mathbb{R}^{J\times Q}$, and $C=B \in \mathbb{R}^{K\times R}$ are the factor matrices (typically orthogonal:=principal components in each mode).  $\mathcal{G}  \in \mathbb{R}^{P \times Q \times R}$ is the core tensor, and its entries show the level of interaction between the different components.

Elementwise, we write the Tucker decomposition as

\begin{align}
    \mathcal{x}_{ijk} = \sum_{p=1}^P \sum_{q=1}^Q \sum_{r=1}^R \mathcal{g}_{pqr} \, a_{ip} \circ b_{jq} \circ c_{kr}  \forall \, i = 1, \cdots, I, j= 1, \cdots J, k = 1, \cdots, K.
\end{align}

In matricized form, we have per mode of each of the decomposition for a 3D tensor as

\begin{align}
    \mathcal{X}_{(1)} &\approx  A \, \mathcal{G}_{(1)} \, \left(C \otimes B\right)^T \\
    \mathcal{X}_{(2)} &\approx  B \, \mathcal{G}_{(2)} \, \left(C \otimes A\right)^T \\
    \mathcal{X}_{(3)} &\approx  C \, \mathcal{G}_{(3)} \, \left(B \otimes A\right)^T \\
\end{align}

where $\otimes$ is the matrix Kronecker product defined for a matrix $F \in \mathbb{R}^{I\times J}$ and $G \in \mathbb{R}^{K\times L}$ as,

\begin{align}
   F \otimes G &=\begin{bmatrix}
                    f_{11} \otimes G &  f_{12} \otimes G &  f_{13} \otimes G & \cdots &  f_{1J} \otimes G \\
                    f_{21} \otimes G &  f_{22} \otimes G &  f_{23} \otimes G & \cdots &  f_{2J} \otimes G \\
            \vdots &  \vdots  &  \vdots  & \ddots &  \vdots \\
                    f_{I1} \otimes G &  f_{I2} \otimes G &  f_{I3} \otimes G & \cdots &  f_{IJ} \otimes G
                 \end{bmatrix}  \\
                 %
              &=   \begin{bmatrix}
                       f_1 \otimes g_1 & f_1 \otimes g_2 & f_1 \otimes g_3 & \cdots & f_J \otimes g_{L-1} & f_J \otimes g_{L}
                 \end{bmatrix} \\
\end{align}

```python
import numpy as np.nimport numpy.random as np.
from Tensors.leading_vecs import nvecs

## adhoc functions/classes we'll need to get things rolling
class TuckerTensor():
    def __init__(self, core, U):
        """
            Tucker Tensor Class:
                Decomposes a high-order tensor into its core component and
                a set of (usually) unitary matrices associated with every
                mode of the tensor.

            Params
            ------
            core: The core tensor, whose entries shows the interaction among
                  its components
            U:    The factor matrices (typically orthogonal:=principal components
                    in each mode)

            Author: Lekan Molux
            Date: November 2, 2021
        """
        if isinstance(core, Tensor):
            self.tensor = core
        else:
            self.tensor = Tensor(core, core.shape)

        self.U    = U

def tucker_als(X, R, **options):
    """
        Performs Tucker's "Method I" for computing a rank
        (R_1, R_2, \cdots, R_N) Tucker decomposition, now known as HOSVD.

        Parameters
        ----------
        X: Tensor to be decomposed
        R: A single rank or best list of ranks to find in obtaining the Tucker SVD
        options: {key:value} map of options to use in the alternating least square optimization

        Returns
        -------
        G: Core Tensor
        [F_1, F_2, ...]: Factors of the Unitary Matrices for the modes of the tensor we are querying.

        Ref: Kolda and Baer Procedure HOSVD
    """

    if isinstance(X, Tensor):
        X = X.data

    N = X.ndim
    normX = np.linalg.norm(X)

    tol = options.get('tol', 1e-4)
    max_iter = options.get('max_iter', 100)
    dimorder = options.get('dimorder', list(range(N)))
    init     = options.get('init', 'random')
    verbose     = options.get('verbose', True)

    if np.isscalar(R):
        R *= np.ones((N, 1), dtype=np.int64)
    U = cell(N)

    assert max_iter > 0, "maximum number of iteratons cannot be negative"

    if strcmp(init,'random'):
        Uinit = cell(N)
        for n in dimorder[1:]:
            Uinit[n] = np..rand(size(X,n),R[n])
    elif strcmp(init,'nvecs') or strcmp(init,'eigs'):
        # Compute an orthonormal basis for the dominant
        # Rn-dimensional left singular subspace of
        # X_(n) (1 <= n <= N).
        Uinit = cell(N)
        for n in dimorder[1:]:
            info(f'Computing {R[n]} leading e-vectors for factor {n}.')
            Uinit[n] = nvecs(X,n,R[n])
    else:
        raise ValueError('The selected initialization method is not supported.')

    U = Uinit
    fit = 0

    if verbose:
        info('Tucker Alternating Least-Squares:')

    # Function Motherlode: Iterate until convergence
    for iter in range(max_iter):
        fitold = fit

        # iterate over all N modes of the tensor
        for n in dimorder:
            Utilde = tensor_matrix_mult(X, U, -n, Transpose=True)

            'Max the norm of (U_tilde x_n W.T) w.r.t W and keep the'
            'orthonormality of W.'
            U[n] = nvecs[Utilde, n, R[n]]

        # Assemble the approx
        core = tensor_matrix_mult(Utilde, U, n, Transpose=True)

        # Compute the fit
        normresidual = np.sqrt(normX**2 - norm(core)**2)
        fit = 1- (normresidual/normX)
        fitchange = np.abs(fitold-fit)

        if iter%5==0:
            info(f"Iter: {iter:2d}, fit: {fit:.4f}, fitdelta: {fitchange:7.1f}")

        # Did we converge yet?
        if iter>1 and fitchange < fitchangetol:
            break

    T = TuckerTensor(core, U)

    return T
```

```python
XX = tucker_als(X, [3, 3, 3])
```

```python
__author__ 		= "Lekan Molu"
__copyright__ 	= "2021, Decomposing Level Sets of PDEs"
__credits__  	= "Sylvia Herbert, Ian Abraham"
__license__ 	= "Lekan License"
__maintainer__ 	= "Lekan Molu"
__email__ 		= "patlekno@icloud.com"
__status__ 		= "Fix Tensor Mode Swap in Memory Layout."

import numpy as np.nfrom Utilities import *

class TenMat():
    def __init__(self, T, **options):
        """
            This class provides the boilerpate for matricizing a Tensor.

            # TODO: Why does Numpy flip 0-Mode with 1-Mode?

            Parameters
            ----------
            T:       A Tensor < see class_tensor.py />.
            options: A bundle class. If it is a dictionary, it is converted to a bundle.
                     It contains the following fields:
                rdims: A numpy/cupy (dtype=intp) index array which specifies the modes of T to
                       which we map the rows of a matrix, and the remaining
                       dimensions (in ascending order) map to the columns.
                cdims:  A numpy/cupy (dtype=intp) index array which specifies the modes of T to
                       which we map the   columns of a matrix, and the
                       remaining dimensions (in ascending order) map
                       to the rows.
                cyclic: String which specifies the dimension in rdim which
                        maps to the rows of the matrix, and the remaining
                        dimensions span the columns in an order specified
                        by the string argument "cyclic" as follows:

                      'fc' - Forward cyclic.  Order the remaining dimensions in the
                           columns by [rdim+1:T.ndim, 1:rdim-1].  This is the
                           ordering defined by Kiers.

                       'bc' - Backward cyclic.  Order the remaining dimensions in the
                           columns by [rdim-1:-1:1, T.ndim:-1:rdim+1].
                           This is the ordering defined by De Lathauwer, De Moor, and Vandewalle.

            Calling Signatures
            ------------------
            TenMat(T, options.rdims): Create a matrix representation of a tensor
                T.  The dimensions (or modes) specified in rdims map to the rows
                of the matrix, and the remaining dimensions (in ascending order)
                map to the columns.

            TenMat(T, cdims, Transpose=True): Similar to rdims, but for column
                dimensions are specified, and the remaining dimensions (in
                ascending order) map to the rows.

            TenMat(T, rdims, cdims): Create a matrix representation of
               tensor T.  The dimensions specified in RDIMS map to the rows of
               the matrix, and the dimensions specified in CDIMS map to the
               columns, in the order given.

            TenMat(T, rdim, cyclic): Create the same matrix representation as
               above, except only one dimension in rdim maps to the rows of the
               matrix, and the remaining dimensions span the columns in an order
               specified by the string argument STR as follows:

              'fc' - Forward cyclic.  Order the remaining dimensions in the
                           columns by [rdim+1:T.ndim, 1:rdim-1].  This is the
                           ordering defined by Kiers.

               'bc' - Backward cyclic.  Order the remaining dimensions in the
                           columns by [rdim-1:-1:1, T.ndim:-1:rdim+1].  This is the
                           ordering defined by De Lathauwer, De Moor, and Vandewalle.

            TenMat(T, options=Bundle({rdims, cdims, tsize})): Create a tenmat from a matrix
                   T along with the mappings of the row (rdims) and column indices
                   (cdims) and the size of the original tensor (T.shape).

            Author: Lekan Molux, November 3, 2021
        """

        assert isinstance(T, Tensor), 'T must be a tensor class.'
        assert T.data.ndim !=2, "Inp.t Tensor must be 2D."
        assert isinstance(T, Tensor), "T must be of class tensor type."

        if not isbundle(options) and isinstance(options, dict):
            options = Bundle(options)
        assert isbundle(options), "options must be of Bundle class."

        self.tsize = np.asarray(options.__dict__.get("tsize", T.shape), dtype=np.intp)
        self.rindices = options.__dict__.get("rdims", None)
        self.cindices = options.__dict__.get("cdims", None)
        self.data = T.data
        self.T= T

        tsize = np.asarray(options.__dict__.get("tsize", T.shape), dtype=np.intp)
        rdims = options.__dict__.get("rdims", None)
        cdims = options.__dict__.get("cdims", None)
        data  = T.data

        n = numel(tsize)

        if np.any(rdims) and np.any(cdims):
            dims_joined = np.concatenate((rdims, cdims))
        elif np.any(rdims) and not np.any(cdims):
            dims_joined = rdims
        elif not np.any(rdims) and np.any(cdims):
            dims_joined = cdims

        if not np.allclose(range(n), np.sort(dims_joined)):
            raise ValueError('Incorrect dimension specifications.')
        elif (np.prod(self.tsize[rdims]) != size(self.data, 0)):
            raise ValueError('T.shape[0] does not match size specified by rdims and shape.')
        elif (np.prod(self.tsize[cdims]) != size(self.data, 1)):
            raise ValueError('T.shape[1] does not match size specified by cdims and shape.')

        tsize = T.shape
        n     = T.ndim

        tmp = np.zeros((n), dtype=bool)
        tmp.fill(True)
        if np.any(rdims):
            tmp[np.ix_(*rdims)] = False
            cdims = np.nonzero(tmp)

        if isfield(options, 'cyclic') and options.cyclic=='T':
            cdims = copy.copy(options.rdims)
            tmp = np.zeros((n), dtype=bool)
            tmp.fill(True)
            tmp[np.ix_(*cdims)] = False
            rdims = np.nonzero(tmp)

        elif isfield(options, 'cyclic') and options.cyclic=='fc':
            rdims = options.rdims

            if numel(rdims)!=1:
                raise ValueError('Only one row dimension if third argument is ''fc''.')
            cdims = np.concatenate((np.arange(rdims, n, dtype=np.intp), \
                                    np.arange(rdims-1, dtype=np.intp)), dtype=np.intp)

        elif isfield(options, 'cyclic') and options.cyclic=='bc':
            rdims = options.rdims

            if numel(rdims)!=1:
                raise ValueError('Only one row dimension if third argument is ''bc''.')

            cdims = np.concatenate((np.arange(rdims, -1, 1, dtype=np.intp),\
                                    np.arange(n-1, -1, rdims+1, dtype=np.intp)), dtype=np.intp)
        else:
            raise ValueError('Unrecognized option.')

        rdims = options.rdims
        cdims = options.cdims
```

```python
import numpy as np.nfrom Utilities import *

class TenMat():
    def __init__(self, T, **options):
    #def __init__(self, T, rdims=None, cdims=None, cyclic=None):
        """
        This class provides the boilerpate for matricizing a Tensor.

        Parameters
        ----------
        T:       A Tensor < see class_tensor.py />.
        options: A bundle class. If it is a dictionary, it is converted to a bundle.
                 It contains the following fields:
            rdims: A numpy/cupy (dtype=np.np.intp) index array which specifies the modes of T to
                   which we map the rows of a matrix, and the remaining
                   dimensions (in ascending order) map to the columns.
            cdims:  A numpy/cupy (dtype=np.np.intp) index array which specifies the modes of T to
                   which we map the   columns of a matrix, and the
                   remaining dimensions (in ascending order) map
                   to the rows.
            cyclic: String which specifies the dimension in rdim which
                    maps to the rows of the matrix, and the remaining
                    dimensions span the columns in an order specified
                    by the string argument "cyclic" as follows:

                  'fc' - Forward cyclic.  Order the remaining dimensions in the
                       columns by [rdim+1:T.ndim, 1:rdim-1].  This is the
                       ordering defined by Kiers.

                   'bc' - Backward cyclic.  Order the remaining dimensions in the
                       columns by [rdim-1:-1:1, T.ndim:-1:rdim+1].
                       This is the ordering defined by De Lathauwer, De Moor, and Vandewalle.

        Calling Signatures
        ------------------
        TenMat(T, options.rdims): Create a matrix representation of a tensor
            T.  The dimensions (or modes) specified in rdims map to the rows
            of the matrix, and the remaining dimensions (in ascending order)
            map to the columns.

        TenMat(T, cdims, Transpose=True): Similar to rdims, but for column
            dimensions are specified, and the remaining dimensions (in
            ascending order) map to the rows.

        TenMat(T, rdims, cdims): Create a matrix representation of
           tensor T.  The dimensions specified in RDIMS map to the rows of
           the matrix, and the dimensions specified in CDIMS map to the
           columns, in the order given.

        TenMat(T, rdim, cyclic): Create the same matrix representation as
           above, except only one dimension in rdim maps to the rows of the
           matrix, and the remaining dimensions span the columns in an order
           specified by the string argument STR as follows:
           'T' - Transpose.

          'fc' - Forward cyclic.  Order the remaining dimensions in the
                       columns by [rdim+1:T.ndim, 1:rdim-1].  This is the
                       ordering defined by Kiers.

           'bc' - Backward cyclic.  Order the remaining dimensions in the
                       columns by [rdim-1:-1:1, T.ndim:-1:rdim+1].  This is the
                       ordering defined by De Lathauwer, De Moor, and Vandewalle.

        TenMat(T, options=Bundle({rdims, cdims, tsize})): Create a tenmat from a matrix
               T along with the mappings of the row (rdims) and column indices
               (cdims) and the size of the original tensor (T.shape).

        Example:
            X  = np.arange(1, 28).reshape(3,3,3)
            # print('X ', X)
            options = dict(rdims=np.array([2], dtype=np.intp))
            X_1 = TenMat(X, **options)

        Author: Lekan Molux, November 3, 2021
        """

        if not isinstance(T, Tensor):
            T = Tensor(T, T.shape)

        if not isbundle(options) and isinstance(options, dict):
            options = Bundle(options)
        assert isbundle(options), "options must be of Bundle class."

        self.tsize = np.asarray(options.__dict__.get("tsize", T.shape))
        self.rindices = options.__dict__.get("rdims", None)
        self.cindices = options.__dict__.get("cdims", None)
        self.data = T.data

        if self.rindices is None and self.cindices is None:
            return

        tsize = np.asarray(options.__dict__.get("tsize", T.shape))
        rdims = options.__dict__.get("rdims", None)
        cdims = options.__dict__.get("cdims", None)
        data  = T.data

        n = T.data.ndim

        if len(options)==1:
        #if isfield(options, 'rdims') and not isfield(options, 'cdims'):
            tmp = np.zeros((n), dtype=bool)
            tmp.fill(True)
            tmp[rdims] = False
            cdims = np.nonzero(tmp)[0]
        #elif isfield(options, 'cyclic'):
        elif len(options)>=2: #isfield(options, 'cyclic'):
            if options.cyclic=='T':
                cdims = options.rdims
                tmp = np.zeros((n,1), dtype=bool)
                tmp.fill(True)
                tmp[cdims] = False
                rdims = np.nonzero(tmp)[0]
            elif options.cyclic=='fc':
                rdims = options.rdims
                if numel(rdims)!=1:
                    raise ValueError(f'Only one row dimension if options.cyclic is ''fc''.')
                cdims = np.concatenate((np.arange(rdims, n, dtype=np.intp), \
                                        np.arange(rdims-1, dtype=np.intp)), dtype=np.intp)
            elif options.cyclic=='bc':
                rdims = options.rdims

                if numel(rdims)!=1:
                    raise ValueError('Only one row dimension if third argument is ''bc''.')

                cdims = np.concatenate((np.arange(rdims-1, dtype=np.intp)[::-1],\
                                        np.arange(rdims, n, dtype=np.intp)[::-1]), dtype=np.intp)
            else:
                raise ValueError('Unrecognized option.')

        else:
            rdims = options.rdims
            cdims = options.cdims

        # Error check
        if not np.array_equal(np.arange(n), np.sort( np.concatenate((rdims, cdims)))):
            raise ValueError('Incorrect specification of dimensions')

        # Permute T so that the dimensions specified by RDIMS come first
        T_Rot = np.transpose(T.data, axes=np.concatenate([rdims, cdims]))
        rprods = np.prod(tsize[rdims])
        np.ods = np.prod(tsize[cdims])

        self.data     = T_Rot.reshape(rprods, np.ods)
        self.rindices = rdims
        self.cindices = cdims
        self.tsize    = tsize
        self.T = Tensor(self.data, shape=self.data.shape)

    def __call__(self):
        return self.T

```

```python
X  = np.arange(1, 28).reshape(3,3,3)
X1 = np.arange(1, 10).reshape(3,3, 1)
X2 = np.arange(10, 19).reshape(3,3, 1)
X3 = np.arange(19, 28).reshape(3,3, 1)
XC = np.array_equal(X, np.concatenate((X1, X2, X3), axis =0))
print('XC: ', XC)
# print('X ', X)
options = dict(rdims=np.array([0], dtype=np.intp))
X_1 = TenMat(X, **options)
```

    XC:  False

```python
X_1.data, X_1.rindices, X_1.cindices, X_1.tsize
```

    (array([[ 1,  2,  3,  4,  5,  6,  7,  8,  9],
            [10, 11, 12, 13, 14, 15, 16, 17, 18],
            [19, 20, 21, 22, 23, 24, 25, 26, 27]]),
     array([0]),
     array([1, 2]),
     array([3, 3, 3]))

```python
XX = np.concatenate((X1, X2, X3), 2)
print('XX ', XX)
XX1 = TenMat(XX, rdims=[0])
XX1.data, XX1.rindices, XX1.cindices
```

    XX  [[[ 1 10 19]
      [ 2 11 20]
      [ 3 12 21]]

     [[ 4 13 22]
      [ 5 14 23]
      [ 6 15 24]]

     [[ 7 16 25]
      [ 8 17 26]
      [ 9 18 27]]]

    (array([[ 1, 10, 19,  2, 11, 20,  3, 12, 21],
            [ 4, 13, 22,  5, 14, 23,  6, 15, 24],
            [ 7, 16, 25,  8, 17, 26,  9, 18, 27]]),
     [0],
     array([1, 2]))

```python
XX
```

    array([[[ 1],
            [ 2],
            [ 3]],

           [[ 4],
            [ 5],
            [ 6]],

           [[ 7],
            [ 8],
            [ 9]],

           [[10],
            [11],
            [12]],

           [[13],
            [14],
            [15]],

           [[16],
            [17],
            [18]],

           [[19],
            [20],
            [21]],

           [[22],
            [23],
            [24]],

           [[25],
            [26],
            [27]]])

```python

```


# Semantics: PrefScLTL.

In this notebook, we ensure that semantics of our proposed preference logic are sound.

## Formulation

Every atomic preference formula induces four partitions of $\Sigma^\omega$. Correspondingly, we define 4 propositions each for $w_1, w_2$ to denote which scLTL formulas the words satisfy.

* `w1_00` means $w1$ satisfies $\neg \alpha_1$,  $\neg \alpha_2$.
* `w1_01` means $w1$ satisfies $\neg \alpha_1$,  $\alpha_2$.
* `w1_10` means $w1$ satisfies $\alpha_1$,  $\neg \alpha_2$.
* `w1_11` means $w1$ satisfies $\alpha_1$,  $\alpha_2$.

* `w2_00` means $w2$ satisfies $\neg \alpha_1$,  $\neg \alpha_2$.
* `w2_01` means $w2$ satisfies $\neg \alpha_1$,  $\alpha_2$.
* `w2_10` means $w2$ satisfies $\alpha_1$,  $\neg \alpha_2$.
* `w2_11` means $w2$ satisfies $\alpha_1$,  $\alpha_2$.

```python
from sympy import *
from sympy.logic import simplify_logic
from sympy.logic.boolalg import to_cnf, to_dnf, And, Or, Not, Implies, Equivalent
```

```python
w1_00, w1_01, w1_10, w1_11, w2_00, w2_01, w2_10, w2_11 = symbols('w1_00 w1_01 w1_10 w1_11 w2_00 w2_01 w2_10 w2_11')
```

```python
w1_constraint1 = (w1_00 & ~w1_01 & ~w1_10 & ~w1_11) | \
                    (~w1_00 & w1_01 & ~w1_10 & ~w1_11) | \
                    (~w1_00 & ~w1_01 & w1_10 & ~w1_11) | \
                    (~w1_00 & ~w1_01 & ~w1_10 & w1_11)

w2_constraint1 = (w2_00 & ~w2_01 & ~w2_10 & ~w2_11) | \
                    (~w2_00 & w2_01 & ~w2_10 & ~w2_11) | \
                    (~w2_00 & ~w2_01 & w2_10 & ~w2_11) | \
                    (~w2_00 & ~w2_01 & ~w2_10 & w2_11)

w1_constraint1, w2_constraint1
```

    ((w1_00 & ~w1_01 & ~w1_10 & ~w1_11) | (w1_01 & ~w1_00 & ~w1_10 & ~w1_11) | (w1_10 & ~w1_00 & ~w1_01 & ~w1_11) | (w1_11 & ~w1_00 & ~w1_01 & ~w1_10),
     (w2_00 & ~w2_01 & ~w2_10 & ~w2_11) | (w2_01 & ~w2_00 & ~w2_10 & ~w2_11) | (w2_10 & ~w2_00 & ~w2_01 & ~w2_11) | (w2_11 & ~w2_00 & ~w2_01 & ~w2_10))

## Semantics

We now propose new semantics directly for strict and indifference. And, derive that for weak and incomparability.

Proposed semantics:
* $(w_1, w_2) \models \alpha_1~\triangleright~\alpha_2$ iff
    either ($w_1 \models \alpha_1 \land \neg \alpha_2$ and $w_2 \models \neg \alpha_1$)
    or ($w_1 \models \alpha_1 \land \alpha_2$ and $w_2 \models \neg \alpha_1 \land \neg \alpha_2$)
    or ($w_1 \models \neg \alpha_1 \land \alpha_2$ and $w_2 \models \neg \alpha_1 \land \neg \alpha_2$)

Hence, the relation holds under three cases:
* $w_1 \in \{10\}$ and $w_2 \in \{01, 00\}$
* $w_1 \in \{11\}$ and $w_2 \in \{00\}$
* $w_1 \in \{01\}$ and $w_2 \in \{00\}$

```python
# strict_12 = (w1_10 & (w2_01 | w2_00)) | \
#               (w1_11 & (w2_00)) | \
#               (w1_01 & (w2_00))

strict_12 = (w1_10 & (w2_01 | w2_00)) | \
              (w1_01 & (w2_00))

to_dnf(strict_12)
```

$\displaystyle \left(w_{1 01} \wedge w_{2 00}\right) \vee \left(w_{1 10} \wedge w_{2 00}\right) \vee \left(w_{1 10} \wedge w_{2 01}\right)$

Proposed semantics for indifference:
* $(w_1, w_2) \models \alpha_1~\sim~\alpha_2$ iff
    either ($w_1 \models \alpha_1 \lor \alpha_2$ and $w_2 \models \alpha_1 \lor \alpha_2$)
    or ($w_1 \models \neg \alpha_1 \land \neg \alpha_2$ and $w_2 \models \alpha_1 \land \neg \alpha_2$)

Hence, the relation holds under three cases:
* $w_1 \in \{10, 01, 11\}$ and $w_2 \in \{01, 01, 11\}$

```python
indiff_12 = ((w1_01 | w1_10 | w1_11) & (w2_01 | w2_10 | w2_11)) | (w1_00 & w2_00)
to_dnf(indiff_12)
```

$\displaystyle \left(w_{1 00} \wedge w_{2 00}\right) \vee \left(w_{1 01} \wedge w_{2 01}\right) \vee \left(w_{1 01} \wedge w_{2 10}\right) \vee \left(w_{1 01} \wedge w_{2 11}\right) \vee \left(w_{1 10} \wedge w_{2 01}\right) \vee \left(w_{1 10} \wedge w_{2 10}\right) \vee \left(w_{1 10} \wedge w_{2 11}\right) \vee \left(w_{1 11} \wedge w_{2 01}\right) \vee \left(w_{1 11} \wedge w_{2 10}\right) \vee \left(w_{1 11} \wedge w_{2 11}\right)$

```python
weak_12 = strict_12 | indiff_12
to_dnf(weak_12 )
```

$\displaystyle \left(w_{1 00} \wedge w_{2 00}\right) \vee \left(w_{1 01} \wedge w_{2 00}\right) \vee \left(w_{1 01} \wedge w_{2 01}\right) \vee \left(w_{1 01} \wedge w_{2 10}\right) \vee \left(w_{1 01} \wedge w_{2 11}\right) \vee \left(w_{1 10} \wedge w_{2 00}\right) \vee \left(w_{1 10} \wedge w_{2 01}\right) \vee \left(w_{1 10} \wedge w_{2 10}\right) \vee \left(w_{1 10} \wedge w_{2 11}\right) \vee \left(w_{1 11} \wedge w_{2 01}\right) \vee \left(w_{1 11} \wedge w_{2 10}\right) \vee \left(w_{1 11} \wedge w_{2 11}\right)$

```python
strict_21 = (w1_01 & (w2_10 | w2_00)) | \
              (w1_10 & (w2_00))
to_dnf(strict_21 )
```

$\displaystyle \left(w_{1 01} \wedge w_{2 00}\right) \vee \left(w_{1 01} \wedge w_{2 10}\right) \vee \left(w_{1 10} \wedge w_{2 00}\right)$

```python
incomp_12 = (w1_00 & (w2_10 | w2_01 | w2_11)) | \
            (w1_01 & (w2_10 | w2_00 | w2_11)) | \
            (w1_10 & (w2_01 | w2_00 | w2_11)) | \
            (w1_11 & (w2_10 | w2_00 | w2_01))
to_dnf(incomp_12, simplify=True)
```

$\displaystyle \left(w_{1 00} \wedge w_{2 01}\right) \vee \left(w_{1 00} \wedge w_{2 10}\right) \vee \left(w_{1 00} \wedge w_{2 11}\right) \vee \left(w_{1 01} \wedge w_{2 00}\right) \vee \left(w_{1 01} \wedge w_{2 10}\right) \vee \left(w_{1 01} \wedge w_{2 11}\right) \vee \left(w_{1 10} \wedge w_{2 00}\right) \vee \left(w_{1 10} \wedge w_{2 01}\right) \vee \left(w_{1 10} \wedge w_{2 11}\right) \vee \left(w_{1 11} \wedge w_{2 00}\right) \vee \left(w_{1 11} \wedge w_{2 01}\right) \vee \left(w_{1 11} \wedge w_{2 10}\right)$

## Transitivity

1. $(\alpha_1 \triangleright \alpha_2) \land (\alpha_2 \triangleright \alpha_3) \implies (\alpha_1 \triangleright \alpha_3)$

```python
w1_000, w1_001, w1_010, w1_011, w1_100, w1_101, w1_110, w1_111 = symbols('w1_000, w1_001, w1_010, w1_011, w1_100, w1_101, w1_110, w1_111')
w2_000, w2_001, w2_010, w2_011, w2_100, w2_101, w2_110, w2_111 = symbols('w2_000, w2_001, w2_010, w2_011, w2_100, w2_101, w2_110, w2_111')

```

```python
# strict_12 = ((w1_100 | w1_101) & (w2_010 | w2_011 | w2_000 | w2_001)) | \
#             ((w1_110 | w1_111) & (w2_000 | w2_001)) | \
#             ((w1_010 | w1_011) & (w2_000 | w2_001))

strict_12 = ((w1_100 | w1_101) & (w2_010 | w2_011 | w2_000 | w2_001)) | \
            ((w1_010 | w1_011) & (w2_000 | w2_001))

to_dnf(strict_12)
```

$\displaystyle \left(w_{1 010} \wedge w_{2 000}\right) \vee \left(w_{1 010} \wedge w_{2 001}\right) \vee \left(w_{1 011} \wedge w_{2 000}\right) \vee \left(w_{1 011} \wedge w_{2 001}\right) \vee \left(w_{1 100} \wedge w_{2 000}\right) \vee \left(w_{1 100} \wedge w_{2 001}\right) \vee \left(w_{1 100} \wedge w_{2 010}\right) \vee \left(w_{1 100} \wedge w_{2 011}\right) \vee \left(w_{1 101} \wedge w_{2 000}\right) \vee \left(w_{1 101} \wedge w_{2 001}\right) \vee \left(w_{1 101} \wedge w_{2 010}\right) \vee \left(w_{1 101} \wedge w_{2 011}\right)$

```python
# strict_23 = ((w1_010 | w1_110) & (w2_001 | w2_101 | w2_000 | w2_100)) | \
#             ((w1_011 | w1_111) & (w2_000 | w2_100)) | \
#             ((w1_001 | w1_101) & (w2_000 | w2_100))

strict_23 = ((w1_010 | w1_110) & (w2_001 | w2_101 | w2_000 | w2_100)) | \
            ((w1_001 | w1_101) & (w2_000 | w2_100))

to_dnf(strict_23)
```

$\displaystyle \left(w_{1 001} \wedge w_{2 000}\right) \vee \left(w_{1 001} \wedge w_{2 100}\right) \vee \left(w_{1 010} \wedge w_{2 000}\right) \vee \left(w_{1 010} \wedge w_{2 001}\right) \vee \left(w_{1 010} \wedge w_{2 100}\right) \vee \left(w_{1 010} \wedge w_{2 101}\right) \vee \left(w_{1 101} \wedge w_{2 000}\right) \vee \left(w_{1 101} \wedge w_{2 100}\right) \vee \left(w_{1 110} \wedge w_{2 000}\right) \vee \left(w_{1 110} \wedge w_{2 001}\right) \vee \left(w_{1 110} \wedge w_{2 100}\right) \vee \left(w_{1 110} \wedge w_{2 101}\right)$

```python
# strict_13 = ((w1_100 | w1_110) & (w2_001 | w2_011 | w2_000 | w2_010)) | \
#             ((w1_101 | w1_111) & (w2_000 | w2_010)) | \
#             ((w1_001 | w1_011) & (w2_000 | w2_010))

strict_13 = ((w1_100 | w1_110) & (w2_001 | w2_011 | w2_000 | w2_010)) | \
            ((w1_001 | w1_011) & (w2_000 | w2_010))

to_dnf(strict_13)
```

$\displaystyle \left(w_{1 001} \wedge w_{2 000}\right) \vee \left(w_{1 001} \wedge w_{2 010}\right) \vee \left(w_{1 011} \wedge w_{2 000}\right) \vee \left(w_{1 011} \wedge w_{2 010}\right) \vee \left(w_{1 100} \wedge w_{2 000}\right) \vee \left(w_{1 100} \wedge w_{2 001}\right) \vee \left(w_{1 100} \wedge w_{2 010}\right) \vee \left(w_{1 100} \wedge w_{2 011}\right) \vee \left(w_{1 110} \wedge w_{2 000}\right) \vee \left(w_{1 110} \wedge w_{2 001}\right) \vee \left(w_{1 110} \wedge w_{2 010}\right) \vee \left(w_{1 110} \wedge w_{2 011}\right)$

```python
to_dnf((strict_12 & strict_23) >> strict_13)
```

$\displaystyle \left(w_{1 001} \wedge w_{2 000}\right) \vee \left(w_{1 001} \wedge w_{2 010}\right) \vee \left(w_{1 011} \wedge w_{2 000}\right) \vee \left(w_{1 011} \wedge w_{2 010}\right) \vee \left(w_{1 100} \wedge w_{2 000}\right) \vee \left(w_{1 100} \wedge w_{2 001}\right) \vee \left(w_{1 100} \wedge w_{2 010}\right) \vee \left(w_{1 100} \wedge w_{2 011}\right) \vee \left(w_{1 110} \wedge w_{2 000}\right) \vee \left(w_{1 110} \wedge w_{2 001}\right) \vee \left(w_{1 110} \wedge w_{2 010}\right) \vee \left(w_{1 110} \wedge w_{2 011}\right) \vee \left(\neg w_{1 001} \wedge \neg w_{1 010} \wedge \neg w_{1 101} \wedge \neg w_{1 110}\right) \vee \left(\neg w_{1 010} \wedge \neg w_{1 011} \wedge \neg w_{1 100} \wedge \neg w_{1 101}\right) \vee \left(\neg w_{1 010} \wedge \neg w_{1 110} \wedge \neg w_{2 000} \wedge \neg w_{2 100}\right) \vee \left(\neg w_{1 100} \wedge \neg w_{1 101} \wedge \neg w_{2 000} \wedge \neg w_{2 001}\right) \vee \left(\neg w_{2 000} \wedge \neg w_{2 001} \wedge \neg w_{2 010} \wedge \neg w_{2 011}\right) \vee \left(\neg w_{2 000} \wedge \neg w_{2 001} \wedge \neg w_{2 100} \wedge \neg w_{2 101}\right) \vee \left(\neg w_{1 001} \wedge \neg w_{1 101} \wedge \neg w_{2 000} \wedge \neg w_{2 001} \wedge \neg w_{2 100} \wedge \neg w_{2 101}\right) \vee \left(\neg w_{1 010} \wedge \neg w_{1 011} \wedge \neg w_{2 000} \wedge \neg w_{2 001} \wedge \neg w_{2 010} \wedge \neg w_{2 011}\right)$

Manually comparing terms not containing $\neg$, the statement holds.

2. Indifference

$(\alpha_1 \sim \alpha_2) \land (\alpha_2 \sim \alpha_3) \implies (\alpha_1 \sim \alpha_3)$

```python
w1_000, w1_001, w1_010, w1_011, w1_100, w1_101, w1_110, w1_111 = symbols('w1_000, w1_001, w1_010, w1_011, w1_100, w1_101, w1_110, w1_111')
w2_000, w2_001, w2_010, w2_011, w2_100, w2_101, w2_110, w2_111 = symbols('w2_000, w2_001, w2_010, w2_011, w2_100, w2_101, w2_110, w2_111')

```

```python
indiff_12 =   ((w1_010 | w1_011 | w1_100 | w1_101 | w1_110 | w1_111) & (w2_010 | w2_011 | w2_100 | w2_101 | w2_110 | w2_111)) | \
              ((w1_000 | w1_001) & (w2_000 | w2_001))
to_dnf(indiff_12)
```

$\displaystyle \left(w_{1 000} \wedge w_{2 000}\right) \vee \left(w_{1 000} \wedge w_{2 001}\right) \vee \left(w_{1 001} \wedge w_{2 000}\right) \vee \left(w_{1 001} \wedge w_{2 001}\right) \vee \left(w_{1 010} \wedge w_{2 010}\right) \vee \left(w_{1 010} \wedge w_{2 011}\right) \vee \left(w_{1 010} \wedge w_{2 100}\right) \vee \left(w_{1 010} \wedge w_{2 101}\right) \vee \left(w_{1 010} \wedge w_{2 110}\right) \vee \left(w_{1 010} \wedge w_{2 111}\right) \vee \left(w_{1 011} \wedge w_{2 010}\right) \vee \left(w_{1 011} \wedge w_{2 011}\right) \vee \left(w_{1 011} \wedge w_{2 100}\right) \vee \left(w_{1 011} \wedge w_{2 101}\right) \vee \left(w_{1 011} \wedge w_{2 110}\right) \vee \left(w_{1 011} \wedge w_{2 111}\right) \vee \left(w_{1 100} \wedge w_{2 010}\right) \vee \left(w_{1 100} \wedge w_{2 011}\right) \vee \left(w_{1 100} \wedge w_{2 100}\right) \vee \left(w_{1 100} \wedge w_{2 101}\right) \vee \left(w_{1 100} \wedge w_{2 110}\right) \vee \left(w_{1 100} \wedge w_{2 111}\right) \vee \left(w_{1 101} \wedge w_{2 010}\right) \vee \left(w_{1 101} \wedge w_{2 011}\right) \vee \left(w_{1 101} \wedge w_{2 100}\right) \vee \left(w_{1 101} \wedge w_{2 101}\right) \vee \left(w_{1 101} \wedge w_{2 110}\right) \vee \left(w_{1 101} \wedge w_{2 111}\right) \vee \left(w_{1 110} \wedge w_{2 010}\right) \vee \left(w_{1 110} \wedge w_{2 011}\right) \vee \left(w_{1 110} \wedge w_{2 100}\right) \vee \left(w_{1 110} \wedge w_{2 101}\right) \vee \left(w_{1 110} \wedge w_{2 110}\right) \vee \left(w_{1 110} \wedge w_{2 111}\right) \vee \left(w_{1 111} \wedge w_{2 010}\right) \vee \left(w_{1 111} \wedge w_{2 011}\right) \vee \left(w_{1 111} \wedge w_{2 100}\right) \vee \left(w_{1 111} \wedge w_{2 101}\right) \vee \left(w_{1 111} \wedge w_{2 110}\right) \vee \left(w_{1 111} \wedge w_{2 111}\right)$

```python
indiff_23 =   ((w1_001 | w1_101 | w1_010 | w1_110 | w1_011 | w1_111) & (w2_001 | w2_101 | w2_010 | w2_110 | w2_011 | w2_111)) | \
              ((w1_000 | w1_100) & (w2_000 | w2_100))
to_dnf(indiff_23)
```

$\displaystyle \left(w_{1 000} \wedge w_{2 000}\right) \vee \left(w_{1 000} \wedge w_{2 100}\right) \vee \left(w_{1 001} \wedge w_{2 001}\right) \vee \left(w_{1 001} \wedge w_{2 010}\right) \vee \left(w_{1 001} \wedge w_{2 011}\right) \vee \left(w_{1 001} \wedge w_{2 101}\right) \vee \left(w_{1 001} \wedge w_{2 110}\right) \vee \left(w_{1 001} \wedge w_{2 111}\right) \vee \left(w_{1 010} \wedge w_{2 001}\right) \vee \left(w_{1 010} \wedge w_{2 010}\right) \vee \left(w_{1 010} \wedge w_{2 011}\right) \vee \left(w_{1 010} \wedge w_{2 101}\right) \vee \left(w_{1 010} \wedge w_{2 110}\right) \vee \left(w_{1 010} \wedge w_{2 111}\right) \vee \left(w_{1 011} \wedge w_{2 001}\right) \vee \left(w_{1 011} \wedge w_{2 010}\right) \vee \left(w_{1 011} \wedge w_{2 011}\right) \vee \left(w_{1 011} \wedge w_{2 101}\right) \vee \left(w_{1 011} \wedge w_{2 110}\right) \vee \left(w_{1 011} \wedge w_{2 111}\right) \vee \left(w_{1 100} \wedge w_{2 000}\right) \vee \left(w_{1 100} \wedge w_{2 100}\right) \vee \left(w_{1 101} \wedge w_{2 001}\right) \vee \left(w_{1 101} \wedge w_{2 010}\right) \vee \left(w_{1 101} \wedge w_{2 011}\right) \vee \left(w_{1 101} \wedge w_{2 101}\right) \vee \left(w_{1 101} \wedge w_{2 110}\right) \vee \left(w_{1 101} \wedge w_{2 111}\right) \vee \left(w_{1 110} \wedge w_{2 001}\right) \vee \left(w_{1 110} \wedge w_{2 010}\right) \vee \left(w_{1 110} \wedge w_{2 011}\right) \vee \left(w_{1 110} \wedge w_{2 101}\right) \vee \left(w_{1 110} \wedge w_{2 110}\right) \vee \left(w_{1 110} \wedge w_{2 111}\right) \vee \left(w_{1 111} \wedge w_{2 001}\right) \vee \left(w_{1 111} \wedge w_{2 010}\right) \vee \left(w_{1 111} \wedge w_{2 011}\right) \vee \left(w_{1 111} \wedge w_{2 101}\right) \vee \left(w_{1 111} \wedge w_{2 110}\right) \vee \left(w_{1 111} \wedge w_{2 111}\right)$

```python
indiff_13 =   ((w1_001 | w1_011 | w1_100 | w1_110 | w1_101 | w1_111) & (w2_001 | w2_011 | w2_100 | w2_110 | w2_101 | w2_111)) | \
              ((w1_000 | w1_010) & (w2_000 | w2_010))
to_dnf(indiff_13)
```

$\displaystyle \left(w_{1 000} \wedge w_{2 000}\right) \vee \left(w_{1 000} \wedge w_{2 010}\right) \vee \left(w_{1 001} \wedge w_{2 001}\right) \vee \left(w_{1 001} \wedge w_{2 011}\right) \vee \left(w_{1 001} \wedge w_{2 100}\right) \vee \left(w_{1 001} \wedge w_{2 101}\right) \vee \left(w_{1 001} \wedge w_{2 110}\right) \vee \left(w_{1 001} \wedge w_{2 111}\right) \vee \left(w_{1 010} \wedge w_{2 000}\right) \vee \left(w_{1 010} \wedge w_{2 010}\right) \vee \left(w_{1 011} \wedge w_{2 001}\right) \vee \left(w_{1 011} \wedge w_{2 011}\right) \vee \left(w_{1 011} \wedge w_{2 100}\right) \vee \left(w_{1 011} \wedge w_{2 101}\right) \vee \left(w_{1 011} \wedge w_{2 110}\right) \vee \left(w_{1 011} \wedge w_{2 111}\right) \vee \left(w_{1 100} \wedge w_{2 001}\right) \vee \left(w_{1 100} \wedge w_{2 011}\right) \vee \left(w_{1 100} \wedge w_{2 100}\right) \vee \left(w_{1 100} \wedge w_{2 101}\right) \vee \left(w_{1 100} \wedge w_{2 110}\right) \vee \left(w_{1 100} \wedge w_{2 111}\right) \vee \left(w_{1 101} \wedge w_{2 001}\right) \vee \left(w_{1 101} \wedge w_{2 011}\right) \vee \left(w_{1 101} \wedge w_{2 100}\right) \vee \left(w_{1 101} \wedge w_{2 101}\right) \vee \left(w_{1 101} \wedge w_{2 110}\right) \vee \left(w_{1 101} \wedge w_{2 111}\right) \vee \left(w_{1 110} \wedge w_{2 001}\right) \vee \left(w_{1 110} \wedge w_{2 011}\right) \vee \left(w_{1 110} \wedge w_{2 100}\right) \vee \left(w_{1 110} \wedge w_{2 101}\right) \vee \left(w_{1 110} \wedge w_{2 110}\right) \vee \left(w_{1 110} \wedge w_{2 111}\right) \vee \left(w_{1 111} \wedge w_{2 001}\right) \vee \left(w_{1 111} \wedge w_{2 011}\right) \vee \left(w_{1 111} \wedge w_{2 100}\right) \vee \left(w_{1 111} \wedge w_{2 101}\right) \vee \left(w_{1 111} \wedge w_{2 110}\right) \vee \left(w_{1 111} \wedge w_{2 111}\right)$

```python
to_dnf((indiff_12 & indiff_23) >> indiff_13)
```

$\displaystyle \left(w_{1 000} \wedge w_{2 000}\right) \vee \left(w_{1 000} \wedge w_{2 010}\right) \vee \left(w_{1 001} \wedge w_{2 001}\right) \vee \left(w_{1 001} \wedge w_{2 011}\right) \vee \left(w_{1 001} \wedge w_{2 100}\right) \vee \left(w_{1 001} \wedge w_{2 101}\right) \vee \left(w_{1 001} \wedge w_{2 110}\right) \vee \left(w_{1 001} \wedge w_{2 111}\right) \vee \left(w_{1 010} \wedge w_{2 000}\right) \vee \left(w_{1 010} \wedge w_{2 010}\right) \vee \left(w_{1 011} \wedge w_{2 001}\right) \vee \left(w_{1 011} \wedge w_{2 011}\right) \vee \left(w_{1 011} \wedge w_{2 100}\right) \vee \left(w_{1 011} \wedge w_{2 101}\right) \vee \left(w_{1 011} \wedge w_{2 110}\right) \vee \left(w_{1 011} \wedge w_{2 111}\right) \vee \left(w_{1 100} \wedge w_{2 001}\right) \vee \left(w_{1 100} \wedge w_{2 011}\right) \vee \left(w_{1 100} \wedge w_{2 100}\right) \vee \left(w_{1 100} \wedge w_{2 101}\right) \vee \left(w_{1 100} \wedge w_{2 110}\right) \vee \left(w_{1 100} \wedge w_{2 111}\right) \vee \left(w_{1 101} \wedge w_{2 001}\right) \vee \left(w_{1 101} \wedge w_{2 011}\right) \vee \left(w_{1 101} \wedge w_{2 100}\right) \vee \left(w_{1 101} \wedge w_{2 101}\right) \vee \left(w_{1 101} \wedge w_{2 110}\right) \vee \left(w_{1 101} \wedge w_{2 111}\right) \vee \left(w_{1 110} \wedge w_{2 001}\right) \vee \left(w_{1 110} \wedge w_{2 011}\right) \vee \left(w_{1 110} \wedge w_{2 100}\right) \vee \left(w_{1 110} \wedge w_{2 101}\right) \vee \left(w_{1 110} \wedge w_{2 110}\right) \vee \left(w_{1 110} \wedge w_{2 111}\right) \vee \left(w_{1 111} \wedge w_{2 001}\right) \vee \left(w_{1 111} \wedge w_{2 011}\right) \vee \left(w_{1 111} \wedge w_{2 100}\right) \vee \left(w_{1 111} \wedge w_{2 101}\right) \vee \left(w_{1 111} \wedge w_{2 110}\right) \vee \left(w_{1 111} \wedge w_{2 111}\right) \vee \left(\neg w_{1 000} \wedge \neg w_{1 001} \wedge \neg w_{1 010} \wedge \neg w_{1 011} \wedge \neg w_{1 100} \wedge \neg w_{1 101} \wedge \neg w_{1 110} \wedge \neg w_{1 111}\right) \vee \left(\neg w_{1 000} \wedge \neg w_{1 001} \wedge \neg w_{2 010} \wedge \neg w_{2 011} \wedge \neg w_{2 100} \wedge \neg w_{2 101} \wedge \neg w_{2 110} \wedge \neg w_{2 111}\right) \vee \left(\neg w_{1 000} \wedge \neg w_{1 100} \wedge \neg w_{2 001} \wedge \neg w_{2 010} \wedge \neg w_{2 011} \wedge \neg w_{2 101} \wedge \neg w_{2 110} \wedge \neg w_{2 111}\right) \vee \left(\neg w_{1 001} \wedge \neg w_{1 010} \wedge \neg w_{1 011} \wedge \neg w_{1 101} \wedge \neg w_{1 110} \wedge \neg w_{1 111} \wedge \neg w_{2 000} \wedge \neg w_{2 100}\right) \vee \left(\neg w_{1 010} \wedge \neg w_{1 011} \wedge \neg w_{1 100} \wedge \neg w_{1 101} \wedge \neg w_{1 110} \wedge \neg w_{1 111} \wedge \neg w_{2 000} \wedge \neg w_{2 001}\right) \vee \left(\neg w_{2 000} \wedge \neg w_{2 001} \wedge \neg w_{2 010} \wedge \neg w_{2 011} \wedge \neg w_{2 100} \wedge \neg w_{2 101} \wedge \neg w_{2 110} \wedge \neg w_{2 111}\right)$

Manually comparing terms not containing $\neg$, the statement holds.

3. PI-transitivity.

$(\alpha_1 \triangleright \alpha_2) \land (\alpha_2 \sim \alpha_3) \implies (\alpha_1 \triangleright \alpha_3)$

```python
w1_000, w1_001, w1_010, w1_011, w1_100, w1_101, w1_110, w1_111 = symbols('w1_000, w1_001, w1_010, w1_011, w1_100, w1_101, w1_110, w1_111')
w2_000, w2_001, w2_010, w2_011, w2_100, w2_101, w2_110, w2_111 = symbols('w2_000, w2_001, w2_010, w2_011, w2_100, w2_101, w2_110, w2_111')

```

```python
strict_12 = ((w1_100 | w1_101) & (w2_010 | w2_011 | w2_000 | w2_001)) | \
            ((w1_010 | w1_011) & (w2_000 | w2_001))

to_dnf(strict_12)
```

$\displaystyle \left(w_{1 010} \wedge w_{2 000}\right) \vee \left(w_{1 010} \wedge w_{2 001}\right) \vee \left(w_{1 011} \wedge w_{2 000}\right) \vee \left(w_{1 011} \wedge w_{2 001}\right) \vee \left(w_{1 100} \wedge w_{2 000}\right) \vee \left(w_{1 100} \wedge w_{2 001}\right) \vee \left(w_{1 100} \wedge w_{2 010}\right) \vee \left(w_{1 100} \wedge w_{2 011}\right) \vee \left(w_{1 101} \wedge w_{2 000}\right) \vee \left(w_{1 101} \wedge w_{2 001}\right) \vee \left(w_{1 101} \wedge w_{2 010}\right) \vee \left(w_{1 101} \wedge w_{2 011}\right)$

```python
indiff_23 =   ((w1_001 | w1_101 | w1_010 | w1_110 | w1_011 | w1_111) & (w2_001 | w2_101 | w2_010 | w2_110 | w2_011 | w2_111)) | \
              ((w1_000 | w1_100) & (w2_000 | w2_100))
to_dnf(indiff_23)
```

$\displaystyle \left(w_{1 000} \wedge w_{2 000}\right) \vee \left(w_{1 000} \wedge w_{2 100}\right) \vee \left(w_{1 001} \wedge w_{2 001}\right) \vee \left(w_{1 001} \wedge w_{2 010}\right) \vee \left(w_{1 001} \wedge w_{2 011}\right) \vee \left(w_{1 001} \wedge w_{2 101}\right) \vee \left(w_{1 001} \wedge w_{2 110}\right) \vee \left(w_{1 001} \wedge w_{2 111}\right) \vee \left(w_{1 010} \wedge w_{2 001}\right) \vee \left(w_{1 010} \wedge w_{2 010}\right) \vee \left(w_{1 010} \wedge w_{2 011}\right) \vee \left(w_{1 010} \wedge w_{2 101}\right) \vee \left(w_{1 010} \wedge w_{2 110}\right) \vee \left(w_{1 010} \wedge w_{2 111}\right) \vee \left(w_{1 011} \wedge w_{2 001}\right) \vee \left(w_{1 011} \wedge w_{2 010}\right) \vee \left(w_{1 011} \wedge w_{2 011}\right) \vee \left(w_{1 011} \wedge w_{2 101}\right) \vee \left(w_{1 011} \wedge w_{2 110}\right) \vee \left(w_{1 011} \wedge w_{2 111}\right) \vee \left(w_{1 100} \wedge w_{2 000}\right) \vee \left(w_{1 100} \wedge w_{2 100}\right) \vee \left(w_{1 101} \wedge w_{2 001}\right) \vee \left(w_{1 101} \wedge w_{2 010}\right) \vee \left(w_{1 101} \wedge w_{2 011}\right) \vee \left(w_{1 101} \wedge w_{2 101}\right) \vee \left(w_{1 101} \wedge w_{2 110}\right) \vee \left(w_{1 101} \wedge w_{2 111}\right) \vee \left(w_{1 110} \wedge w_{2 001}\right) \vee \left(w_{1 110} \wedge w_{2 010}\right) \vee \left(w_{1 110} \wedge w_{2 011}\right) \vee \left(w_{1 110} \wedge w_{2 101}\right) \vee \left(w_{1 110} \wedge w_{2 110}\right) \vee \left(w_{1 110} \wedge w_{2 111}\right) \vee \left(w_{1 111} \wedge w_{2 001}\right) \vee \left(w_{1 111} \wedge w_{2 010}\right) \vee \left(w_{1 111} \wedge w_{2 011}\right) \vee \left(w_{1 111} \wedge w_{2 101}\right) \vee \left(w_{1 111} \wedge w_{2 110}\right) \vee \left(w_{1 111} \wedge w_{2 111}\right)$

```python
strict_13 = ((w1_100 | w1_110) & (w2_001 | w2_011 | w2_000 | w2_010)) | \
            ((w1_001 | w1_011) & (w2_000 | w2_010))

to_dnf(strict_13)
```

$\displaystyle \left(w_{1 001} \wedge w_{2 000}\right) \vee \left(w_{1 001} \wedge w_{2 010}\right) \vee \left(w_{1 011} \wedge w_{2 000}\right) \vee \left(w_{1 011} \wedge w_{2 010}\right) \vee \left(w_{1 100} \wedge w_{2 000}\right) \vee \left(w_{1 100} \wedge w_{2 001}\right) \vee \left(w_{1 100} \wedge w_{2 010}\right) \vee \left(w_{1 100} \wedge w_{2 011}\right) \vee \left(w_{1 110} \wedge w_{2 000}\right) \vee \left(w_{1 110} \wedge w_{2 001}\right) \vee \left(w_{1 110} \wedge w_{2 010}\right) \vee \left(w_{1 110} \wedge w_{2 011}\right)$

```python
to_dnf((strict_12 & indiff_23) >> strict_13)
```

$\displaystyle \left(w_{1 001} \wedge w_{2 000}\right) \vee \left(w_{1 001} \wedge w_{2 010}\right) \vee \left(w_{1 011} \wedge w_{2 000}\right) \vee \left(w_{1 011} \wedge w_{2 010}\right) \vee \left(w_{1 100} \wedge w_{2 000}\right) \vee \left(w_{1 100} \wedge w_{2 001}\right) \vee \left(w_{1 100} \wedge w_{2 010}\right) \vee \left(w_{1 100} \wedge w_{2 011}\right) \vee \left(w_{1 110} \wedge w_{2 000}\right) \vee \left(w_{1 110} \wedge w_{2 001}\right) \vee \left(w_{1 110} \wedge w_{2 010}\right) \vee \left(w_{1 110} \wedge w_{2 011}\right) \vee \left(\neg w_{1 010} \wedge \neg w_{1 011} \wedge \neg w_{1 100} \wedge \neg w_{1 101}\right) \vee \left(\neg w_{1 100} \wedge \neg w_{1 101} \wedge \neg w_{2 000} \wedge \neg w_{2 001}\right) \vee \left(\neg w_{2 000} \wedge \neg w_{2 001} \wedge \neg w_{2 010} \wedge \neg w_{2 011}\right) \vee \left(\neg w_{1 010} \wedge \neg w_{1 011} \wedge \neg w_{2 000} \wedge \neg w_{2 001} \wedge \neg w_{2 010} \wedge \neg w_{2 011}\right) \vee \left(\neg w_{1 000} \wedge \neg w_{1 001} \wedge \neg w_{1 010} \wedge \neg w_{1 011} \wedge \neg w_{1 100} \wedge \neg w_{1 101} \wedge \neg w_{1 110} \wedge \neg w_{1 111}\right) \vee \left(\neg w_{1 000} \wedge \neg w_{1 100} \wedge \neg w_{2 001} \wedge \neg w_{2 010} \wedge \neg w_{2 011} \wedge \neg w_{2 101} \wedge \neg w_{2 110} \wedge \neg w_{2 111}\right) \vee \left(\neg w_{1 001} \wedge \neg w_{1 010} \wedge \neg w_{1 011} \wedge \neg w_{1 101} \wedge \neg w_{1 110} \wedge \neg w_{1 111} \wedge \neg w_{2 000} \wedge \neg w_{2 100}\right) \vee \left(\neg w_{2 000} \wedge \neg w_{2 001} \wedge \neg w_{2 010} \wedge \neg w_{2 011} \wedge \neg w_{2 100} \wedge \neg w_{2 101} \wedge \neg w_{2 110} \wedge \neg w_{2 111}\right)$

## Left Disjunctive Distribution

$(\alpha_1 \triangleright \alpha_2) \land (\alpha_1 \sim \alpha_3) \implies (\alpha_1 \lor \alpha_3) \triangleright \alpha_2$

```python
w1_000, w1_001, w1_010, w1_011, w1_100, w1_101, w1_110, w1_111 = symbols('w1_000, w1_001, w1_010, w1_011, w1_100, w1_101, w1_110, w1_111')
w2_000, w2_001, w2_010, w2_011, w2_100, w2_101, w2_110, w2_111 = symbols('w2_000, w2_001, w2_010, w2_011, w2_100, w2_101, w2_110, w2_111')

```

```python
strict_12 = ((w1_100 | w1_101) & (w2_010 | w2_011 | w2_000 | w2_001)) | \
            ((w1_010 | w1_011) & (w2_000 | w2_001))

to_dnf(strict_12)
```

$\displaystyle \left(w_{1 010} \wedge w_{2 000}\right) \vee \left(w_{1 010} \wedge w_{2 001}\right) \vee \left(w_{1 011} \wedge w_{2 000}\right) \vee \left(w_{1 011} \wedge w_{2 001}\right) \vee \left(w_{1 100} \wedge w_{2 000}\right) \vee \left(w_{1 100} \wedge w_{2 001}\right) \vee \left(w_{1 100} \wedge w_{2 010}\right) \vee \left(w_{1 100} \wedge w_{2 011}\right) \vee \left(w_{1 101} \wedge w_{2 000}\right) \vee \left(w_{1 101} \wedge w_{2 001}\right) \vee \left(w_{1 101} \wedge w_{2 010}\right) \vee \left(w_{1 101} \wedge w_{2 011}\right)$

```python
indiff_13 =   ((w1_001 | w1_011 | w1_100 | w1_110 | w1_101 | w1_111) & (w2_001 | w2_011 | w2_100 | w2_110 | w2_101 | w2_111)) | \
              ((w1_000 | w1_010) & (w2_000 | w2_010))
to_dnf(indiff_13)
```

$\displaystyle \left(w_{1 000} \wedge w_{2 000}\right) \vee \left(w_{1 000} \wedge w_{2 010}\right) \vee \left(w_{1 001} \wedge w_{2 001}\right) \vee \left(w_{1 001} \wedge w_{2 011}\right) \vee \left(w_{1 001} \wedge w_{2 100}\right) \vee \left(w_{1 001} \wedge w_{2 101}\right) \vee \left(w_{1 001} \wedge w_{2 110}\right) \vee \left(w_{1 001} \wedge w_{2 111}\right) \vee \left(w_{1 010} \wedge w_{2 000}\right) \vee \left(w_{1 010} \wedge w_{2 010}\right) \vee \left(w_{1 011} \wedge w_{2 001}\right) \vee \left(w_{1 011} \wedge w_{2 011}\right) \vee \left(w_{1 011} \wedge w_{2 100}\right) \vee \left(w_{1 011} \wedge w_{2 101}\right) \vee \left(w_{1 011} \wedge w_{2 110}\right) \vee \left(w_{1 011} \wedge w_{2 111}\right) \vee \left(w_{1 100} \wedge w_{2 001}\right) \vee \left(w_{1 100} \wedge w_{2 011}\right) \vee \left(w_{1 100} \wedge w_{2 100}\right) \vee \left(w_{1 100} \wedge w_{2 101}\right) \vee \left(w_{1 100} \wedge w_{2 110}\right) \vee \left(w_{1 100} \wedge w_{2 111}\right) \vee \left(w_{1 101} \wedge w_{2 001}\right) \vee \left(w_{1 101} \wedge w_{2 011}\right) \vee \left(w_{1 101} \wedge w_{2 100}\right) \vee \left(w_{1 101} \wedge w_{2 101}\right) \vee \left(w_{1 101} \wedge w_{2 110}\right) \vee \left(w_{1 101} \wedge w_{2 111}\right) \vee \left(w_{1 110} \wedge w_{2 001}\right) \vee \left(w_{1 110} \wedge w_{2 011}\right) \vee \left(w_{1 110} \wedge w_{2 100}\right) \vee \left(w_{1 110} \wedge w_{2 101}\right) \vee \left(w_{1 110} \wedge w_{2 110}\right) \vee \left(w_{1 110} \wedge w_{2 111}\right) \vee \left(w_{1 111} \wedge w_{2 001}\right) \vee \left(w_{1 111} \wedge w_{2 011}\right) \vee \left(w_{1 111} \wedge w_{2 100}\right) \vee \left(w_{1 111} \wedge w_{2 101}\right) \vee \left(w_{1 111} \wedge w_{2 110}\right) \vee \left(w_{1 111} \wedge w_{2 111}\right)$

```python
strict_13_2 = ((w1_100 | w1_001 | w1_101) & (w2_010 | w2_000)) | \
            ((w1_010) & (w2_000))

to_dnf(strict_13_2)
```

$\displaystyle \left(w_{1 001} \wedge w_{2 000}\right) \vee \left(w_{1 001} \wedge w_{2 010}\right) \vee \left(w_{1 010} \wedge w_{2 000}\right) \vee \left(w_{1 100} \wedge w_{2 000}\right) \vee \left(w_{1 100} \wedge w_{2 010}\right) \vee \left(w_{1 101} \wedge w_{2 000}\right) \vee \left(w_{1 101} \wedge w_{2 010}\right)$

```python
to_dnf(((strict_12 & indiff_13) >> strict_13_2))
```

$\displaystyle \left(w_{1 001} \wedge w_{2 000}\right) \vee \left(w_{1 001} \wedge w_{2 010}\right) \vee \left(w_{1 010} \wedge w_{2 000}\right) \vee \left(w_{1 100} \wedge w_{2 000}\right) \vee \left(w_{1 100} \wedge w_{2 010}\right) \vee \left(w_{1 101} \wedge w_{2 000}\right) \vee \left(w_{1 101} \wedge w_{2 010}\right) \vee \left(\neg w_{1 010} \wedge \neg w_{1 011} \wedge \neg w_{1 100} \wedge \neg w_{1 101}\right) \vee \left(\neg w_{1 100} \wedge \neg w_{1 101} \wedge \neg w_{2 000} \wedge \neg w_{2 001}\right) \vee \left(\neg w_{2 000} \wedge \neg w_{2 001} \wedge \neg w_{2 010} \wedge \neg w_{2 011}\right) \vee \left(\neg w_{1 010} \wedge \neg w_{1 011} \wedge \neg w_{2 000} \wedge \neg w_{2 001} \wedge \neg w_{2 010} \wedge \neg w_{2 011}\right) \vee \left(\neg w_{1 000} \wedge \neg w_{1 001} \wedge \neg w_{1 010} \wedge \neg w_{1 011} \wedge \neg w_{1 100} \wedge \neg w_{1 101} \wedge \neg w_{1 110} \wedge \neg w_{1 111}\right) \vee \left(\neg w_{1 000} \wedge \neg w_{1 010} \wedge \neg w_{2 001} \wedge \neg w_{2 011} \wedge \neg w_{2 100} \wedge \neg w_{2 101} \wedge \neg w_{2 110} \wedge \neg w_{2 111}\right) \vee \left(\neg w_{1 001} \wedge \neg w_{1 011} \wedge \neg w_{1 100} \wedge \neg w_{1 101} \wedge \neg w_{1 110} \wedge \neg w_{1 111} \wedge \neg w_{2 000} \wedge \neg w_{2 010}\right) \vee \left(\neg w_{2 000} \wedge \neg w_{2 001} \wedge \neg w_{2 010} \wedge \neg w_{2 011} \wedge \neg w_{2 100} \wedge \neg w_{2 101} \wedge \neg w_{2 110} \wedge \neg w_{2 111}\right)$

THE PROPERTY HOLDS! Notice that clauses in `strict_12 & indiff_13) >> strict_13_2` are same as `strict_13_2`.


# Matriz de rigidez de una viga-columna

_Análisis de Estructuras. Grado en Ingeniería Civil_

Autor:

- Alejandro E. Martínez-Castro.
- Departamento de Mecánica de Estructuras e Ingeniería Hidráulica. Universidad de Granada.

Matriz de rigidez de viga-columna by Alejandro Martínez Castro is licensed under a Creative Commons Reconocimiento-NoComercial-CompartirIgual 4.0 Internacional License.

## Objetivos del cuaderno

En este cuaderno se va a obtener la matriz de rigidez de una viga-columna a compresión. En la asignatura se codifica esta matriz en términos de cuatro funciones, denominadas _funciones de estabilidad_. En este cuaderno se demuestran dichas expresiones, con ayuda de la agilidad de cálculo de un asistente simbólico como Sympy.

En este cuaderno se hace referencia al siguiente libro:

 _A. Martínez-Castro, E. Puertas, R. Gallego. "Inestabilidad de estructuras. Inestabilidad de barras comprimidas y pandeo global de estructuras". Ed. Godel._

Los objetivos son:

- Generación de las funciones de desplazamiento equivalentes a los polinomios de Hermite en flexión de vigas, pero incluyendo el comportamiento viga-columna a compresión. Estas funciones están generadas por una base trigonométrica.
- Introducción de la metodología general para obtener las denominadas "funciones de forma", que más adelante se emplearán en el Método de los Elementos Finitos.
- Determinación de los términos de las columnas de la matriz de rigidez mediante la evaluación directa de los esfuerzos cortante y flector en los extremos (teniendo en cuenta las expresiones para viga-columna), y sus relaciones con las fuerzas y momentos en los extremos de barra.

En primer lugar, cargaremos el paquete SymPy, para poder realizar cálculos simbólicos

```python
from sympy import init_session
init_session()
```

    IPython console for SymPy 1.9 (Python 3.8.12-64-bit) (ground types: gmpy)

    These commands were executed:
    >>> from __future__ import division
    >>> from sympy import *
    >>> x, y, z, t = symbols('x y z t')
    >>> k, m, n = symbols('k m n', integer=True)
    >>> f, g, h = symbols('f g h', cls=Function)
    >>> init_printing()

    Documentation can be found at https://docs.sympy.org/1.9/

## Definición de matrices de base trigonométricas y su primera derivada

Las soluciones para viga-columna sometida a carga de compresión $P$ están generadas por la base de funciones

$$\left[ 1,x,\cos \left(\dfrac{\mu}{L} x \right), \sin \left(\dfrac{\mu}{L} x \right) \right]$$

siendo $\mu = L \, \sqrt{\dfrac{P}{EI}}$, $P$ el esfuerzo axil de compresión, y $EI$ la rigidez de la viga (producto del módulo de elasticidad $E$ por el momento de inercia de la sección transversal $I$).

Definiremos a continuación los símbolos $L$ y $\mu$, y definiremos la base de funciones sobre la que se construirán las soluciones.

```python
mu, L = symbols('\mu L')
base = Matrix([[1,x,cos(mu/L*x),sin(mu/L*x)]])
base
```

Nótese que, a diferencia de las soluciones para vigas de Bernoulli-Euler, en esta ocasión el espacio de soluciones depende no sólo de $x$, que permitirá posicionar cada abscisa, sino también de $\mu$, que definirá una forma diferente (sinusoidal) para cada carga $P$. Para valores crecientes de $P$, la forma presentará mayor número de pasos por 0 para el mismo intervalo de referencia.

Definamos además una lista con la derivada primera de estas funciones de base, pues necesitaremos imponer condiciones en giros:

```python
deribase = diff(base,x)
deribase
```

## Construcción de la matriz de aplicaciones lineales asociadas a evaluar en los extremos del elemento viga-columna comprimida.

A continuación, construiremos la matriz asociada a cuatro aplicaciones lineales (por filas). En cada fila, se construirán las siguientes matrices:
- En la primera fila, la aplicación para evaluar en el origen, $x=0$.
- En la segunda fila, la aplicación para evaluar la primera derivada en el origen $x=0$.
- En la tercera fila, la aplicación para evaluar la función en $x=L$.
- En la cuarta fila, la aplicación para evaluar la derivada de la función en $x=L$.

```python
matM = Matrix.vstack(base.subs(x,0),
                     deribase.subs(x,0),
                       base.subs(x, L),
                     deribase.subs(x,L))
matM
```

La inversa de esta matriz, por columnas, contiene los cuatro coeficientes que multiplicarán a la base para construir las cuatro funciones $N1(x), N2(x), N3(x), N4(x)$.

```python
Inv_matM = matM.inv()
Inv_matM
```

En este caso, las expresiones son "farragosas". Vamos a simplificarlas.

```python
Inv_matM = simplify(Inv_matM)
Inv_matM
```

## Construcción de las funciones de forma

```python
N1 = base.dot(Inv_matM.col(0))
N2 = base.dot(Inv_matM.col(1))
N3 = base.dot(Inv_matM.col(2))
N4 = base.dot(Inv_matM.col(3))
```

Veamos alguna de ellas

```python
N1
```

## Definición de las funciones "esfuerzo cortante" y "momento flector" en la viga-columna comprimida

A continuación se definen las funciones que permiten obtener el esfuerzo cortante $\mathcal{V}(x)$ y el momento flector $\mathcal{M}(x)$, para una viga-columna comprimida. Estas funciones pueden construirse a partir de la solución para el desplazamiento $y(x)$, como:

\begin{eqnarray}
\mathcal{V}(x) & = & - EI\,y'''(x) - P \,y'\\
\mathcal{M}(x) & = & EI\,y''(x)
\end{eqnarray}

Siendo $P$ el esfuerzo axil de compresión.

Nótese que se está empleando $\mu$ en lugar de $P$ en las expresiones. Su relación es:

$$P = EI\,\dfrac{\mu^2}{L^2}$$

Nótese que estas funciones tienen signo, coherente con lo visto en la asignatura.

```python
EI = symbols('EI')
def cortante(f):
    return -EI*diff(f,x,3) - EI*mu**2/L**2 * diff(f,x) # Corrección para el cortante en una viga-columna

def flector(f):
    return EI*diff(f,x,2)
```

## Construcción de la matriz de rigidez.

La columna $j$ de la matriz de rigidez, con $j\in[1,4]$ tiene 4 términos. En los cuatro términos de las filas, se ubican las fuerzas y momentos, según el criterio de signos para fuerzas en extremos de barras (no confundir con los signos para los momentos), que deben aplicarse para que la función de desplazamiento sea $hj(x)$.

Sus relaciones con las fuerzas y momentos en los extremos de barra, $P_{iy}, M_i, P_{jy}, M_j$, son las siguientes:

\begin{eqnarray}
P_{iy} & = & - \mathcal{V}(0) \\
M_i & = & - \mathcal{M}{0} \\
P_{jy} & = & \mathcal{V}(L) \\
M_j & = & \mathcal{M}(L)
\end{eqnarray}

Considerando estas relaciones, podemos construir la matriz de rigidez, evaluando en los extremos según la relación anterior, y teniendo en cuenta las soluciones ya calculadas para las funciones $N1(x), N2(x), N3(x), N4(x)$.

Definamos una función que nos devolverá la columna correspondiente a la matriz de rigidez, dada una función de desplazamiento genérica:

```python
def columna(f):
    return Matrix([[-cortante(f).subs(x,0)],[-flector(f).subs(x,0)],[cortante(f).subs(x,L)],[flector(f).subs(x,L)]])
```

Construimos la matriz de rigidez, apilando las cuatro columnas.

```python
matriz_rigidez = Matrix.hstack(columna(N1),columna(N2),columna(N3),columna(N4))

```

```python
matriz_rigidez = simplify(matriz_rigidez)
```

```python
matriz_rigidez
```

## Comparación con solución aportada

A continuación, vamos a comprobar si esta matriz se corresponde con la matriz que se propone en el libro de la asignatura. Para esto, definamos en primer lugar los símbolos para las cuatro funciones de estabilidad, y generemos la matriz simbólica.

```python
fi1,fi2,fi3,fi4 = symbols('\phi_1 \phi_2 \phi_3 \phi_4')
```

```python
matriz_rigidez_propuesta = Matrix( [ [fi4*EI/L**3,fi1*EI/L**2,-fi4*EI/L**3,fi1*EI/L**2],
                               [fi1*EI/L**2,fi2*EI/L,-fi1*EI/L**2,fi3*EI/L],
                               [-fi4*EI/L**3,-fi1*EI/L**2,fi4*EI/L**3,-fi1*EI/L**2,],
                               [fi1*EI/L**2,fi3*EI/L,-fi1*EI/L**2,fi2*EI/L]])
```

```python
matriz_rigidez_propuesta
```

Puede comprobarse, término a término, que esta matriz se corresponde con la facilitada en el libro de la asignatura.

Vamos a continuación a reemplazar las funciones de estabilidad por sus expresiones, propuestas en el libro:

```python
deltac = 2*(1-cos(mu))-mu*sin(mu)
fi1 = mu**2 * (1 - cos(mu)) / deltac
fi2 = ( mu * sin(mu) - mu**2*cos(mu) )/deltac
fi3 = (mu**2 - mu * sin(mu))/deltac
fi4 = mu**3 * sin(mu) / deltac

mrigidez_propuesta = Matrix( [ [fi4*EI/L**3,fi1*EI/L**2,-fi4*EI/L**3,fi1*EI/L**2],
                               [fi1*EI/L**2,fi2*EI/L,-fi1*EI/L**2,fi3*EI/L],
                               [-fi4*EI/L**3,-fi1*EI/L**2,fi4*EI/L**3,-fi1*EI/L**2,],
                               [fi1*EI/L**2,fi3*EI/L,-fi1*EI/L**2,fi2*EI/L]])
```

Finalmente, compararemos ambas matrices. Una buena forma para hacerlo consiste en restar sus expresiones, y operando en modo simbólico.

```python
simplify(matriz_rigidez - mrigidez_propuesta)
```

En efecto, observamos que la diferencia es cero para todos los términos. Esto indica la matriz proporcionada en el libro de inestabilidad es correcta.

# Ejercicio propuesto:

Copie este cuaderno y modifíquelo para comprobar las expresiones de las matrices de rigidez para viga-columna sometida a un axil de tracción $T$. Recuerde que en esta ocasión, la base ya no incluye términos trigonométricos, sino trigonométricos hiperbólicos.

## Expansión en serie de la matriz de rigidez

La expansión en serie, en función de $\mu$, de esta matriz de rigidez permite linealizar dicha matriz. En Sympy es sencillo obtener expansiones en serie.

```python
series(matriz_rigidez[(0,0)],mu)
```

```python
series(matriz_rigidez[(0,1)],mu)
```

```python

```