Introducción al estudio del Método de los Elementos Finitos

Una aproximación al análisis estático lineal

Problemas 2D - Ecuación de Poisson

 Text provided under a Creative Commons Attribution license, CC-BY. All code is made available under the FSF-approved MIT license.(c) Carlos Alberto Alvarez Henao

***

***Docente:*** Carlos Alberto Álvarez Henao, I.C. D.Sc.

***e-mail:*** carlosalvarezh@gmail.com

***skype:*** carlos.alberto.alvarez.henao

***Herramienta:*** [Jupyter Notebook](http://jupyter.org/)

***Kernel:*** Python 3.8

***MEDELLÍN - COLOMBIA***

***2021/02***

***

Tabla de Contenidos

7  Aplicación del MEF a la ecuación de Poisson en 2D

7.1  Introducción

7.2  Ecuación estacionaria de Poisson en 2D

7.3  Resolución por el método de los elementos finitos

7.3.1  Forma integral del MRP

7.3.2  Discretización por el MEF

7.4  Elemento triangular de tres nodos

7.4.1  Matriz de rigidez elemental

7.4.2  Vector de flujos equivalentes nodales

7.4.3  Ejemplo - Generación de calor en un dominio cuadrado

## Aplicación del MEF a la ecuación de Poisson en 2D

### Introducción

En este capítulo se describirá la aplicación del MEF a la solución de la ecuación de Poisson en dominios bidimensionales.

Tras la presentación de las ideas fundamentales se presenta la solución por el MEF utilizando el sencillo elemento triangular de tres nodos.

### Ecuación estacionaria de Poisson en 2D

La ecuación bidimensional de Pöisson en régimen estacionario es

\begin{equation*}
A(\phi):=\frac{\partial}{\partial x}\left(k_{x} \frac{\partial \phi}{\partial x}\right)+\frac{\partial}{\partial y}\left(k_{y} \frac{\partial \phi}{\partial y}\right)+Q=0 \quad \text { en } \boldsymbol{\Omega}
\label{eq:Ec7_1} \tag{7.1}
\end{equation*}

siendo $\phi(x, y)$ la función incógnita. $k_{x}$ y $k_{y}$ son las conductividades en direcciones $x$ e $y$ respectivamente y $Q(x, y)$ es una fuente distribuida sobre el dominio $\Omega$.

Dicha ecuación se puede obtener planteando el balance de flujos para un elemento deferencial (Figura 7.1).

Figura 7.1.Definición del problema de Poisson en 2D, por Oñate,  Cálculo de Estructuras por el Método de los Elementos Finitos. Análisis Estático Lineal. Volumen 1. Sólidos.

Las condiciones de contorno pueden ser de dos tipos

\begin{equation*}
B(\phi):\left\{\begin{array}{c}
\text { Condición de variable prescrita (Dirichlet) } \\
\phi-\bar{\phi}=0 \quad \text { en } \Gamma_{\phi} \\
\text { Condición de flujo normal prescrito (Neumann) } \\
q_{n}=\bar{q}_{n}+\alpha\left(\phi-\phi_{a}^{\prime}\right)
\end{array}\right.
\label{eq:Ec7_2a} \tag{7.2a}
\end{equation*}

El flujo normal $q_{n}$ se obtiene proyectando el flujo en el contorno sobre la normal. Así

$$
q_{n}=\mathbf{n}^{T} \mathbf{q}=n_{x} q_{x}+n_{y} q_{y}
$$

Sustituyendo $q_{x}=-k_{x} \frac{\partial \phi}{\partial x}$ y $q_{y}=-k_{y} \frac{\partial \phi}{\partial y}$ en las ecuaciones anteriores se obtiene la condición de flujo prescrito en el contorno (condición de Neumann) en la forma usual siguiente

\begin{equation*}
k_{x} \frac{\partial \phi}{\partial x} n_{x}+k_{y} \frac{\partial \phi}{\partial y} n_{y}+\bar{q}_{n}+\alpha\left(\phi-\bar{\phi}_{a}\right)=0 \quad \text { en } \Gamma_{q}
\label{eq:Ec7_2b} \tag{7.2b}
\end{equation*}

En lo anterior $\bar{q}_{n}$ es el flujo prescrito en dirección normal al contorno $\Gamma_{q}$ ( $\bar{q}_{n}$ es positivo, si el flujo es en dirección de la normal al contorno $\left.\mathbf{n}\right)$. El último término de la [Ec. 7.2b](#Ec7_2b) expresa el flujo de calor que sale por el contorno debido a una diferencia de temperatura entre los puntos del contorno y el ambiente a temperatura $\phi_{a} .$ Asimismo, $\alpha$ es el denominado coeficiente de convección.

Adviértase que

\begin{equation*}
\Gamma=\Gamma_{\phi} \cup \Gamma_{q}
\label{eq:Ec7_3} \tag{7.3}
\end{equation*}

La división de $\Gamma$ en dos zonas $\Gamma_{\phi}$ y $\Gamma_{q}$ es meramente conceptual, puesto que puede darse la situación de que estas condiciones se vayan alternando en los sucesivos puntos del contorno.

### Resolución por el método de los elementos finitos

Resolveremos el problema definido por las [Ecs. 7.1 y 7.2](#Ec7_1) en modo similar al caso unidimensional, aplicando el método de *residuos ponderados tipo Galerkin*.

#### Forma integral del MRP

La forma integral equivalente a las ecuaciones diferenciales se obtiene por el método de residuos ponderados (MRP). Así, multiplicando las ecs. [7.1](#Ec7_1) y [7.2](#Ec7_2) por sendas funciones de peso $W$ y $\bar{W}$ e integrando sobre el dominio y el contorno $\Gamma_{q}$, respectivamente, se obtiene

\begin{equation*}
\begin{aligned}
&\iint_{\Omega} W\left[\frac{\partial}{\partial x}\left(k_{x} \frac{\partial \phi}{\partial x}\right)+\frac{\partial}{\partial y}\left(k_{y} \frac{\partial \phi}{\partial y}\right)+Q\right] \mathrm{d} x \mathrm{d} y+ \\
&+\int_{\Gamma_{q}} W\left[k_{x} \frac{\partial \phi}{\partial x} n_{x}+k_{y} \frac{\partial \phi}{\partial y} n_{y}+\bar{q}_{n}+\alpha\left(\phi-\phi_{a}\right)\right] \mathrm{d} T=0
\end{aligned}
\label{eq:Ec7_4} \tag{7.4}
\end{equation*}

La forma débil se obtiene integrando por partes los términos en derivadas segundas en [Ec. 7.4](#Ec7_4). Así

\begin{equation*}
\begin{aligned}
\begin{gathered}
\iint_{\Omega} W \frac{\partial}{\partial x}\left(k_{x} \frac{\partial \phi}{\partial x}\right) \mathrm{d} x \mathrm{d} y=\int \mathrm{d} y \int W \frac{\partial}{\partial x}\left(k_{x} \frac{\partial \phi}{\partial x}\right) \mathrm{d} x= \\
=-\iint_{\Omega} \frac{\partial W}{\partial x} k_{x} \frac{\partial \phi}{\partial x} \mathrm{d} x \mathrm{d} y+\int_{\Gamma=\Gamma_{q}+\Gamma_{\phi}} W k_{x} \frac{\partial \phi}{\partial x} \mathrm{d} y
\end{gathered}
\end{aligned}
\label{eq:Ec7_5} \tag{7.5}
\end{equation*}

Realizando una operación similar para la integral de $\frac{\partial}{\partial y}\left(k_{x} \frac{\partial \phi}{\partial y}\right)$, se obtiene tras sustituir en [Ec. 7.4](#Ec7_4)

\begin{equation*}
\begin{aligned}
&\iint_{\Omega} W\left[\frac{\partial}{\partial x}\left(k_{x} \frac{\partial \phi}{\partial x}\right)+\frac{\partial}{\partial y}\left(k_{y} \frac{\partial \phi}{\partial y}\right)+Q\right] \mathrm{d} x \mathrm{d} y= \\
&\quad=-\iint_{\Omega}\left[\frac{\partial W}{\partial x} k_{x} \frac{\partial \phi}{\partial x}+\frac{\partial W}{\partial y} k_{y} \frac{\partial \phi}{\partial y}\right] \mathrm{d} x \mathrm{d} y+\iint_{\Omega} W Q \mathrm{d} x \mathrm{d} y+ \\
&\quad+\int_{\Gamma_{q}+\Gamma_{\phi}} W\left[k_{x} \frac{\partial \phi}{\partial x} \mathrm{d} y+k_{y} \frac{\partial \phi}{\partial y} \mathrm{d} x\right]
\end{aligned}
\label{eq:Ec7_6} \tag{7.6}
\end{equation*}

Figura 7.2.Definición de la normal al contorno, por Oñate,  Cálculo de Estructuras por el Método de los Elementos Finitos. Análisis Estático Lineal. Volumen 1. Sólidos.

Teniendo en cuenta que (de la Figura 7.2)

\begin{equation*}
\begin{aligned}
&\mathrm{d} y=\mathrm{d} \Gamma \cos \alpha=d \Gamma n_{x} \\
&\mathrm{d} x=\mathrm{d} \Gamma \cos \beta=d \Gamma n_{y}
\end{aligned}
\label{eq:Ec7_7} \tag{7.7}
\end{equation*}

y adoptando $W=-W$, tras sustituir en [Ec. 7.6](#Ec7_6), se llega a

\begin{equation*}
\begin{aligned}
&-\iint_{\Omega}\left[\frac{\partial W}{\partial x} k_{x} \frac{\partial \phi}{\partial x}+\frac{\partial W}{\partial y} k_{y} \frac{\partial \phi}{\partial y}\right] \mathrm{d} x \mathrm{d} y+\iint_{\Omega} W Q \mathrm{d} x \mathrm{d} y+ \\
&+\int_{\Gamma_{q}+\Gamma_{\phi}} W\left[k_{x} \frac{\partial \phi}{\partial x} n_{x}+k_{y} \frac{\partial \phi}{\partial y} n_{y}\right] \mathrm{d} \Gamma- \\
&-\int_{\Gamma_{q}} W\left[k_{x} \frac{\partial \phi}{\partial x} n_{x}+k_{y} \frac{\partial \phi}{\partial y} n_{y}+\bar{q}_{n}+\alpha\left(\phi-\phi_{a}\right)\right] \mathrm{d} \Gamma=0
\end{aligned}
\label{eq:Ec7_8} \tag{7.8}
\end{equation*}

en la que los términos subrayados se cancelan entre sí mutuamente.

La forma integral resultante es

\begin{equation*}
\begin{aligned}
&-\iint_{\Omega}\left[\frac{\partial W}{\partial x} k_{x} \frac{\partial \phi}{\partial x}+\frac{\partial W}{\partial y} k_{y} \frac{\partial \phi}{\partial y}\right] \mathrm{d} \Omega+\iint_{\Omega} W Q \mathrm{d} \Omega+ \\
&\left.+\int_{\Gamma_{\phi}} W\left[k_{x} \frac{\partial \phi}{\partial x} n_{x}+k_{y} \frac{\partial \phi}{\partial y} n_{y}\right)\right] \mathrm{d} \Gamma-\int_{\Gamma_{\phi}}\left[\bar{q}_{n}+\alpha\left(\phi-\phi_{a}\right)\right] \mathrm{d} \Gamma=0
\end{aligned}
\label{eq:Ec7_9} \tag{7.9}
\end{equation*}

Teniendo en cuenta que

\begin{equation*}
\begin{aligned}
q_{n}=q_{x} n_{x}+q_{y} n_{y}=-k_{x} \frac{\partial \phi}{\partial x} n_{x}-k_{y} \frac{\partial \phi}{\partial y} n_{y}
\end{aligned}
\label{eq:Ec7_10} \tag{7.10}
\end{equation*}

Agrupando los términos en los que interviene $\phi$ en el primer miembro y teniendo en cuenta la expresión de $q_{n}$ anterior, se obtiene finalmente

\begin{equation*}
\begin{aligned}
&\iint_{\Omega}\left[\frac{\partial W}{\partial x} k_{x} \frac{\partial \phi}{\partial x}+\frac{\partial W}{\partial y} k_{y} \frac{\partial \phi}{\partial y}\right] \mathrm{d} \Omega+\int_{\Gamma_{q}} \alpha W \phi \mathrm{d} \Gamma= \\
&=\iint_{\Omega} W Q \mathrm{d} \Omega-\int_{\Gamma_{q}} W\left[\vec{q}_{n}-\alpha \phi_{a}\right] \mathrm{d} \Gamma-\int_{\Gamma_{\phi}} W q_{n} \mathrm{d} \Gamma
\end{aligned}
\label{eq:Ec7_11} \tag{7.11}
\end{equation*}

La forma débil [(Ec. 7.11)](#Ec7_11) es el punto de partida para obtener las ecuaciones de la aproximación por el MEF.

#### Discretización por el MEF

La última integral en [Ec. 7.11](#Ec7_11) se debe al flujo $q_{n}$ que sale por el contorno $\Gamma_{\phi}$ donde el valor de $\phi$ es conccido. Adviértase que el signo que precede la integral es negativo, lo que indica que el flujo normal $q_{n}$ sale del contorno. Como en el problema unidimensional, dicho flujo puede interpretarse como una "reacción" que se calcula una vez conocido el valor de $\phi$ sobre $\Gamma_{\phi}$.

Figura 7.3.Discretización de  un dominio 2D, por Oñate,  Cálculo de Estructuras por el Método de los Elementos Finitos. Análisis Estático Lineal. Volumen 1. Sólidos.

Supongamos ahora una discretización del dominio $\Omega$ en elementos finitos bidimensionales de $n$ nodos (Figura 7.3). La aproximación de $\phi$ en el interior de cada elemento se define en la forma usual por

\begin{equation*}
\begin{aligned}
\phi \cong \hat{\phi}=\sum_{i=1}^{n} N_{i}^{(e)}(x, y) \phi_{i}^{(e)}
\end{aligned}
\label{eq:Ec7_12} \tag{7.12}
\end{equation*}

donde $N_{i}^{(e)}(x, y)$ son las runciones de forma bidimensional y $\phi^{(e)}$ los valores nodales de la solución aproximada.

El sistema de ecuaciones de la discretización se obtiene sustituyendo la aproximación [Ec. 7.12](#Ec7_12) en la forma débil [Ec. 7.11](#Ec7_11) y escogiendo $N$ funciones de peso $W_{i} .$ Así

\begin{equation*}
\begin{aligned}
&\iint_{\Omega}\left[\frac{\partial W_{i}}{\partial x} k_{x} \frac{\partial \hat{\phi}}{\partial x}+\frac{\partial W_{i}}{\partial y} k_{y} \frac{\partial \hat{\phi}}{\partial y}\right] \mathrm{d} \Omega+\int_{\Gamma_{q}} \alpha W_{i} \hat{\phi} \mathrm{d} \Omega= \\
&\quad=\iint_{\Omega} W_{i} Q \mathrm{d} \Omega-\int_{\Gamma_{q}} W_{i}\left[\bar{q}_{n}-\alpha \phi_{a}\right] \mathrm{d} \Gamma-\int_{\Gamma_{\phi}} W_{i} q_{n} \mathrm{d} \Gamma \quad i=1, \ldots, N
\end{aligned}
\label{eq:Ec7_13} \tag{7.13}
\end{equation*}

siendo $N$ el número total de nodos en la malla.

La forma de Galerkin se obtiene haciendo $W_{i}=N_{i}$. Suponiendo que todas las funciones que aparecen en [Ec. 7.13](#Ec7_13) son integrables y teniendo en cuenta la propiedad aditiva de la integral, puede expresarse la forma de Galerkin en función de las contribuciones elementales por

\begin{equation*}
\begin{aligned}
&\sum_{e} \iint_{\Omega^{(e)}}\left[\frac{\partial N_{i}^{(e)}}{\partial x} k_{x}\left(\sum_{j=1}^{n} \frac{\partial N_{j}^{(e)}}{\partial x} \phi_{j}^{(e)}\right)+\frac{\partial N_{i}^{(e)}}{\partial y} k_{y}\left(\sum_{j=1}^{n} \frac{\partial N_{j}^{(e)}}{\partial y} \phi_{j}^{(e)}\right)\right] \mathrm{d} \Omega+ \\
&\sum_{e} \int_{\Gamma_{q}^{(e)}} \alpha N_{i}^{(e)}\left(\sum_{j=1}^{n} N_{j}^{(e)} \phi_{j}^{(e)}\right) \mathrm{d} \Gamma= \\
&\quad=\sum_{e} \iint_{\Omega^{(e)}} N_{i}^{(e)} Q \mathrm{d} \Omega-\sum_{e} \int_{\Gamma_{q}^{(e)}} N_{i}^{(e)}\left[\bar{q}_{n}-\alpha \phi_{a}\right] \mathrm{d} \Gamma-\sum_{e} \int_{\Gamma_{\phi}^{(e)}} N_{i}^{(e)} q_{n} \mathrm{d} \Gamma
\end{aligned}
\label{eq:Ec7_14} \tag{7.14}
\end{equation*}

donde el sumatorio se extiende sobre todos los elementos del dominio.

La expresión [Ec. 7.14](#Ec7_14) puede escribirse en la forma matricial estándar

\begin{equation*}
\begin{aligned}
\mathbf{K a}=\mathbf{f}
\end{aligned}
\label{eq:Ec7_15} \tag{7.15}
\end{equation*}

Los términos de $\mathbf{K}$ y $\mathbf{f}$ se obtienen ensamblando las contribuciones elementales en la forma usual.

Asi, advirtiendo que las funciones de forma $N_{i}^{(e)}$ toman un valor nulo fuera de cada elemento, puede deducirse que

\begin{equation*}
\begin{aligned}
K_{i j}^{(e)}=& \iint_{\Omega^{(e)}}\left(\frac{\partial N_{i}^{(e)}}{\partial x} k_{x} \frac{\partial N_{j}^{(\epsilon)}}{\partial x}+\frac{\partial N_{i}^{(e)}}{\partial y} k_{y} \frac{\partial N_{j}^{(e)}}{\partial y}\right) \mathrm{d} \Omega+\\
&+\int_{\Gamma_{q}^{(e)}} \alpha N_{i}^{(e)} N_{j}^{(e)} d \Gamma=K_{d_{i j}}+K_{c_{i j}}
\end{aligned}
\label{eq:Ec7_16} \tag{7.16}
\end{equation*}

\begin{equation*}
\begin{aligned}
f_{i}^{(e)}=\iint_{\Omega^{(e)}} N_{i}^{(e)} Q \mathrm{d} \Omega-\int_{\Gamma_{q}^{(e)}} N_{i}^{(e)}\left[\bar{q}_{n}-\alpha \phi_{a}\right] \mathrm{d} \Gamma-\int_{\Gamma_{\phi}^{(e)}} N_{i}^{(e)} q_{n} d \Gamma
\end{aligned}
\label{eq:Ec7_17} \tag{7.17}
\end{equation*}

Es importante advertir que las contribuciones de cada elemento provenientes de las integrales sobre los contornos elementales se cancelan entre sí, cuando el elemento pertenece al interior del dominio. Los términos $K_{d_{k j}}^{(e)}$ y $K_{c_{i j}}^{(c)}$ en [Ec. 7.16](#Ec7_16) represeutan las contribuciones de la conductividad y la convección en la matriz de rigidez elemental. Dichos términos se obtienen por

\begin{equation*}
\begin{aligned}
K_{d_{i j}}^{(e)}=\iint_{\Omega^{(\epsilon)}}\left[\frac{\partial N_{i}^{(e)}}{\partial x} k_{x} \frac{\partial N_{j}^{(\epsilon)}}{\partial x}+\frac{\partial N_{i}^{(())}}{\partial y} k_{y} \frac{\partial N_{j}^{(\epsilon)}}{\partial y}\right] \mathrm{d} \Omega
\end{aligned}
\label{eq:Ec7_18} \tag{7.18}
\end{equation*}

\begin{equation*}
\begin{aligned}
K_{c_{i j}}^{(e)}=\int_{\Gamma_{q}^{(\epsilon)}} \alpha N_{i}^{(e)} N_{j}^{(e)} \mathrm{d} \Gamma
\end{aligned}
\label{eq:Ec7_19} \tag{7.19}
\end{equation*}

El cálculo por separado de $K_{d_{i j}}^{(e)}$ y $K_{c_{i j}}^{(e)}$ facilita la obtención de los coeficientes de la matriz de rigidez elemental.

De la misma manera, es útil separar los componentes del vector $f_{i}^{(e)}$ como

\begin{equation*}
\begin{aligned}
f_{i}^{(e)}=f_{Q_{i}}^{(e)}+f_{c_{i}}^{(e)}+q_{n_{i}}
\end{aligned}
\label{eq:Ec7_20} \tag{7.20}
\end{equation*}

donde

\begin{equation*}
\begin{aligned}
f_{Q_{i}}^{(e)}=\iint_{\Omega^{(e)}} N_{i}^{(e)} Q \mathrm{d} \Omega
\end{aligned}
\label{eq:Ec7_21} \tag{7.21}
\end{equation*}

es la contribución del término de fuente de calor sobre el dominio

\begin{equation*}
\begin{aligned}
f_{i}^{(e)}=-\int_{\Gamma_{\phi}^{(e)}} N_{i}^{(e)}\left[\bar{q}_{n}-\alpha \phi_{a}\right] \mathrm{d} \Omega
\end{aligned}
\label{eq:Ec7_22} \tag{7.22}
\end{equation*}

es la contribución del tic rnino de flujo de calor a través del contorno de Neumann donde se prescribe el flujo saliente y

\begin{equation*}
\begin{aligned}
q_{n_{1}}^{(e)}=-\int_{\Gamma_{\phi}^{(e)}} N_{i}^{(e)} q_{n} \mathrm{d} \Gamma
\end{aligned}
\label{eq:Ec7_23} \tag{7.23}
\end{equation*}

es el término de flujo "reacción" sobre el contorno de $\Gamma_{\phi}$ donde se prescribe la variable de $\phi$. Dicho flujo se calcula "a posteriori", una vez obtenidos los valores nodales de $\phi .$

Se recuerda de nuevo que las integrales sobre los contornos elementales en [Ec. 7.17](#Ec7_17) sólo contribuyen al vector de fuerzas globales en el caso de que el elemento en cuestión tenga uno o más de sus lados sobre los contornos externos $\Gamma_{q} \circ \Gamma_{\phi}$

Particularizaremos ahora el proceso de obtención de las matrices y vectores anteriores para el sencillo elemento triangular de tres nodos.

### Elemento triangular de tres nodos

Supongamos una discretización del dominio de análisis en elementos triangulares de tres nodos ([Figura 7.3](#Fig7_3)).

Cada nodo del elemento tiene un grado de libertad, lo cual corresponde al valor de la incógnita $\phi$ en el nodo.

La variación de la incógnita se expresa por el polinomio lineal siguiente

\begin{equation*}
\begin{aligned}
\phi(x, y)=\alpha_{0}+\alpha_{1} x+\alpha_{2} y
\end{aligned}
\label{eq:Ec7_24} \tag{7.24}
\end{equation*}

Para hallar los coeficientes $\alpha_{0}, \alpha_{1}$, y $\alpha_{2}$, basta sustituir las coordenadas nodales en [Ec. 7.24](#Ec7_24), se obtiene

\begin{equation*}
\begin{aligned}
&\phi_{1}^{(e)}=\alpha_{0}+\alpha_{1} x_{1}^{(e)}+\alpha_{2} y_{1}^{(e)} \\
&\phi_{2}^{(e)}=\alpha_{0}+\alpha_{1} x_{2}^{(e)}+\alpha_{2} y_{2}^{(e)} \\
&\phi_{3}^{(e)}=\alpha_{0}+\alpha_{1} x_{3}^{(e)}+\alpha_{3} y_{3}^{(e)}
\end{aligned}
\label{eq:Ec7_25} \tag{7.25}
\end{equation*}

Resolviendo el sistema y sustituyendo los valores obtenidos de $\alpha_{i}$ en [Ec. 7.24](#Ec7_24) se obtiene, tras operar

\begin{equation*}
\begin{aligned}
\phi(x, y)=\frac{1}{2 A^{(\sigma)}}\left[\left(a_{1}^{(e)}+b_{1}^{(e)} x+c_{1}^{(e)} y\right) \phi_{1}^{(e)}+\left(a_{2}^{(e)}+b_{2}^{(e)} x+c_{2}^{(e)} y\right) \phi_{2}^{(e)}+\right.\\
\left.+\left(a_{3}^{(e)}+b_{3}^{(e)} x+c_{3}^{(e)} y\right) \phi_{3}^{(e)}\right]
\end{aligned}
\label{eq:Ec7_26} \tag{7.26}
\end{equation*}

en donde

\begin{equation*}
\begin{aligned}
&a_{i}^{(e)}=x_{j}^{(e)} y_{k}^{(e)}-x_{k}^{(e)} y_{j}^{(e)} \\
&b_{i}^{(e)}=y_{j}^{(e)}-y_{k}^{(e)} \\
&c_{i}^{(e)}=x_{k}^{(e)}-x_{j}^{(c)}
\end{aligned}
\label{eq:Ec7_27} \tag{7.27}
\end{equation*}

Los subíndices $i, j, k$ varían ve 1 a 3 ([Figura 7.3](#Fig7_3)). Permutando $i, j, k$ cíclicamente se pueden hallar las expresiones de cada uno de los parámetros.

El área del elemento $A^{(e)}$ se obtiene del determinante de los coeficientes de [Ec. 7.25](#Ec7_25), siendo

\begin{equation*}
\begin{aligned}
2 A^{(e)}=\left|\begin{array}{lll}
1 & x_{i}^{(e)} & y_{i}^{(e)} \\
1 & x_{j}^{(e)} & y_{j}^{(e)} \\
1 & x_{k}^{(e)} & y_{k}^{(e)}
\end{array}\right|
\end{aligned}
\label{eq:Ec7_28} \tag{7.28}
\end{equation*}

La ec. [Ec. 7.26](#Ec7_26) se puede compactar como

\begin{equation*}
\begin{aligned}
\phi=\sum_{i=1}^{3} N_{i}^{(e)} \phi_{i}^{(e)}=N_{1}^{(e)} \phi_{1}^{(e)}+N_{2}^{(e)} \phi_{2}^{(e)}+N_{3}^{(e)} \phi_{3}^{(e)}
\end{aligned}
\label{eq:Ec7_29} \tag{7.29}
\end{equation*}

la cual permite identificar las funciones de forma de. elemento por

\begin{equation*}
\begin{aligned}
N_{i}^{(e)}(x, y)=\frac{1}{2 A^{(e)}}\left(a_{i}^{(e)}+b_{i}^{(e)} x+c_{i}^{(e)} y\right) ; \quad i=1,2,3
\end{aligned}
\label{eq:Ec7_30} \tag{7.30}
\end{equation*}

La función de forma de un nodo, como es usual, toma un valor unidad en un nodo y cero en los restantes (Figura 7.4).

Figura 7.4.Función de forma triángulo de tres nodos, por Oñate,  Cálculo de Estructuras por el Método de los Elementos Finitos. Análisis Estático Lineal. Volumen 1. Sólidos.

Asimismo, se destaca que la función de forma global de un nodo vale uno en el nodo y cero en todos los restantes. Como en el caso unidimensional, el dominio de influencia de la función de forma global de un nodo abarca todos los elementos que rodean al nodo.

#### Matriz de rigidez elemental

De la [Ec. 7.30](#Ec7_30) se deduce

\begin{equation*}
\begin{aligned}
\frac{\partial N_{i}^{(e)}}{\partial x}=\frac{b_{i}}{2 A^{(e)}} ; \quad \frac{\partial N_{i}^{(e)}}{\partial y}=\frac{c_{i}}{2 A^{(e)}}
\end{aligned}
\label{eq:Ec7_31} \tag{7.31}
\end{equation*}

Sustituyendo [Ec. 7.30](#Ec7_30) y [Ec. 7.31](#Ec7_31) en [Ec. 7.18](#Ec7_18) y [Ec. 7.19](#Ec7_19), se obtiene

\begin{equation*}
\begin{aligned}
K_{d_{i j}}^{(e)}=\iint_{\Omega^{(e)}} \frac{1}{4 A^{(e) 2}}\left[k_{x} b_{i} b_{j}+k_{y} c_{i} c_{j}\right] \mathrm{d} \Omega=\frac{1}{4 A^{(e)}}\left[k_{x} b_{i} b_{j}+k_{y} c_{i} c_{j}\right]
\end{aligned}
\label{eq:Ec7_32} \tag{7.32}
\end{equation*}

y la matriz $\mathbf{K}_{d}^{(e)}$ se obtiene por

\begin{equation*}
\begin{aligned}
\mathbf{K}_{d}^{(c)}=\frac{1}{4 A^{(e)}}\left[\begin{array}{lll}
k_{x} b_{1} b_{1}+k_{y} c_{1} c_{1} & k_{x} b_{1} b_{2}+k_{y} c_{1} c_{2} & k_{x} b_{1} b_{3}+k_{y} c_{1} c_{3} \\
k_{z} b_{2} b_{1}+k_{y} c_{2} c_{1} & k_{x} b_{2} b_{2}+k_{y} c_{2} c_{2} & k_{x} b_{2} b_{3}+k_{y} c_{2} c_{3} \\
k_{z} b_{3} b_{1}+k_{y} c_{3} c_{1} & k_{x} b_{3} b_{2}+k_{y} c_{3} c_{2} & k_{x} b_{3} b_{3}+k_{y} c_{3} c_{3}
\end{array}\right]^{(e)}
\end{aligned}
\label{eq:Ec7_33} \tag{7.33}
\end{equation*}

La evaluación de la integral de [Ec. 7.19](#Ec7_19) proporciona la matriz de rigidez de convección por

\begin{equation*}
\begin{aligned}
\mathbf{K}_{c}^{(e)}=\int_{\Gamma_{q}^{(e)}} \alpha\left[\begin{array}{lll}
N_{1}^{(e)} N_{1}^{(e)} & N_{1}^{(e)} N_{2}^{(e)} & N_{1}^{(e)} N_{3}^{(e)} \\
N_{2}^{(e)} N_{1}^{(e)} & N_{2}^{(e)} N_{2}^{(e)} & N_{2}^{(e)} N_{3}^{(e)} \\
N_{3}^{(e)} N_{1}^{(e)} & N_{3}^{(e)} N_{2}^{(e)} & N_{3}^{(e)} N_{3}^{(e)}
\end{array}\right]^{(e)} \mathrm{d} \Gamma
\end{aligned}
\label{eq:Ec7_34} \tag{7.34}
\end{equation*}

Es imporante advertir que al realizar la integral anterior sobre un lado del elemento (sobre el contorno), la función de forma del tercer nodo que no pertenece al lado forma un valor nulo sobre el mismo. Ello permite simplificar la expresión (4.34) de acuerdo con el lado donde se calcula la integral de contorno.

Así, para el $lado$ $12$, $N_{3}^{(e)}=0 \mathrm{y}$

\begin{equation*}
\begin{aligned}
K_{c_{12}}^{(e)}=\int_{l_{12}^{(e)}} \alpha\left[\begin{array}{ccc}
N_{1}^{(e)} N_{1}^{(e)} & N_{1}^{(e)} N_{2}^{(e)} & 0 \\
N_{2}^{(e)} N_{1}^{(e)} & N_{2}^{(e)} N_{2}^{(e)} & 0 \\
0 & 0 & 0
\end{array}\right] \mathrm{d}  \mathbf{\Gamma}
\end{aligned}
\label{eq:Ec7_35} \tag{7.35}
\end{equation*}

Para el $lado$ $23$

\begin{equation*}
\begin{aligned}
K_{c_{23}}^{(e)}=\int_{l_{23}^{(e)}} \alpha\left[\begin{array}{ccc}
0 & 0 & 0 \\
0 & N_{2}^{(e)} N_{2}^{(e)} & N_{2}^{(e)} N_{3}^{(e)} \\
0 & N_{3}^{(e)} N_{2}^{(e)} & N_{3}^{(e)} N_{3}^{(e)}
\end{array}\right] \mathrm{d} \Gamma
\end{aligned}
\label{eq:Ec7_36} \tag{7.36}
\end{equation*}

Para el $lado$ $13$

\begin{equation*}
\begin{aligned}
K_{c_{13}}^{(e)}=\int_{l_{13}^{(e)}} \alpha\left[\begin{array}{ccc}
N_{1}^{(e)} N_{1}^{(e)} & 0 & N_{1}^{(e)} N_{3}^{(e)} \\
0 & 0 & 0 \\
N_{1}^{(e)} N_{3}^{(e)} & 0 & N_{3}^{(e)} N_{3}^{(e)}
\end{array}\right] d \Gamma
\end{aligned}
\label{eq:Ec7_37} \tag{7.37}
\end{equation*}

Suponiendo que $\alpha$ es constante sobre el lado es fácil integrar los términos anteriores, lo que proporciona

\begin{equation*}
\begin{aligned}
\int_{l_{i j}^{(e)}} N_{i}^{(e)} N_{j}^{(e)} \mathrm{d} \Gamma= \begin{cases}l_{i j}^{(e)} / 3 ; & i=j \\ l_{i j}^{(e)} / 6 ; & i \neq j\end{cases}
\end{aligned}
\label{eq:Ec7_38} \tag{7.38}
\end{equation*}

siendo $l_{i j}^{(e)}$ la longitud del lado $i-j$ del elemento $(e)$.
En función de ello, se tiene

\begin{equation*}
\begin{aligned}
K_{c_{12}}^{(\mathrm{e})}=\frac{\alpha l_{12}^{(\mathrm{e})}}{6}\left[\begin{array}{lll}
2 & 1 & 0 \\
1 & 2 & 0 \\
0 & 0 & 0
\end{array}\right] ; \quad K_{c_{23}}^{(e)}=\frac{\alpha l_{23}^{(e)}}{6}\left[\begin{array}{lll}
0 & 0 & 0 \\
0 & 2 & 1 \\
0 & 1 & 2
\end{array}\right] ; \quad K_{c_{13}}^{(e)}=\frac{\alpha l_{13}^{(e)}}{6}\left[\begin{array}{lll}
2 & 0 & 1 \\
0 & 0 & 0 \\
1 & 0 & 2
\end{array}\right]
\end{aligned}
\label{eq:Ec7_39} \tag{7.39}
\end{equation*}

Por lo tanto, en definitiva, la matriz de rigidez total del elemento se obtiene por

\begin{equation*}
\begin{aligned}
\mathbf{K}^{(e)}=\mathbf{K}_{d}^{(e)}+\mathbf{K}_{c_{12}}^{(e)}+\mathbf{K}_{c_{23}}^{(e)}+\mathbf{K}_{c_{13}}^{(e)}
\end{aligned}
\label{eq:Ec7_40} \tag{7.40}
\end{equation*}

Recordemos que la contribución de las matrices $\mathbf{K}_{\text {cij }}^{(e)}$ sólo es relevante si el lado $i j$ pertenece al contorno exterior $\Gamma_{q}$.

#### Vector de flujos equivalentes nodales

La expresión genérica de $\mathbf{f}^{(e)}$ para el triángulo de tres nodos es

\begin{equation*}
\begin{aligned}
\mathbf{f}^{(e)}-\left\{\begin{array}{l}
f_{1}^{(e)} \\
f_{2}^{(e)} \\
f_{3}^{(e)}
\end{array}\right\}
\end{aligned}
\label{eq:Ec7_41} \tag{7.41}
\end{equation*}

Obtengamos segufdamente las contribuciones de los términos de fuente y de convección a
$\mathrm{f}^{(e)}$.

De [Ec. 7.21](#Ec7_21) se deduce

\begin{equation*}
\begin{aligned}
\mathbf{f}_{Q}^{(e)}=\left\{\begin{array}{l}
f_{Q_{1}}^{(e)} \\
f_{Q_{2}}^{(e)} \\
f_{Q_{3}}^{(e)}
\end{array}\right\}=\iint_{\Omega^{(e)}}\left\{\begin{array}{l}
N_{1}^{(e)} \\
N_{2}^{(e)} \\
N_{3}^{(e)}
\end{array}\right\} Q \mathrm{d} \Omega
\end{aligned}
\label{eq:Ec7_42} \tag{7.42}
\end{equation*}

Suponiendo un valor constante de $Q$ sobre el elemento se obtiene

$$
f_{Q}^{(e)}=\frac{Q A^{(e)}}{3}\left\{\begin{array}{l}
1 \\
1 \\
1
\end{array}\right\}
$$

Como era de esperar el flujo total sobre el elemento se reparte en partes iguales entre los tres nodos.

La contribución del término de convección al vector de flujos se calcula como sigue (ver. ec. (4.22))

\begin{equation*}
\begin{aligned}
f_{c}^{(e)}=-\int_{\Gamma_{q}^{(e)}}\left\{\begin{array}{l}
N_{1}^{(e)} \\
N_{2}^{(e)} \\
N_{3}^{(e)}
\end{array}\right\}\left[\bar{q}_{n}-\alpha \phi_{a}\right] \mathrm{d} \Gamma
\end{aligned}
\label{eq:Ec7_43} \tag{7.43}
\end{equation*}

Como en el cálculo de la matriz de rigidez, en la evaluación de la integral anterior hay que tener $\epsilon_{2}$ cuenta que sobre cada lado sólo son diferentes de cero las funciones de forma correspondientes a dicho lado. Asi, el valor de $\mathbf{f}_{c}^{(e)}$ se obtiene para cada lado por:

- $lado$ $12$

\begin{equation*}
\begin{aligned}
\mathbf{f}_{c_{12}}^{(e)}=-\int_{l_{12}^{(e)}}\left\{\begin{array}{c}
N_{1}^{(e)} \\
N_{2}^{(e)} \\
0
\end{array}\right\}\left[\bar{q}_{n}-\alpha \phi_{a}\right] \mathrm{d} \Gamma
\end{aligned}
\label{eq:Ec7_44} \tag{7.44}
\end{equation*}

- $lado$ $13$

\begin{equation*}
\begin{aligned}
\mathbf{f}_{c_{13}}^{(e)}=-\int_{l_{13}^{(e)}}\left\{\begin{array}{c}
N_{1}^{(e)} \\
0 \\
N_{3}^{(e)}
\end{array}\right\}\left[\bar{q}_{n}-\alpha \phi_{a}\right] \mathrm{d} \Gamma
\end{aligned}
\label{eq:Ec7_45} \tag{7.45}
\end{equation*}

- $lado$ $29$

\begin{equation*}
\begin{aligned}
\mathbf{f}_{c 23}^{(e)}=-\int_{l_{23}^{(e)}}\left\{\begin{array}{c}
0 \\
N_{2}^{(e)} \\
N_{3}^{(e)}
\end{array}\right\}\left[\bar{q}_{n}-\alpha \phi_{a}\right] \mathrm{d} \Gamma
\end{aligned}
\label{eq:Ec7_46} \tag{7.46}
\end{equation*}

Suponiendo ahora que $\bar{q}_{n}, \alpha$ y $\phi_{a}$ son constantes sobre el lado se obtiene

\begin{equation*}
\begin{aligned}
\mathbf{f}_{c_{12}}^{(e)}=-\frac{l_{12}^{(e)}}{2}\left[\bar{q}_{n}-\alpha \phi_{a}\right]\left\{\begin{array}{l}
1 \\
1 \\
0
\end{array}\right\}
\end{aligned}
\label{eq:Ec7_47} \tag{7.47}
\end{equation*}

Con expresiones similares para $\mathbf{f}_{c_{13}}^{(e)}$ y $\mathbf{f}_{c_{23}}^{(e)}$.

Al vector de flujos nodales hay que añadir la contribución de los flujos reacción en los contornos $\Gamma_{\phi}$ donde $\phi$ está prescrito ([Ec. 7.23](#Ec7_23)). Dichos flujos se añaden directamente a los nodos del contorno correspondiente y su valor se obtiene a posteriori una vez calculados los valores nodales de $\phi$.

#### Ejemplo - Generación de calor en un dominio cuadrado

Sea el dominio cuadrado de la Figura $7.5$ en el cual se genera una cantidad de calor interno conocida. Analizaremos la transmísión de calor en el mismo.

Figura 7.5.Flujo de calor en un dominio cuadrado, por Oñate,  Cálculo de Estructuras por el Método de los Elementos Finitos. Análisis Estático Lineal. Volumen 1. Sólidos.

- ***Solución:***

Supongamos $k_{x}=k_{y}=k=2,0\left[\mathrm{cal} / \mathrm{cm}^{\circ} \mathrm{C}\right]$ y $Q=2,4\left[\mathrm{cal} / \mathrm{cm}^{2} \mathrm{~s}\right] .$ Dada la simetría del problema, basta analizar un octavo del dominio total. Las condiciones de contorno son:

- en el contorno exterior $4-6: \alpha=0, \bar{\phi}=0$
- en los otros dos segmentos de borde $1-6$ y $1-4$, las condiciones de simetría exigen que

$$
\frac{\partial \phi}{\partial n}=0
$$

lo cual indica que el gradiente de temperatura en una dirección normal al contorno de simetría es cero. Ello significa que dicho contorno está térmicamente aislado. Adviértase que al no transmítirse el flujo a través de los ejes de simetría, éste lo hará sólo a través del lado exterior $1-4$ del dominio cuadrado.

Se divide el dominio triangular de análisis en una malla de cuatro elementos triangulares de tres nodos.

Puesto que $\alpha=0$

$$
\mathbf{K}^{(e)}=\mathbf{K}_{k}^{(e)}=\frac{k}{4 A^{(e)}}\left[\begin{array}{lll}
b_{1} b_{1}+c_{1} c_{1}, & b_{1} b_{2}+c_{1} c_{2}, & b_{1} b_{3}+c_{1} c_{3} \\
b_{2} b_{1}+c_{2} c_{1}, & b_{2} b_{2}+c_{2} c_{2}, & b_{2} b_{3}+c_{2} c_{3} \\
b_{3} b_{1}+c_{3} c_{1}, & b_{3} b_{2}+c_{3} c_{2}, & b_{3} b_{3}+c_{3} c_{3}
\end{array}\right]
$$

con

$$
\begin{aligned}
&b_{i}=y_{j}^{(e)}-y_{k}^{(e)} \\
&c_{i}=x_{k}^{(e)}-x_{j}^{(e)}
\end{aligned}
$$

Realizamos la numeración localmente, de elemento en elemento.

Para los elementos $1,2$ y $4$ se obtiene

$$
\mathbf{K}^{(1)}=\mathbf{K}^{(2)}=\mathbf{K}^{(4)}=\frac{2,0}{4 \frac{25^{2}}{2}}\left[\begin{array}{ccc}
6,25 & -6,25 & 0 \\
-6,25 & 6,25+6,25 & -6,25 \\
0 & -6,25 & 6,25
\end{array}\right]=\left[\begin{array}{ccc}
1 & -1 & 0 \\
-1 & 2 & -1 \\
0 & -1 & 1
\end{array}\right]
$$

De la misma forma se obtiene (ver Tabla 7.2)

|             $b_i$                    |              $c_i$                  |
|:------------------------------------:|:-----------------------------------:|
| $b_{1}=y_{2}^{(e)}-y_{3}^{(e)}=-2.5$ | $c_{1}=x_{3}^{(e)}-x_{2}^{(e)}=0.0$ |
| $b_{2}=y_{3}^{(e)}-y_{1}^{(e)}= 2.5$ | $c_{2}=x_{1}^{(e)}-x_{3}^{(e)}=-2.5$|
| $b_{3}=y_{1}^{(e)}-y_{2}^{(e)}=0$    | $c_{3}=x_{3}^{(e)}-x_{1}^{((e)}=2.5$|

***Tabla 7.1:*** Coeficientes $b_i$ y $c_i$ para los elementos $1,2$ y $4$

|             $b_i$                    |              $c_i$                  |
|:------------------------------------:|:-----------------------------------:|
| $b_{1}=y_{2}^{(3)}-y_{3}^{(3)}=0$    | $c_{1}=x_{3}^{(3)}-x_{2}^{(3)}=-2.5$|
| $b_{2}=y_{3}^{(3)}-y_{1}^{(3)}=2.5$  | $c_{2}=x_{1}^{(3)}-x_{3}^{(3)}=0$   |
| $b_{3}=y_{1}^{(3)}-y_{2}^{(3)}=-2.5$ | $c_{3}=x_{2}^{(3)}-x_{1}^{(3)}=2.5$|

***Tabla 7.2:*** Coeficientes $b_{i}$ y $c_{i}$ para el elemento 3

$$
\mathbf{K}^{(3)}=\frac{2,0}{4 \frac{6,25}{2}}\left[\begin{array}{ccc}
6,25 & 0 & -6,25 \\
0 & 6,25 & -6,25 \\
-6,25 & -6,25 & 12,50
\end{array}\right]=\left[\begin{array}{ccc}
1 & 0 & -1 \\
0 & 1 & -1 \\
-1 & -1 & 2
\end{array}\right]
$$

Vector de flujos nodales de cada elemento

$$
f^{(e)}=f_{Q}^{(e)}=\frac{Q A^{(e)}}{3}\left\{\begin{array}{l}
1 \\
1 \\
1
\end{array}\right\}
$$

$$
f^{(e)}=\frac{2.4 \times 2.5^{2}}{3 \times 2}\left\{\begin{array}{l}
1 \\
1 \\
1
\end{array}\right\}=2,5\left\{\begin{array}{l}
1 \\
1 \\
1
\end{array}\right\}
$$

- Construcción de la matriz de rigidez global

Identificamos la posición de cada coeficiente de rigidez de las matrices de los respectivos elementos $\mathbf{K}$ global a través de la torrespondencia entre la numeración local y global. Para mayor claridad consultar la Figura 7.5.

Se ensambla finalmente en $\mathbf{f}$ los flujos nodales de cada elemento. El sistema resultante es:

\begin{equation}
\underbrace{\left[\begin{array}{cccccc}
1 & -1 & 0 & 0 & 0 & 0 \\
-1 & 4 & -2 & -1 & 0 & 0 \\
0 & -2 & 4 & 0 & -2 & 0 \\
0 & -1 & 0 & 2 & -1 & 0 \\
0 & 0 & -2 & -1 & 4 & -1 \\
0 & 0 & 0 & 0 & -1 & 1
\end{array}\right]}_{\mathbf{K}} \underbrace{\left\{\begin{array}{l}
\phi_{1} \\
\phi_{2} \\
\phi_{3} \\
\phi_{4} \\
\phi_{5} \\
\phi_{6}
\end{array}\right\}}_{\mathbf{a}}=\underbrace{\left\{\begin{array}{l}
f_{1}^{(1)}=2,5 \\
f_{2}^{(1)}+f_{1}^{(2)}+f_{1}^{(3)}=7,5 \\
f_{3}^{(1)}+f_{3}^{(3)}+f_{1}^{(4)}=7,5 \\
f_{2}^{(2)}=2,5+R_{4} \\
f_{3}^{(2)}+f_{2}^{(3)}+f_{2}^{(4)}=7,5+R_{5} \\
f_{3}^{(4)}=2,5+R_{6}
\end{array}\right\}}_{\mathbf{f}}
\end{equation}

$R_4, R_5$ y $R_6$ son los flujos reacción en los nodos $4,5$ y $6$ respectivamente, y en los que se conoce el valor de $\phi$ ($\phi_4 = \phi_5 = \phi_6 = 0$). Después de resolver el SEL resultante, se obtiene el valor de las incógnitas del problema en cada nodo:

$$
\begin{aligned}
&\phi_{1}=9.3748^{\circ} \mathrm{C} \\
&\phi_{2}=6.8748^{\circ} \mathrm{C} \\
&\phi_{3}=5.3123^{\circ} \mathrm{C} \\
&\phi_{4}=0^{\circ} \mathrm{C} ; \quad \mathrm{R}_{4}=-9,3748 \mathrm{cal} / \mathrm{s} / \mathrm{cm}^{2} \\
&\phi_{5}=0^{\circ} \mathrm{C} ; \quad \mathrm{R}_{5}=-18,124 \mathrm{cal} / \mathrm{s} / \mathrm{cm}^{2} \\
&\phi_{6}=0^{\circ} \mathrm{C} ; \quad \mathrm{R}_{6}=-2,5 \mathrm{cal} / \mathrm{s} / \mathrm{cm}^{2}
\end{aligned}
$$

Comparemos ahora el flujo total generado en el dominio $(Q A)$ con la suma de flujo que se pierde por los contornos.

$$
\begin{aligned}
R_{4}+R_{5}+R_{6} &=29.9988 \\
Q A &=30
\end{aligned}
$$

La aproximación entre el flujo total generado y el flujo total saliente por el contorno puede considerarse aceptable, dentro de los límites de exactitud de la sencilla malla empleada.

```python

```


# LSTM Structure and Hidden State

We know that RNNs are used to maintain a kind of memory by linking the output of one node to the input of the next. In the case of an LSTM, for each piece of data in a sequence (say, for a word in a given sentence),
there is a corresponding *hidden state* $h_t$. This hidden state is a function of the pieces of data that an LSTM has seen over time; it contains some weights and, essentially, represents a kind of memory for the data that the LSTM has already seen. So, for an LSTM that is looking at words in a sentence, the hidden state of the LSTM will change based on each new word it sees. And, we can use the hidden state to predict the next word in a sequence or help identify the type of word in a language model, and lots of other things!

### LSTMs in Pytorch

To create and train an LSTM, you have to know how to structure the inputs, and hidden state of an LSTM.

In PyTorch, an LSTM expects all of its inputs to be 3D tensors, as follows:
* The first axis is the number of sequences of input data (a dimension of 20 could represent 20 input sequences)
* The second represents the number of sequences that will be processed in a batch of data (i.e. a dimension of 10 for a batch size of 10 input sequences)
* The third represents the number if inputs to process at a time, for example: 1 for one word at a time word

These will become clearer in the example in this notebook. This and the following notebook are modified versions of [this PyTorch LSTM tutorial](https://pytorch.org/tutorials/beginner/nlp/sequence_models_tutorial.html#lstm-s-in-pytorch).

Let's take a simple example and say we have a batch_size of 1, for processing one sentence. If we want to run the sequence model over one sentence "Giraffes in a field", our input should look like this `1x4` row vector:

\begin{align}\begin{bmatrix}
   \text{Giraffes  }
   \text{in  }
   \text{a  }
   \text{field}
   \end{bmatrix}\end{align}

With an additional 2nd dimension of size 1 to indicate that this one sentence is one batch.

If we wanted to process this sentence one word at a time, the 1st axis will also have a size of 1.

Next, let's see an example of one LSTM that is designed to look at a sequence of 4 values (numerical values since those are easiest to create and track) and generate 3 values as output; you are encouraged to change these input/hidden-state sizes to see the effect on the structure of the LSTM!

```python
import torch
import torch.nn as nn
import matplotlib.pyplot as plt

%matplotlib inline

torch.manual_seed(2) # so that random variables will be consistent and repeatable for testing
```

### Define a simple LSTM

**A note on hidden and output dimensions**

The `hidden_dim` and size of the output will be the same unless you define your own LSTM and change the number of outputs by adding a linear layer at the end of the network, ex. fc = nn.Linear(hidden_dim, output_dim).

```python
# define an LSTM with an input dim of 4 and hidden dim of 3
# this expects to see 4 values as input and generates 3 values as output
input_dim = 4
hidden_dim = 3
lstm = nn.LSTM(input_size=input_dim, hidden_size=hidden_dim)

# make 5 input sequences of 4 random values each
inputs_list = [torch.randn(1, input_dim) for _ in range(5)]
print('inputs: \n', inputs_list)
print('\n')

# initialize the hidden state
# (1 layer, 1 batch_size, 3 outputs)
# first tensor is the hidden state, often called h0
# second tensor initializes the cell memory, c0
hidden = (torch.randn(1, 1, hidden_dim),
          torch.randn(1, 1, hidden_dim))

# step through the sequence one element at a time.
for i in inputs_list:
    # after each step, hidden contains the hidden state
    out, hidden = lstm(i.view(1, 1, -1), hidden)
    print('out: \n', out)
    print('hidden: \n', hidden)

```

You should see that the output and hidden Tensors are always of length 3, which we specified when we defined the LSTM with `hidden_dim`.

### All at once

A for loop is not very efficient for large sequences of data, so we can also, **process all of these inputs at once.**

1. concatenate all our input sequences into one big tensor, with a defined batch_size
2. define the shape of our hidden state
3. get the outputs and the *most recent* hidden state (created after the last word in the sequence has been seen)

The outputs may look slightly different due to our differently initialized hidden state.

```python
# turn inputs into a tensor with 5 rows of data
# add the extra 2nd dimension (1) for batch_size
inputs = torch.cat(inputs_list).view(len(inputs_list), 1, -1)

# print out our inputs and their shape
# you should see (number of sequences, batch size, input_dim)
print('inputs size: \n', inputs.size())
print('\n')

print('inputs: \n', inputs)
print('\n')

# initialize the hidden state
hidden = (torch.randn(1, 1, hidden_dim),
          torch.randn(1, 1, hidden_dim))

# get the outputs and hidden state
out, hidden = lstm(inputs, hidden)

print('out: \n', out)
print('hidden: \n', hidden)
```

### Next: Part of Speech

Now that you have an understanding of the input and output size for an LSTM, next let's define our own model to tag parts of speech (nouns, verbs, determinants), include an LSTM and a Linear layer to define a desired output size, *and* finally train our model to create a distribution of class scores that associates each input word with a part of speech.


# Simplified ikeda Wave
Comparison between original paper and IMO

```python
import sympy as sp
import dill
import numpy as np
import pandas as pd
pd.options.display.max_rows = 999
pd.options.display.max_columns = 999

import matplotlib.pyplot as plt

from rolldecayestimators.substitute_dynamic_symbols import lambdify, run
```

```python
with open('equations.dill', mode='rb') as file:
    equations = dill.load(file)
```

```python
with open('symbols.dill', mode='rb') as file:
    symbols = dill.load(file)
```

## Proposed Formula of the Wave Roll Damping

```python
equations.B_n_W
```

$\displaystyle B_{n W} = \frac{A_{1} e^{- A_{2} \log{\left(x_{5} \right)} - 0.694444444444444 A_{3}^{2}}}{x_{5}}$

```python
equations.A_3
```

$\displaystyle A_{3} = AA_{3} + A_{31} x_{4}^{6} + A_{32} x_{4}^{5} + A_{33} x_{4}^{4} + A_{34} x_{4}^{3} + A_{35} x_{4}^{2} + A_{36} x^{4} + A_{37}$

```python
equations.A_3.args
```

    (A_3,
     AA_3 + A_31*x_4**6 + A_32*x_4**5 + A_33*x_4**4 + A_34*x_4**3 + A_35*x_4**2 + A_36*x**4 + A_37)

```python
lambdas.A_3
```

    <function _lambdifygenerated(AA_3, A_31, A_32, A_33, A_34, A_35, A_36, A_37, x, x_4)>

```python
lambda_dict = {}
for name, equation in equations._asdict().items():
    lambda_dict[name] = lambdify(equation.rhs)
```

```python
from collections import namedtuple
Lambdas = namedtuple('Lambdas', lambda_dict.keys())
lambdas = Lambdas(**lambda_dict)
```

```python
B = 60  # [m]
d = 20  # [m]
Lpp = 300  # [m]
b_BK = 0.03*B
l_BK = 0.3*Lpp
phi_a = 20  # [deg]
w_hat = np.linspace(0.5,5,10)
Cb = 0.8
Cm = 0.94

OG_div_d = 0.2
OG = OG_div_d*d

inputs = pd.DataFrame()
inputs['w_hat'] = w_hat
inputs['x_5'] = w_hat

inputs['OG'] = OG
inputs['OG/d'] = OG/d
inputs['phi_a'] = phi_a
inputs['x_1'] = B/d
inputs['x_2'] = Cb
inputs['x_3'] = Cm
inputs['x_4'] = OG/d

#Unsure about this one, there was a missprint in the original paper.
x_2 = inputs['x'] = inputs['x_4']  # or x_2?

```

```python
lambdas.B_n_W
```

    <function _lambdifygenerated(A_1, A_2, A_3, x_5)>

```python
lambdas.A_1
```

    <function _lambdifygenerated(AA_1, A_11, A_12, A_13, x_4)>

```python
As = [11,12,13,111,112,113,121,122,123,124,131,132,133,134,
    31,31,33,34,35,36,37,
]
AAs = [1,31,32,311,
    11,12,111,112,113, 121, 122,123]

As2 = [1,2,32]

AAs2 = [3,]

As3 = [3]

def run_A(As, inputs, label='A'):

    for A in reversed(As):
        name = '%s_%i' % (label,A)
        function = getattr(lambdas, name)

        try:
            inputs[name] = run(function, inputs)
        except:
            print('%s: %s' % (name,function))
            raise

run_A(As=As, inputs=inputs, label='A')
run_A(As=AAs, inputs=inputs, label='AA')
run_A(As=As2, inputs=inputs, label='A')
inputs['x_6'] = run(lambdas.x_6, inputs)
run_A(As=AAs2, inputs=inputs, label='AA')
run_A(As=As3, inputs=inputs, label='A')

inputs['B_n_W'] = run(lambdas.B_n_W,inputs)

```

```python
inputs.plot(x='w_hat', y='B_n_W')
```

```python
lambdas.B_n_W(0.5,0,0,1)
```

    0.5

```python
lambdas.B_n_W
```

    <function _lambdifygenerated(A_1, A_2, A_3, x_5)>

## IMO implementation

```python
Q1_str = """
0 1 2 3 4 5
1 0.00000 0.00000 0.00000 0.00000 0.00000
2 0.00000 -0.00222 0.04087 -0.28687 0.59942
3 0.00000 0.01019 -0.16118 0.90499 -1.64139
4 0.00000 -0.01542 0.22037 -1.08499 1.83417
5 -0.06287 0.49893 0.52735 -10.79187 16.61633
6 0.11407 -0.81090 -2.21868 25.12697 -37.77298
7 -0.05893 0.26397 3.19497 -21.81266 31.41135
8 0.01077 0.00187 -1.24941 6.94279 -10.20190
9 0.00000 0.19221 -2.78746 12.50785 -14.76486
10 0.00000 -0.35056 5.22235 -23.97485 29.00785
11 0.00000 0.23710 -3.53506 16.36838 -20.53991
12 0.00000 -0.06712 0.96636 -4.40754 5.89470
13 0.00000 17.945 -166.294 489.799 -493.142
14 0.00000 -25.507 236.275 -698.683 701.494
15 0.00000 9.077 -84.332 249.983 -250.787
16 0.00000 -16.872 156.399 -460.689 463.848
17 0.00000 24.015 -222.507 658.027 -660.665
18 0.00000 -8.56 79.549 -235.827 236.579
"""
Q1 = np.fromstring(Q1_str,sep=' ').reshape(19,6)
Q1
```

    array([[ 0.000000e+00,  1.000000e+00,  2.000000e+00,  3.000000e+00,
             4.000000e+00,  5.000000e+00],
           [ 1.000000e+00,  0.000000e+00,  0.000000e+00,  0.000000e+00,
             0.000000e+00,  0.000000e+00],
           [ 2.000000e+00,  0.000000e+00, -2.220000e-03,  4.087000e-02,
            -2.868700e-01,  5.994200e-01],
           [ 3.000000e+00,  0.000000e+00,  1.019000e-02, -1.611800e-01,
             9.049900e-01, -1.641390e+00],
           [ 4.000000e+00,  0.000000e+00, -1.542000e-02,  2.203700e-01,
            -1.084990e+00,  1.834170e+00],
           [ 5.000000e+00, -6.287000e-02,  4.989300e-01,  5.273500e-01,
            -1.079187e+01,  1.661633e+01],
           [ 6.000000e+00,  1.140700e-01, -8.109000e-01, -2.218680e+00,
             2.512697e+01, -3.777298e+01],
           [ 7.000000e+00, -5.893000e-02,  2.639700e-01,  3.194970e+00,
            -2.181266e+01,  3.141135e+01],
           [ 8.000000e+00,  1.077000e-02,  1.870000e-03, -1.249410e+00,
             6.942790e+00, -1.020190e+01],
           [ 9.000000e+00,  0.000000e+00,  1.922100e-01, -2.787460e+00,
             1.250785e+01, -1.476486e+01],
           [ 1.000000e+01,  0.000000e+00, -3.505600e-01,  5.222350e+00,
            -2.397485e+01,  2.900785e+01],
           [ 1.100000e+01,  0.000000e+00,  2.371000e-01, -3.535060e+00,
             1.636838e+01, -2.053991e+01],
           [ 1.200000e+01,  0.000000e+00, -6.712000e-02,  9.663600e-01,
            -4.407540e+00,  5.894700e+00],
           [ 1.300000e+01,  0.000000e+00,  1.794500e+01, -1.662940e+02,
             4.897990e+02, -4.931420e+02],
           [ 1.400000e+01,  0.000000e+00, -2.550700e+01,  2.362750e+02,
            -6.986830e+02,  7.014940e+02],
           [ 1.500000e+01,  0.000000e+00,  9.077000e+00, -8.433200e+01,
             2.499830e+02, -2.507870e+02],
           [ 1.600000e+01,  0.000000e+00, -1.687200e+01,  1.563990e+02,
            -4.606890e+02,  4.638480e+02],
           [ 1.700000e+01,  0.000000e+00,  2.401500e+01, -2.225070e+02,
             6.580270e+02, -6.606650e+02],
           [ 1.800000e+01,  0.000000e+00, -8.560000e+00,  7.954900e+01,
            -2.358270e+02,  2.365790e+02]])

```python
Q2 = np.array([0, 0.00000, -1.402, 7.189, -10.993, 9.45])
```

```python
Q3_str = """
0 1 2 3 4 5 6 7
1 -7686.0287 30131.5678 -49048.9664 42480.7709 -20665.147 5355.2035 -577.8827
2 61639.9103 -241201.0598 392579.5937 -340629.4699 166348.6917 -43358.7938 4714.7918
3 -130677.4903 507996.2604 -826728.7127 722677.104 -358360.7392 95501.4948 -10682.8619
4 -110034.6584 446051.22 -724186.4643 599411.9264 -264294.7189 58039.7328 -4774.6414
5 709672.0656 -2803850.2395 4553780.5017 -3888378.9905 1839829.259 -457313.6939 46600.823
6 -822735.9289 3238899.7308 -5256636.5472 4500543.147 -2143487.3508 538548.1194 -55751.1528
7 299122.8727 -1175773.1606 1907356.1357 -1634256.8172 780020.9393 -196679.7143 20467.0904
"""
Q3 = np.fromstring(Q3_str,sep=' ').reshape(8,8)
Q3
```

    array([[ 0.00000000e+00,  1.00000000e+00,  2.00000000e+00,
             3.00000000e+00,  4.00000000e+00,  5.00000000e+00,
             6.00000000e+00,  7.00000000e+00],
           [ 1.00000000e+00, -7.68602870e+03,  3.01315678e+04,
            -4.90489664e+04,  4.24807709e+04, -2.06651470e+04,
             5.35520350e+03, -5.77882700e+02],
           [ 2.00000000e+00,  6.16399103e+04, -2.41201060e+05,
             3.92579594e+05, -3.40629470e+05,  1.66348692e+05,
            -4.33587938e+04,  4.71479180e+03],
           [ 3.00000000e+00, -1.30677490e+05,  5.07996260e+05,
            -8.26728713e+05,  7.22677104e+05, -3.58360739e+05,
             9.55014948e+04, -1.06828619e+04],
           [ 4.00000000e+00, -1.10034658e+05,  4.46051220e+05,
            -7.24186464e+05,  5.99411926e+05, -2.64294719e+05,
             5.80397328e+04, -4.77464140e+03],
           [ 5.00000000e+00,  7.09672066e+05, -2.80385024e+06,
             4.55378050e+06, -3.88837899e+06,  1.83982926e+06,
            -4.57313694e+05,  4.66008230e+04],
           [ 6.00000000e+00, -8.22735929e+05,  3.23889973e+06,
            -5.25663655e+06,  4.50054315e+06, -2.14348735e+06,
             5.38548119e+05, -5.57511528e+04],
           [ 7.00000000e+00,  2.99122873e+05, -1.17577316e+06,
             1.90735614e+06, -1.63425682e+06,  7.80020939e+05,
            -1.96679714e+05,  2.04670904e+04]])

```python
Q4_str = """
0 1 2 3 4
1 -0.3767 3.39 -10.356 11.588
2 -17.109 41.495 -33.234 8.8007
3 36.566 -89.203 71.8 -18.108
4 0 -0.0727 0.7 -1.2818
"""
Q4 = np.fromstring(Q4_str,sep=' ').reshape(5,5)
Q4
```

    array([[ 0.0000e+00,  1.0000e+00,  2.0000e+00,  3.0000e+00,  4.0000e+00],
           [ 1.0000e+00, -3.7670e-01,  3.3900e+00, -1.0356e+01,  1.1588e+01],
           [ 2.0000e+00, -1.7109e+01,  4.1495e+01, -3.3234e+01,  8.8007e+00],
           [ 3.0000e+00,  3.6566e+01, -8.9203e+01,  7.1800e+01, -1.8108e+01],
           [ 4.0000e+00,  0.0000e+00, -7.2700e-02,  7.0000e-01, -1.2818e+00]])

```python
Q5 = np.array([0, -1.05584, 12.688, -63.70534, 172.84571,
              -274.05701, 257.68705, -141.40915, 44.13177,
               -7.1654, -0.0495, 0.4518, -0.61655,
              ])
Q5
```

    array([ 0.0000000e+00, -1.0558400e+00,  1.2688000e+01, -6.3705340e+01,
            1.7284571e+02, -2.7405701e+02,  2.5768705e+02, -1.4140915e+02,
            4.4131770e+01, -7.1654000e+00, -4.9500000e-02,  4.5180000e-01,
           -6.1655000e-01])

```python
inputs_IMO = pd.DataFrame()
inputs_IMO['w_hat'] = w_hat
x_5 = inputs_IMO['x_5'] = w_hat
inputs_IMO['OG'] = OG
inputs_IMO['OG/d'] = OG/d
inputs_IMO['phi_a'] = phi_a
x_1 = inputs_IMO['x_1'] = B/d
x_2 = inputs_IMO['x_2'] = Cb
x_3 = inputs_IMO['x_3'] = Cm
x_4 = inputs_IMO['x_4'] = OG/d
```

```python
sum1 = np.zeros(len(inputs_IMO))
for i in range(1,4+1):
    sum1+= Q4[1,i]*x_1**(4-i)

sum2 = np.zeros(len(inputs_IMO))
for j in np.arange(1,2+1):
    for k in np.arange(1,4+1):
        sum2+=Q4[2+j,k]*x_2**(3-k)*x_4**(2-j)

sum4 = np.zeros(len(inputs_IMO))
for i in range(1,3+1):
    sum4+=Q5[i+9]*x_1**(3-i)

sum3 = np.zeros(len(inputs_IMO))
for i in range(1,10+1):
    for j in range(1,4+1):
        sum3+=Q5[i]*(x_4-np.sum(Q4[4,j]*x_1**(4-j)))**(10-i)

AA_3 = inputs['AA_3'] = sum1*sum2*(sum3+sum4)

```

```python
sum5 = np.zeros(len(inputs_IMO))
for i in range(1,7+1):
    for j in range(1,7+1):
        sum5+=Q3[i,j]*x_2**(7-j)*x_4**(7-i)

A_3 = inputs['A_3'] = AA_3 + sum5
```

```python
sum6 = np.zeros(len(inputs_IMO))
for i in range(1,5+1):
    sum6+=Q2[i]*x_4**(5-i)

A_2 = inputs['A_2'] = sum6
```

```python
sum7 = np.zeros(len(inputs_IMO))
for i in range(1,2+1):
    for j in range(1,4+1):
        for k in range(1,5+1):
            sum7+=Q1[j+4*i+12]

```

```python
fig,ax = plt.subplots()
inputs.plot(x='OG/d', y='B_n_BK', label='Kawahara', ax=ax)
inputs_IMO.plot(x='OG/d', y='B_n_BK', label='IMO', style='--',ax=ax)

```

```python

ys = ['f_1','f_2','f_3','B_BK','B_B2K','B_B3K']

for y in ys:
    fig,ax = plt.subplots()
    inputs.plot(x='OG/d', y=y, label='Kawahara', ax=ax)
    inputs_IMO.plot(x='OG/d', y=y, label='IMO', style='--', ax=ax)
    ax.set_title(y)
```

```python

```


```python
import tensorflow as tf
import tensorflow_quantum as tfq

import cirq
import sympy
import numpy as np
import seaborn as sns
import collections

# visualization tools
%matplotlib inline
import matplotlib.pyplot as plt
from cirq.contrib.svg import SVGCircuit
```

```python
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()

# Rescale the images from [0,255] to the [0.0,1.0] range.
x_train, x_test = x_train[..., np.newaxis]/255.0, x_test[..., np.newaxis]/255.0

print("Number of original training examples:", len(x_train))
print("Number of original test examples:", len(x_train))
```

    Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz
    11493376/11490434 [==============================] - 0s 0us/step
    11501568/11490434 [==============================] - 0s 0us/step
    Number of original training examples: 60000
    Number of original test examples: 60000

```python
def filter_36(x, y):
    keep = (y == 3) | (y == 6)
    x, y = x[keep], y[keep]
    y = y == 3
    return x,y
```

```python
x_train, y_train = filter_36(x_train, y_train)
x_test, y_test = filter_36(x_test, y_test)

print("Number of filtered training examples:", len(x_train))
print("Number of filtered test examples:", len(x_test))
```

    Number of filtered training examples: 12049
    Number of filtered test examples: 1968

```python
print(y_train[17])

plt.imshow(x_train[0, :, :, 0])
plt.colorbar()
```

```python
x_train_small = tf.image.resize(x_train, (4,4)).numpy()
x_test_small = tf.image.resize(x_test, (4,4)).numpy()
```

```python
print(y_train[0])

plt.imshow(x_train_small[0,:,:,0], vmin=0, vmax=1)
plt.colorbar()
```

```python
def remove_contradicting(xs, ys):
    mapping = collections.defaultdict(set)
    # Determine the set of labels for each unique image:
    for x,y in zip(xs,ys):
       mapping[tuple(x.flatten())].add(y)

    new_x = []
    new_y = []
    for x,y in zip(xs, ys):
      labels = mapping[tuple(x.flatten())]
      if len(labels) == 1:
          new_x.append(x)
          new_y.append(list(labels)[0])
      else:
          # Throw out images that match more than one label.
          pass

    num_3 = sum(1 for value in mapping.values() if True in value)
    num_6 = sum(1 for value in mapping.values() if False in value)
    num_both = sum(1 for value in mapping.values() if len(value) == 2)

    print("Number of unique images:", len(mapping.values()))
    print("Number of 3s: ", num_3)
    print("Number of 6s: ", num_6)
    print("Number of contradictory images: ", num_both)
    print()
    print("Initial number of examples: ", len(xs))
    print("Remaining non-contradictory examples: ", len(new_x))

    return np.array(new_x), np.array(new_y)
```

```python
x_train_nocon, y_train_nocon = remove_contradicting(x_train_small, y_train)
#x_test_nocon, y_test_nocon = remove_contradicting(x_test_small,
```

    Number of unique images: 10387
    Number of 3s:  4961
    Number of 6s:  5475
    Number of contradictory images:  49

    Initial number of examples:  12049
    Remaining non-contradictory examples:  11520

```python
THRESHOLD = 0.5

x_train_bin = np.array(x_train_nocon > THRESHOLD, dtype=np.float32)
x_test_bin = np.array(x_test_small > THRESHOLD, dtype=np.float32)
```

```python
x_train_bin2,y_train_nocon2 = remove_contradicting(x_train_bin, y_train_nocon)
#x_test_bin2,y_test_nocon2 = remove_contr
```

    Number of unique images: 193
    Number of 3s:  124
    Number of 6s:  113
    Number of contradictory images:  44

    Initial number of examples:  11520
    Remaining non-contradictory examples:  3731

```python
def convert_to_circuit(image):
    """Encode truncated classical image into quantum datapoint."""
    values = np.ndarray.flatten(image)
    #qubits = cirq.GridQubit.rect(4, 4)
    qubits = cirq.GridQubit.rect(4, 4)
    circuit = cirq.Circuit()
    for i, value in enumerate(values):
        if value:
            circuit.append(cirq.X(qubits[i]))
    return circuit

x_train_circ = [convert_to_circuit(x) for x in x_train_bin2]
x_test_circ = [convert_to_circuit(x) for x in x_test_bin]
```

```python
bin_img = x_train_bin[0,:,:,0]
indices = np.array(np.where(bin_img)).T
indices
```

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

```python
x_train_tfcirc = tfq.convert_to_tensor(x_train_circ)
x_test_tfcirc = tfq.convert_to_tensor(x_test_circ)
```

```python
x_train_bin[0].shape
```

    (4, 4, 1)

```python
class CircuitLayerBuilder():
    def __init__(self, data_qubits, readout):
        self.data_qubits = data_qubits
        self.readout = readout

    def add_layer(self, circuit, gate, prefix):
        for i, qubit in enumerate(self.data_qubits):
            symbol = sympy.Symbol(prefix + '-' + str(i))
            circuit.append(gate(qubit, self.readout)**symbol)
```

```python
demo_builder = CircuitLayerBuilder(data_qubits = cirq.GridQubit.rect(4,1),
                                   readout=cirq.GridQubit(-1,-1))

circuit = cirq.Circuit()
demo_builder.add_layer(circuit, gate = cirq.XX, prefix='xx')
SVGCircuit(circuit)
```

    findfont: Font family ['Arial'] not found. Falling back to DejaVu Sans.

```python
def create_quantum_model():
    """Create a QNN model circuit and readout operation to go along with it."""
    data_qubits = cirq.GridQubit.rect(4, 4)  # a 4x4 grid.
    readout = cirq.GridQubit(-1, -1)         # a single qubit at [-1,-1]
    circuit = cirq.Circuit()

    # Prepare the readout qubit.
    circuit.append(cirq.X(readout))
    circuit.append(cirq.H(readout))

    builder = CircuitLayerBuilder(
        data_qubits = data_qubits,
        readout=readout)

    # Then add layers (experiment by adding more).
    builder.add_layer(circuit, cirq.XX, "xx1")
    builder.add_layer(circuit, cirq.ZZ, "zz1")

    # Finally, prepare the readout qubit.
    circuit.append(cirq.H(readout))
    circuit.append(cirq.measure(readout))

    return circuit, cirq.Z(readout)
```

```python
model_circuit, model_readout = create_quantum_model()
```

```python
SVGCircuit(model_circuit)
```

```python
# Build the Keras model.
model = tf.keras.Sequential([
    # The input is the data-circuit, encoded as a tf.string
    tf.keras.layers.Input(shape=(), dtype=tf.string),
    # The PQC layer returns the expected value of the readout gate, range [-1,1].
    tfq.layers.PQC(model_circuit, model_readout),
    #tf.keras.layers.Activation('softmax'),

])
```

```python
y_train_hinge = 2.0*y_train_nocon-1.0
y_test_hinge = 2.0*y_test-1.0
```

```python
y_test
```

    array([False,  True, False, ..., False,  True, False])

```python
model.compile(
    loss=tf.keras.losses.mean_squared_error,
    optimizer=tf.keras.optimizers.Adam(lr=0.01),
    metrics=['accuracy'])
    #metrics=[hinge_accuracy])

#model.compile(optimizer='rmsprop',
#              loss='binary_crossentropy',
#              metrics=['accuracy'])
```

    /usr/local/lib/python3.7/dist-packages/keras/optimizer_v2/adam.py:105: UserWarning: The `lr` argument is deprecated, use `learning_rate` instead.
      super(Adam, self).__init__(name, **kwargs)

```python
print(model.summary())

```

    Model: "sequential"
    _________________________________________________________________
     Layer (type)                Output Shape              Param #
    =================================================================
     pqc (PQC)                   (None, 1)                 32

    =================================================================
    Total params: 32
    Trainable params: 32
    Non-trainable params: 0
    _________________________________________________________________
    None

```python
EPOCHS = 1
BATCH_SIZE = 32

NUM_EXAMPLES = len(x_train_tfcirc)
```

```python
len(x_test_tfcirc)
```

    1968

```python
x_train_tfcirc_sub = x_train_tfcirc[:NUM_EXAMPLES]
y_train_hinge_sub = y_train_hinge[:NUM_EXAMPLES]
```

```python
x_train_tfcirc_sub[0].shape
```

    TensorShape([])

```python
class LossHistory(tf.keras.callbacks.Callback):
    def on_train_begin(self, logs={}):
        self.losses = []

    def on_batch_end(self, batch, logs={}):
        self.losses.append(logs.get('loss'))
```

```python
len(x_train_tfcirc)
```

    3731

```python
import time
history = LossHistory()
begin_time = time.time()
qnn_history = model.fit(
      x_train_tfcirc_sub, y_train_hinge_sub,
      batch_size=32,
      epochs=EPOCHS,
      verbose=1,
      validation_data=(x_test_tfcirc, y_test_hinge),
      callbacks=[history])
end_time = time.time()
print('the cost time is '+str(end_time-begin_time))
qnn_results = model.evaluate(x_test_tfcirc, y_test)
```

    117/117 [==============================] - 579s 5s/step - loss: 1.0008 - accuracy: 0.0000e+00 - val_loss: 0.9947 - val_accuracy: 0.0000e+00
    the cost time is 624.1300377845764
    62/62 [==============================] - 48s 775ms/step - loss: 0.5099 - accuracy: 0.4868

```python
import time
history = LossHistory()
begin_time = time.time()
qnn_history = model.fit(
      x_train_tfcirc_sub[:200], y_train_hinge_sub[:200],
      batch_size=20,
      epochs=EPOCHS,
      verbose=1,
      validation_data=(x_test_tfcirc[:100], y_test_hinge[:100]),
      callbacks=[history])
end_time = time.time()
print('the cost time is '+str(end_time-begin_time))
#qnn_results = model.evaluate(x_test_tfcirc, y_test)
```

    10/10 [==============================] - 24s 2s/step - loss: 1.0007 - accuracy: 0.0000e+00 - val_loss: 0.9979 - val_accuracy: 0.0000e+00
    the cost time is 24.24513077735901

```python
qnn_results
```

    [0.5099204182624817, 0.48678863048553467]

```python
model.evaluate(x_test_tfcirc, y_test)
```

    62/62 [==============================] - 37s 589ms/step - loss: 0.5117 - accuracy: 0.4868

    [0.5116992592811584, 0.48678863048553467]

```python
def create_classical_model():
    # A simple model based off LeNet from https://keras.io/examples/mnist_cnn/
    model = tf.keras.Sequential()
    model.add(tf.keras.layers.Conv2D(32, [3, 3], activation='relu', input_shape=(28,28,1)))
    model.add(tf.keras.layers.Conv2D(64, [3, 3], activation='relu'))
    model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2)))
    model.add(tf.keras.layers.Dropout(0.25))
    model.add(tf.keras.layers.Flatten())
    model.add(tf.keras.layers.Dense(128, activation='relu'))
    model.add(tf.keras.layers.Dropout(0.5))
    model.add(tf.keras.layers.Dense(1))
    return model

model = create_classical_model()
model.compile(loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
              optimizer=tf.keras.optimizers.Adam(),
              metrics=['accuracy'])

model.summary()
```

    Model: "sequential_1"
    _________________________________________________________________
     Layer (type)                Output Shape              Param #
    =================================================================
     conv2d (Conv2D)             (None, 26, 26, 32)        320

     conv2d_1 (Conv2D)           (None, 24, 24, 64)        18496

     max_pooling2d (MaxPooling2D  (None, 12, 12, 64)       0
     )

     dropout (Dropout)           (None, 12, 12, 64)        0

     flatten (Flatten)           (None, 9216)              0

     dense (Dense)               (None, 128)               1179776

     dropout_1 (Dropout)         (None, 128)               0

     dense_1 (Dense)             (None, 1)                 129

    =================================================================
    Total params: 1,198,721
    Trainable params: 1,198,721
    Non-trainable params: 0
    _________________________________________________________________

```python
def create_classical_model():
    # A simple model based off LeNet from https://keras.io/examples/mnist_cnn/
    model = tf.keras.Sequential()
    model.add(tf.keras.layers.Conv2D(32, [3, 3], activation='relu', input_shape=(28,28,1)))
    model.add(tf.keras.layers.Conv2D(64, [3, 3], activation='relu'))
    model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2)))
    model.add(tf.keras.layers.Dropout(0.25))
    model.add(tf.keras.layers.Flatten())
    model.add(tf.keras.layers.Dense(128, activation='relu'))
    model.add(tf.keras.layers.Dropout(0.5))
    model.add(tf.keras.layers.Dense(1))
    return model

model = create_classical_model()
model.compile(loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
              optimizer=tf.keras.optimizers.Adam(),
              metrics=['accuracy'])

model.summary()
```

    Model: "sequential_2"
    _________________________________________________________________
     Layer (type)                Output Shape              Param #
    =================================================================
     conv2d_2 (Conv2D)           (None, 26, 26, 32)        320

     conv2d_3 (Conv2D)           (None, 24, 24, 64)        18496

     max_pooling2d_1 (MaxPooling  (None, 12, 12, 64)       0
     2D)

     dropout_2 (Dropout)         (None, 12, 12, 64)        0

     flatten_1 (Flatten)         (None, 9216)              0

     dense_2 (Dense)             (None, 128)               1179776

     dropout_3 (Dropout)         (None, 128)               0

     dense_3 (Dense)             (None, 1)                 129

    =================================================================
    Total params: 1,198,721
    Trainable params: 1,198,721
    Non-trainable params: 0
    _________________________________________________________________

```python
history = LossHistory()
begin_time = time.time()
model.fit(x_train,
          y_train,
          batch_size=128,
          epochs=1,
          verbose=1,
          validation_data=(x_test, y_test),
          callbacks=[history])
end_time = time.time()
print('the cost time is '+str(end_time-begin_time))
cnn_results = model.evaluate(x_test, y_test)
```

    95/95 [==============================] - 47s 478ms/step - loss: 0.0444 - accuracy: 0.9815 - val_loss: 0.0064 - val_accuracy: 0.9980
    the cost time is 83.20136618614197
    62/62 [==============================] - 2s 34ms/step - loss: 0.0064 - accuracy: 0.9980

```python
len(history.losses)
```

    95

```python
def create_fair_classical_model():
    # A simple model based off LeNet from https://keras.io/examples/mnist_cnn/
    model = tf.keras.Sequential()
    model.add(tf.keras.layers.Flatten(input_shape=(4,4,1)))
    model.add(tf.keras.layers.Dense(2, activation='relu'))
    model.add(tf.keras.layers.Dense(1))
    return model

model = create_fair_classical_model()
model.compile(loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
              optimizer=tf.keras.optimizers.Adam(),
              metrics=['accuracy'])

model.summary()
```

    Model: "sequential_3"
    _________________________________________________________________
     Layer (type)                Output Shape              Param #
    =================================================================
     flatten_2 (Flatten)         (None, 16)                0

     dense_4 (Dense)             (None, 2)                 34

     dense_5 (Dense)             (None, 1)                 3

    =================================================================
    Total params: 37
    Trainable params: 37
    Non-trainable params: 0
    _________________________________________________________________

```python
model.fit(x_train_bin,
          y_train_nocon,
          batch_size=128,
          epochs=20,
          verbose=2,
          validation_data=(x_test_bin, y_test))

fair_nn_results = model.evaluate(x_test_bin, y_test)
```

    Epoch 1/20
    90/90 - 1s - loss: 0.6355 - accuracy: 0.6566 - val_loss: 0.5982 - val_accuracy: 0.6687 - 1s/epoch - 12ms/step
    Epoch 2/20
    90/90 - 0s - loss: 0.5585 - accuracy: 0.7552 - val_loss: 0.5202 - val_accuracy: 0.7774 - 285ms/epoch - 3ms/step
    Epoch 3/20
    90/90 - 0s - loss: 0.4778 - accuracy: 0.8161 - val_loss: 0.4461 - val_accuracy: 0.7912 - 288ms/epoch - 3ms/step
    Epoch 4/20
    90/90 - 0s - loss: 0.4062 - accuracy: 0.8232 - val_loss: 0.3832 - val_accuracy: 0.7988 - 288ms/epoch - 3ms/step
    Epoch 5/20
    90/90 - 0s - loss: 0.3512 - accuracy: 0.8391 - val_loss: 0.3375 - val_accuracy: 0.8222 - 234ms/epoch - 3ms/step
    Epoch 6/20
    90/90 - 0s - loss: 0.3116 - accuracy: 0.8514 - val_loss: 0.3048 - val_accuracy: 0.8308 - 283ms/epoch - 3ms/step
    Epoch 7/20
    90/90 - 0s - loss: 0.2838 - accuracy: 0.8569 - val_loss: 0.2822 - val_accuracy: 0.8313 - 271ms/epoch - 3ms/step
    Epoch 8/20
    90/90 - 0s - loss: 0.2638 - accuracy: 0.8587 - val_loss: 0.2658 - val_accuracy: 0.8313 - 265ms/epoch - 3ms/step
    Epoch 9/20
    90/90 - 0s - loss: 0.2494 - accuracy: 0.8670 - val_loss: 0.2538 - val_accuracy: 0.8699 - 284ms/epoch - 3ms/step
    Epoch 10/20
    90/90 - 0s - loss: 0.2388 - accuracy: 0.8725 - val_loss: 0.2451 - val_accuracy: 0.8699 - 302ms/epoch - 3ms/step
    Epoch 11/20
    90/90 - 0s - loss: 0.2308 - accuracy: 0.8826 - val_loss: 0.2388 - val_accuracy: 0.8669 - 310ms/epoch - 3ms/step
    Epoch 12/20
    90/90 - 0s - loss: 0.2250 - accuracy: 0.8832 - val_loss: 0.2337 - val_accuracy: 0.8669 - 259ms/epoch - 3ms/step
    Epoch 13/20
    90/90 - 0s - loss: 0.2205 - accuracy: 0.8832 - val_loss: 0.2300 - val_accuracy: 0.8674 - 276ms/epoch - 3ms/step
    Epoch 14/20
    90/90 - 0s - loss: 0.2172 - accuracy: 0.8840 - val_loss: 0.2273 - val_accuracy: 0.8674 - 253ms/epoch - 3ms/step
    Epoch 15/20
    90/90 - 0s - loss: 0.2145 - accuracy: 0.9092 - val_loss: 0.2251 - val_accuracy: 0.9141 - 250ms/epoch - 3ms/step
    Epoch 16/20
    90/90 - 0s - loss: 0.2125 - accuracy: 0.9119 - val_loss: 0.2232 - val_accuracy: 0.9141 - 333ms/epoch - 4ms/step
    Epoch 17/20
    90/90 - 0s - loss: 0.2109 - accuracy: 0.9120 - val_loss: 0.2219 - val_accuracy: 0.9141 - 300ms/epoch - 3ms/step
    Epoch 18/20
    90/90 - 0s - loss: 0.2095 - accuracy: 0.9121 - val_loss: 0.2206 - val_accuracy: 0.9141 - 264ms/epoch - 3ms/step
    Epoch 19/20
    90/90 - 0s - loss: 0.2085 - accuracy: 0.9122 - val_loss: 0.2199 - val_accuracy: 0.9141 - 255ms/epoch - 3ms/step
    Epoch 20/20
    90/90 - 0s - loss: 0.2076 - accuracy: 0.9122 - val_loss: 0.2192 - val_accuracy: 0.9141 - 257ms/epoch - 3ms/step
    62/62 [==============================] - 0s 3ms/step - loss: 0.2192 - accuracy: 0.9141

```python
fair_nn_results[1]
```

    0.9141260385513306

```python
qnn_accuracy = qnn_results[1]
cnn_accuracy = cnn_results[1]
fair_nn_accuracy = fair_nn_results[1]

sns.barplot(["Quantum", "Classical, full", "Classical, fair"],
            [qnn_accuracy, cnn_accuracy, fair_nn_accuracy])
```

```python
def cluster_state_circuit(bits):
    """Return a cluster state on the qubits in `bits`."""
    circuit = cirq.Circuit()
    circuit.append(cirq.H.on_each(bits))
    for this_bit, next_bit in zip(bits, bits[1:] + [bits[0]]):
        circuit.append(cirq.CZ(this_bit, next_bit))
    return circuit
```

```python
SVGCircuit(cluster_state_circuit(cirq.GridQubit.rect(1, 8)[0::2]))
```

```python
def one_qubit_unitary(bit, symbols):
    """Make a Cirq circuit enacting a rotation of the bloch sphere about the X,
    Y and Z axis, that depends on the values in `symbols`.
    """
    return cirq.Circuit(
        cirq.X(bit)**symbols[0],
        cirq.Y(bit)**symbols[1],
        cirq.Z(bit)**symbols[2])

def two_qubit_unitary(bits, symbols):
    """Make a Cirq circuit that creates an arbitrary two qubit unitary."""
    circuit = cirq.Circuit()
    circuit += one_qubit_unitary(bits[0], symbols[0:3])
    circuit += one_qubit_unitary(bits[1], symbols[3:6])
    circuit += [cirq.ZZ(*bits)**symbols[7]]
    circuit += [cirq.YY(*bits)**symbols[8]]
    circuit += [cirq.XX(*bits)**symbols[9]]
    circuit += one_qubit_unitary(bits[0], symbols[9:12])
    circuit += one_qubit_unitary(bits[1], symbols[12:])
    return circuit

def two_qubit_pool(source_qubit, sink_qubit, symbols):
    """Make a Cirq circuit to do a parameterized 'pooling' operation, which
    attempts to reduce entanglement down from two qubits to just one."""
    pool_circuit = cirq.Circuit()
    sink_basis_selector = one_qubit_unitary(sink_qubit, symbols[0:3])
    source_basis_selector = one_qubit_unitary(source_qubit, symbols[3:6])
    pool_circuit.append(sink_basis_selector)
    pool_circuit.append(source_basis_selector)
    pool_circuit.append(cirq.CNOT(control=source_qubit, target=sink_qubit))
    pool_circuit.append(sink_basis_selector**-1)
    return pool_circuit
```

```python
def quantum_conv_circuit(bits, symbols):
    """Quantum Convolution Layer following the above diagram.
    Return a Cirq circuit with the cascade of `two_qubit_unitary` applied
    to all pairs of qubits in `bits` as in the diagram above.
    """
    circuit = cirq.Circuit()
    for first, second in zip(bits[0::2], bits[1::2]):
        circuit += two_qubit_unitary([first, second], symbols)
    for first, second in zip(bits[1::2], bits[2::2] + [bits[0]]):
        circuit += two_qubit_unitary([first, second], symbols)
    return circuit
```

```python
qubits=cirq.GridQubit.rect(1, 8)[0::2][0]
symbols = sympy.symbols('qconv0:63')
xxx=two_qubit_pool(cirq.GridQubit.rect(1, 8)[0::2][0],cirq.GridQubit.rect(1, 8)[0::2][1], symbols[0:12])
SVGCircuit(xxx)
```

```python
def quantum_pool_circuit(source_bits, sink_bits, symbols):
    """A layer that specifies a quantum pooling operation.
    A Quantum pool tries to learn to pool the relevant information from two
    qubits onto 1.
    """
    circuit = cirq.Circuit()
    for source, sink in zip(source_bits, sink_bits):
        circuit += two_qubit_pool(source, sink, symbols)
    return circuit
```

```python
qubits=cirq.GridQubit.rect(1, 8)[0::2][0]
symbols = sympy.symbols('qconv0:63')
xxx=quantum_pool_circuit(cirq.GridQubit.rect(1, 8)[:4],cirq.GridQubit.rect(1, 8)[4:], symbols[0:12])
SVGCircuit(xxx)
```

```python
qubits=cirq.GridQubit.rect(1, 8)
symbols = sympy.symbols('qconv0:63')
xxx=quantum_conv_circuit(qubits, symbols[0:15])
```

```python
SVGCircuit(xxx)
```

```python
def quantum_conv_circuit(bits, symbols):
    """Quantum Convolution Layer following the above diagram.
    Return a Cirq circuit with the cascade of `two_qubit_unitary` applied
    to all pairs of qubits in `bits` as in the diagram above.
    """
    circuit = cirq.Circuit()
    for first, second in zip(bits[0::2], bits[1::2]):
        circuit += two_qubit_unitary([first, second], symbols)
    for first, second in zip(bits[1::2], bits[2::2] + [bits[0]]):
        circuit += two_qubit_unitary([first, second], symbols)
    return circuit
```

```python
cirq.GridQubit.rect(1, 8)[0::2][0]
```

    cirq.GridQubit(0, 0)

```python
def quantum_pool_circuit(source_bits, sink_bits, symbols):
    """A layer that specifies a quantum pooling operation.
    A Quantum pool tries to learn to pool the relevant information from two
    qubits onto 1.
    """
    circuit = cirq.Circuit()
    for source, sink in zip(source_bits, sink_bits):
        circuit += two_qubit_pool(source, sink, symbols)
    return circuit
```

```python
def create_model_circuit(qubits):
    """Create sequence of alternating convolution and pooling operators
    which gradually shrink over time."""
    model_circuit = cirq.Circuit()
    symbols = sympy.symbols('qconv0:21')
    model_circuit += quantum_conv_circuit(qubits, symbols[0:15])
    model_circuit += quantum_pool_circuit(qubits[:4], qubits[4:],
                                          symbols[15:21])
    return model_circuit

# Create our qubits and readout operators in Cirq.
cluster_state_bits = cirq.GridQubit.rect(1, 8)
readout_operators = cirq.Z(cluster_state_bits[-1])

# Build a sequential model enacting the logic in 1.3 of this notebook.
# Here you are making the static cluster state prep as a part of the AddCircuit and the
# "quantum datapoints" are coming in the form of excitation
excitation_input = tf.keras.Input(shape=(), dtype=tf.dtypes.string)
cluster_state = tfq.layers.AddCircuit()(
    excitation_input, prepend=cluster_state_circuit(cluster_state_bits))

quantum_model = tfq.layers.PQC(create_model_circuit(cluster_state_bits),
                               readout_operators)(cluster_state)

qcnn_model = tf.keras.Model(inputs=[excitation_input], outputs=[quantum_model])

# Show the keras plot of the model
tf.keras.utils.plot_model(qcnn_model,
                          show_shapes=True,
                          show_layer_names=False,
                          dpi=70)
```

```python
qcnn_model.summary()
```

    Model: "model"
    _________________________________________________________________
     Layer (type)                Output Shape              Param #
    =================================================================
     input_2 (InputLayer)        [(None,)]                 0

     add_circuit_1 (AddCircuit)  (None,)                   0

     pqc_1 (PQC)                 (None, 1)                 20

    =================================================================
    Total params: 20
    Trainable params: 20
    Non-trainable params: 0
    _________________________________________________________________

Quantum Convolutional Neural Networks/QCNN_with_cirq_and_tf.ipynb