# Problema # 1 -Equilibrio de la carga de trabajo-

# Parseador de funciones

### Katherine Yohanna Mazariegos Guerra
### Abner Xocop Chacach

```python
# En archivo exercise01.txt x1, x2 y x3 es el nombre de las variables, se usaron esas para seguir con el estándar de nombramiento en Programación Lineal.
# Es necesario escribir si es min/max para que el programa identifique si es de maximización o minimización, st para indicar que esas son las restricciones y end para indicar al programa que el problema termina ahí

def parse_coefficients(coefficient_list, monomial):
    """
    Este es un parseador de coeficientes. Consiste en comprobar si una cadena tiene una expresión regular en donde pueda extraer caracteres específicos, en este caso se busca extraer los coeficientes.

    Args:
        :rtype: None
        :param coefficient_list: Lista en la que se almacenarán los coeficientes
        :param monomial: Una cadena (por ejemplo, -3x1) que será analizada hasta su coeficiente (por ejemplo, -3)

    Verifica qué patrón coincide. Válidos son: (s)(n)lv
        Los paréntesis indican la existencia opcional
        s es + o - (la ausencia significa +)
        n es un número (coeficiente, la ausencia significa 1)
        l es una letra latina minúscula (letra variable)
        v es un número, probablemente incremental (número variable)

    Import re:
        Una expresión regular (o RE) especifica un conjunto de cadenas que se corresponde con ella; las funciones de este módulo le permiten comprobar si una cadena particular se corresponde con una expresión regular dada (o si una expresión regular dada se corresponde con una cadena particular, que se reduce a lo mismo)
        Source: https://docs.python.org/3/library/re.html
    """
    import re

    if re.match('[ ]*[\+ ]?[\d]+[\.]?[\d]*', monomial):
        float_cast = float(re.match('[ ]*[\+ ]?[\d]+[\.]?[\d]*', monomial).group(0))
        coefficient_list.append(float_cast)
    elif re.match('[ ]*[\-][\d]+[\.]?[\d]*', monomial):
        float_cast = float(re.match('[ ]*[\-][\d]+[\.]?[\d]*', monomial).group(0))
        coefficient_list.append(float_cast)
    elif re.match('[ ]*[\+]*[a-z][\d]+', monomial):
        coefficient_list.append(1)
    elif re.match('[ ]*[\-][a-z][\d]+', monomial):
        coefficient_list.append(-1)
```

```python
lines = []
def parse_lp1(input_filename):
    """
    Esta función es la encargada de leer el archivo, hará uso del parser para mandar línea a línea el contenido del .txt y según los coeficientes que obtenga devolverá las matrices y arrays correspondientes.

    Tiene tareas como verificar que el archivo haya sido encontrado, leer si es un problema de maximización o minimización, llenar las matrices/ arrays.

    :rtype : tuple
    :param input_filename: Nombre de archivo de la entrada del problema lineal
    :return: Retorna A-matrix, b-vector, c-vector, MinMax
    """
    import re

    error = 0 # Inicializar la variable de error. Si el error!=0 entonces hubo un problema de entrada de archivo

    try:
        infile = open('exercise01.txt')
    except FileNotFoundError:
        error = 1
        print('\nInput file error: Archivo no encontrado.') # Archivo no encontrado

    #lines = []
    if error != 1:
        for line in infile:
            lines.append(line)
        infile.close()
    for line in lines:
        print(line, end='')

    minmax_line = '' # Verficar si el problema es de maximización o de minimización
    for line in lines:
        if re.match('^[ ]*max|min', line):
            minmax_line = line

    minmax = 0
    objective_function = ''

    if re.match('^[ ]*max', minmax_line): #Si en el archivo se encuentra la palabra 'max' entonces el problema es de maximización
        minmax = -1
        objective_function = minmax_line
        objective_function = objective_function.strip('max')

    elif re.match('^[ ]*min', minmax_line): # Si en el archivo se encuentra la palabra 'min' entonces el problema es de minimización
        minmax = 1
        objective_function = minmax_line
        objective_function = objective_function.strip('min')

    if minmax_line == '' and minmax == 0: # Si en el archivo no se encuentra ni 'max' ni  'min' entonces no hay función objetivo
        error = 2
        print('\nInput file error: Función objetivo no encontrada.')

    c_vector = [] # Rellenar el vector c con coeficientes de función objetiva

    regex = re.compile('^[\+\- ]?[\d]*[\.]?[\d]*[a-z][\d+]')
    while regex.match(objective_function):
        monomial = regex.match(objective_function).group(0)
        parse_coefficients(c_vector, monomial)
        objective_function = objective_function.replace(monomial, '', 1)

    a_matrix = [] # Rellenar la matriz A (coeficientes) y el vector b utilizando las restricciones del problema
    b_vector = []
    eqin = []

    st_line = ''
    st_index = 0
    for index, line in enumerate(lines):
        if 'st' in line:
            st_index = index
            st_line = line

    if re.match('^[ ]*st', st_line):
        st_line = st_line.replace('st', '  ', 1)

    if st_line == '':
        error = 3
        print('\nInput file error: Línea de restricciones no encontrada. No existe la keyword \'st\'.')

    while st_index < len(lines) - 1:
        sub_a_vector = []
        a_matrix.append(sub_a_vector)
        while True:
            st_line = st_line.strip(' ')
            if re.match('^[\+\- ]?[\d]*[\.]?[\d]*[a-z][\d+]', st_line):
                monomial = re.match('^[\+\- ]?[\d]*[\.]?[\d]*[a-z][\d+]', st_line).group(0)
                parse_coefficients(sub_a_vector, monomial)
                st_line = st_line.replace(monomial, '', 1)
            elif re.match('^[<>=]+', st_line):
                monomial = re.match('^[<>=]+', st_line).group(0)
                if monomial == '<=':
                    eqin.append(-1)
                elif monomial == '>=':
                    eqin.append(1)
                elif monomial == '=':
                    eqin.append(0)
                else:
                    error = 4
                    print('\nInput file error: Caracter inesperado; esperados <=, >=, = al menos', monomial)
                st_line = st_line.replace(monomial, '', 1)
            elif re.match('^[\d]+', st_line):
                monomial = re.match('^[\d]+', st_line).group(0)
                int_cast = int(re.match('^[\d]+', st_line).group(0))
                b_vector.append(int_cast)
                st_line = st_line.replace(monomial, '', 1)
            else:
                if not sub_a_vector:    # Evalúa true cuando las líneas están vacías entre las restricciones
                    a_matrix.pop()
                break

        st_index += 1 #         Incrementar el número de línea y obtener el siguiente
        st_line = lines[st_index]

        if st_line == 'end\n' and error == 0: #         Búsqueda de la declaración final y ausencia de errores
            print('\nArchivo cargado exitosamente.')
            break

    return a_matrix, b_vector, c_vector, eqin, minmax # Devolver todas las listas y variables creadas
```

```python
def convert_to_dual(input_filename, output_filename):
    """
    Verifica si son restricciones de >=, <= o =. También tiene como tarea hacer un archivo de salida en el que muestre los resultados de las matrices que se llenaron.

    :param input_filename: Nombre de archivo de la entrada del problema lineal
    :param output_filename: Filename of the linear problem output
    :return: Returns A-matrix, b-vector, c-vector, Variable-constraints, MinMax
    """

    (a_matrix, b_vector, c_vector, eqin, minmax) = parse_lp1(input_filename)     # Llamar la función parse_lp1

    variable_constraints = [] # Convertir las restricciones a equivalentes duales '*' significa libre
    if minmax == -1:
        for el in eqin:
            if el == 0:
                variable_constraints.append('*')
            elif el == 1:
                variable_constraints.append('>=0')
            elif el == -1:
                variable_constraints.append('<=0')

    a_matrix = list(zip(a_matrix)) # Traspuesta de A-matrix

    minmax = -minmax # min(max) el problema dual es max(min)

    outfile = open(output_filename, 'w') # Escribir el problema a un archivo de salida
    outfile.write('(Objective Function) b-vector: [' + ', '.join(map(str, b_vector)) + ']\n')

    outfile.write('\nA-matrix: [')
    thing = ''
    for index, sub_a_vector in enumerate(a_matrix):
        thing += '[ ' + ', '.join(map(str, sub_a_vector)) + ']'
        if index != (len(a_matrix) - 1):
            thing += ', '
    outfile.write(thing + ']\n')

    outfile.write('\n(Contraints) c-vector: [' + ', '.join(map(str, c_vector)) + ']\n')
    outfile.write('\n(Variable Contraints) variable_constraints-vector: [' + ', '.join(map(str, c_vector)) + ']\n')
    outfile.write('\nEqin: [' + ', '.join(map(str, eqin)) + ']\n')
    outfile.write('\nMinMax: [' + str(minmax) + ']\n')
    outfile.close()

    return a_matrix, b_vector, c_vector, variable_constraints, eqin, minmax

(a_matrix, b_vector, c_vector, variable_contraints, eqin, minmax) = convert_to_dual('input-lp1', 'output-lp2')
```

    min 42x1+87x2
    st 4x1+2x2<=480
       3x1+6x2<=480
    end

# Imports

```python
import numpy as np
import math
from sympy.abc import x,y
from sympy import *
```

# Lista con todas las soluciones posibles

```python
lista = []
```

# Soluciones(Interceptos) en x

```python
#Soluciones(Interceptos) en x
def x_intercepts():

    """
    Esta función nos ayuda a encontrar los interceptos en el eje x, almacenarlos en una matriz y verificar cuál de estos interceptos pordría ser una solución óptima para el proyecto.
    """

    new_mat= np.array(a_matrix).reshape(2,2) # Pasar los valores a una matriz operable
    new_b= np.array(b_vector).reshape(2,1)
    sol = np.linalg.solve(new_mat, new_b) # Resolver para la matriz

    new_mat[1,1]=0 # Únicamente se tomarán los valores de x
    new_mat[0,1]=0
    new_mat_2=[new_mat[0,0], 0],[0, new_mat[1,0]]
    new_mat_2 = np.array(new_mat_2)
    sol = np.linalg.solve(new_mat_2,new_b)
    #new_mat_2= [new_mat[0,1],new_mat[1,1]]

    x_intercepts=[[sol[0][0],0],[sol[1][0],0]]
    #print(x_intercepts)
    return x_intercepts

#x_intercepts()
x_i_min = math.inf

for i in x_intercepts():
    if i[0] < x_i_min:
        x_i_min = i[0]

lista.append([x_i_min, 0])
print(lista)

```

    [[120.0, 0]]

# Método de validación de Restricciones

```python
equations_evalued = []
def check(x,y):
    for i in range(1,len(lines)-5): # recorremos el array de strings para convertir cada ecuación str a symbolic.

        string_eq = lines[i].replace("max ",'').replace('min ','').replace("st ",'').replace("end",'').replace('\n','').replace(" ",'').replace("x1","*x").replace("x2","*y")
        if eval(string_eq):
            equations_evalued.append(string_eq) # añadimos cada restricción sym a un arreglo
        else:
            pass

    return (equations_evalued[0]) and (equations_evalued[1]) and (x>=0) and (y>=0) # retornamos un booleano que para chequear cada punto cuando se invoque la func check()

```

# Soluciones(Interceptos) en y

```python
#Soluciones(Interceptos) en y
def y_intercepts():

    """
    Esta función nos ayuda a encontrar los interceptos en el eje y, almacenarlos en una matriz y verificar cuál de estos interceptos pordría ser una solución óptima para el proyecto.
    """

    new_mat= np.array(a_matrix).reshape(2,2) # Pasar los valores a una matriz operable
    new_b= np.array(b_vector).reshape(2,1)
    sol = np.linalg.solve(new_mat, new_b) # Resolver para la matriz

    new_mat[0,0]=0 # Únicamente se tomarán los valores de y
    new_mat[1,0]=0
    new_mat_2=[0,new_mat[0,1]],[new_mat[1,1],0]
    new_mat_2 = np.array(new_mat_2)
    sol = np.linalg.solve(new_mat_2,new_b)
    y_intercepts=[[0, sol[0][0]],[0,sol[1][0]]]
    #print(y_intercepts)
    return y_intercepts

#y_intercepts()

y_i_min = math.inf

for i in y_intercepts():
    if i[1] < y_i_min:
        y_i_min = i[1]

lista.append([0, y_i_min]) # Almacena el elemento en la lista principal
print(lista)
```

    [[120.0, 0], [0, 80.0]]

# Restricción de no negatividad

```python
#Funcion 0,0
def p_origen():

    """
    El objetivo de tener esta función es de ingreser el origen (0, 0) dentro de la lista principal para ser verificado si es el punto óptimo, es decir que el origen también es tomado como un candidato.
    """

    origen = [0,0]
    #print(origen)
    lista.append(origen)
print(lista)
```

    [[120.0, 0], [0, 80.0]]

# Soluciones de las restricciones

```python
def solve_equation():

    """
    Aquí se resuelve la intersección entre las dos restricciones ingresadas. Los puntos (x, y) obtenidos son ingresados a la lista principal de candidatos a ser puntos óptimos
    """

    #print(a_matrix)
    new_mat= np.array(a_matrix).reshape(2,2)
    new_b= np.array(b_vector).reshape(2,1)
    sol = np.linalg.solve(new_mat, new_b)
    return sol

solve_e = [[solve_equation()[0][0], solve_equation()[1][0]]]
print(solve_e[0])

lista.append(solve_e[0])
print(lista)

```

    [106.66666666666667, 26.666666666666668]
    [[120.0, 0], [0, 80.0], [106.66666666666667, 26.666666666666668]]

# Coeficientes función objetivo

```python
# Se imprimen los coeficientes de la función objetivo
print(c_vector)
```

    [42.0, 87.0]

```python
solved = False

maxi = 0
x_max = 0
y_max = 0
```

# Resultados

```python
solved = False

maxi = 0
x_max = 0
y_max = 0

def resultado():
    global maxi
    for each_point in lista:
        #print(f"each_point {each_point}")
        solved = check(each_point[0],each_point[1])
        #print(f"eval_eq_arr {equations_evalued}")
      #  print(check(each_point[0],each_point[1]))

        if solved is False:
            lista.remove(each_point[0],each_point[1])
            solved = True

    if solved:
        """
        En esta función se hacen las verificaciones para saber cuál de los puntos candidatos hacen la optimización de la función objetivo. La variable maxi es la que indica la maximización, y_max y x_max son los puntos que optimizan la función con las restricciones dadas
        """

        for i in lista:
            temporal = c_vector[0]*i[0]+c_vector[1]*i[1]
            if temporal > maxi:
                maxi = temporal
                print(f"temporal: {temporal}")
                y_max = i[1]
                x_max = i[0]

        print("\n\n=== Resultados ===")

        print('c-vector:', c_vector)
        print('A-matrix:', a_matrix)
        print('b-vector:', b_vector)
        print('Variable-contraints-vector:', variable_contraints)
        #print('Eqin:', eqin)
        print('MinMax:', minmax)
        print('Punto y:', y_max)
        print("Punto x: ", x_max)
        print('Resultado optimización:', maxi)

    else:
        print("ERROR PLS FIX ME :(")

(a_matrix, b_vector, c_vector, variable_contraints, eqin, minmax) = convert_to_dual('input-lp1', 'output-lp2')

resultado()
print(f"\nmaxi {maxi}")

```

    min 42x1+87x2
    st 4x1+2x2<=480
       3x1+6x2<=480
    endmin 42x1+87x2
    st 4x1+2x2<=480
       3x1+6x2<=480
    endtemporal: 5040.0
    temporal: 6960.0

    === Resultados ===
    c-vector: [42.0, 87.0]
    A-matrix: [([4.0, 2.0],), ([3.0, 6.0],)]
    b-vector: [480, 480]
    Variable-contraints-vector: []
    MinMax: -1
    Punto y: 80.0
    Punto x:  0
    Resultado optimización: 6960.0

    maxi 6960.0

# Sensibilidad

```python
"""
Esto únicamente imprime las matrices importantes para la solución del problema.
    c-vector es para el vector de la función objetivo
    A-matrix es la matriz que contiene las restricciones
    b-vector es el vector que contiene los coeficientes del lado derecho de las restricciones
    MinMax indica si es maximización o minimización
"""

print('\n===Results===')

print('c-vector:', c_vector)
print('A-matrix:', a_matrix)
print('b-vector:', b_vector)
#print('Variable-contraints-vector:', variable_contraints)
#print('Eqin:', eqin)
print('MinMax:', minmax)
```

    ===Results===
    c-vector: [42.0, 87.0]
    A-matrix: [([4.0, 2.0],), ([3.0, 6.0],)]
    b-vector: [480, 480]
    Variable-contraints-vector: []
    Eqin: [-1, -1]
    MinMax: -1

```python
"""
La matriz A-matrix generada en pasos anteriores se tiene lo siguiente
A-matrix: [([4.0, 2.0],), ([3.0, 6.0],)]

Aquí lo que se hace es hacer dos matrices nuevas, una llamada a_matrix_const1 para que contenga a [4.0, 2.0] y otra llamada
a_matrix_const2 = a_matrix[1][0] que contiene los valores [3.0, 6.0]
"""

a_matrix_const1 = a_matrix[0][0]
print(a_matrix_const1)
a_matrix_const2 = a_matrix[1][0]
print(a_matrix_const2)
a_matrix_const1[1]
```

    [4.0, 2.0]
    [3.0, 6.0]

    2.0

```python
"""
Aquí se calculan las pendientes de los constraints y la pendiente de la función objetivo
    m_const1 es la pendiente de la constraint 1
    m_const2 es la pendiente de la constraint 2
    m_c1 es la pendiente de la función objetivo, siendo c1 el primer coeficiente de la función
    m_c2 es la pendiente de la función objetivo, siendo c2 el segundo coeficiente de la función

Es MENESTER mencionar que c1 es el primer coeficiente de la función objetivo, por lo que todo lo que a continuación se menciona
como c1, se refiere a este número. c2 es el segundo coeficiente de la función objetivo, al igual que c1, a partir de aquí
c2 se refiere al número indicado.
"""

m_const1 = -1 * a_matrix_const1[0]/a_matrix_const1[1]

m_const2 = -1 * a_matrix_const2[0]/a_matrix_const2[1]

m_c1 = -1 / c_vector[1]

m_c2 = -c_vector[0]

print(f"m_const1: {m_const1}")
print(f"m_const2: {m_const2}")
print(f"m_c1: {m_c1}")
print(f"m_c2: {m_c2}")
```

    m_const1: -2.0
    m_const2: -0.5
    m_c1: -0.011494252873563218
    m_c2: -42.0

```python
"""
Aquí se analizan los rangos en los que c1 y c2 son válidos. Lo que hace es analizar las pendientes para c1 y c2 contra
las pendientes de las restricciones 1 y 2. Al dividirse, se obtiene tanto el límite superior como el límite inferior permitido
tanto para c1 como para c2
"""

limit_lo_c1 = m_const1 / m_c1

limit_up_c1 = m_const2 / m_c1

limit_lo_c2 = m_c2 / m_const2

limit_up_c2 = m_c2 / m_const1

#print(f"Límite c1 permitido:\n{limit_lo_c1}")
#print(f"{limit_up_c1}")
#print(f"Límite c2 permitido:\n{limit_lo_c2}")
#print(f"{limit_up_c2}")
```

    Límite c1 permitido:
    174.0
    43.5
    Límite c2 permitido:
    84.0
    21.0

```python
"""
Aquí se calculan los decreases and increases permitidos. Lo que hace es tomar el valor de c1, evalúa el límite superior
obtenido, la diferencia obtenida es el increase permitido. Luego toma el mismo valor de c1, evalúa el límite inferior, la
diferencia entre ambos es el decrease permitido. Luego evalúa el valor de c2 y hace lo mismo en los límites para calcular el
increase y decrease permitidos para c2.
"""

#1
allow_increasex = limit_up_c1 - c_vector[0]
if allow_increasex < 0:
    print(f"Incremento en x permitido: infinito")
else:
    print(f"allow_increasex: {allow_increasex}")
#2
allow_decreasex = c_vector[0] - limit_lo_c1

if allow_decreasex < 0:
    print(f"allow_decreasex: infinito")
else:
    print(f"allow_decreasex: {allow_decreasex}")

#3
allow_increasey = limit_up_c2 - c_vector[1]
if allow_increasey < 0:
    print(f"allow_increasey: infinito")
else:
    print(f"allow_increasey: {allow_increasey}")

    #4
allow_decreasey = c_vector[1] - limit_lo_c2
if allow_decreasey < 0:
    print(f"allow_decreasey: infinito")
else:
    print(f"allow_decreasey: {allow_decreasey}")

```

    allow_increasex: 1.5
    allow_decreasex: infinito
    allow_increasey: infinito
    allow_decreasey: 3.0


```python
from IPython.display import HTML
tag = HTML('''
Toggle cell visibility <a href="javascript:code_toggle()">here</a>.''')
display(tag)
```

Toggle cell visibility here.

```python
# Erasmus+ ICCT project (2018-1-SI01-KA203-047081)

%matplotlib notebook
import numpy as np
import math
import matplotlib.pyplot as plt
from scipy import signal
import ipywidgets as widgets
import control as c
import sympy as sym
from IPython.display import Latex, display, Markdown # For displaying Markdown and LaTeX code
from fractions import Fraction
import matplotlib.patches as patches
```

## First-order systems with no zeros

### Introduction

First-order systems without zeros are characterized by the following transfer function:

\begin{equation}
    G(s)=\frac{k}{s+k}.
\end{equation}

The $k$ value is important since it defines the following parameters:
- $1/k$ denotes the *time constant* of the response, which defines the time needed for the step response to reach $\approx$ 63% of its final value.
- $t_r$ denotes the *rise time*, i.e. the time needed for the system response to go from 10\% to 90\% of the steady state value.
- $t_s$ denotes the *settling time*, i.e. the time at which the system response is outside the error band (e.g. 2\% as set in the example below) for the last time.

The step response of these systems is given by:

\begin{equation}
    c(t)=1-e^{-at},
\end{equation}

where the forced response is equal to $1$ and natural response to $-e^{-at}$.

---

### How to use this notebook?

Move the slider to define the $k$ value in the transfer function of the first-order system - $G(s)=\frac{k}{s+k}$ and observe the unit step time response of the defined system.

```python
# set up plot
fig, ax = plt.subplots(figsize=[9.8,4],num='First-order system')
ax.set_ylim([-1, 2])
ax.set_xlim([0, 5])
ax.grid(True)
ax.set_title ('Time response')
ax.set_xlabel('$t$ [s]')
ax.set_ylabel('Input, output')
xaxis = ax.axhline(y=0,color='k',lw=1)

response, = ax.plot([], [])
slope, = ax.plot([], [])
x1a, = ax.plot([], [])
y1a, = ax.plot([], [])
tr11, = ax.plot([], [])
trv1, = ax.plot([], [])
trv2, = ax.plot([], [])
trh1, = ax.plot([], [])
trh2, = ax.plot([], [])
ts11, = ax.plot([], [])
ts1, = ax.plot([], [])
ts2, = ax.plot([], [])
texttr=ax.text(0,0,'')
textts=ax.text(0,0,'')

ax.step([0,5],[0,1],color='C0',label='input')

# generate x values
t = np.linspace(0, 2 * np.pi, 10000)

def response_func(t, k):
    """"Return response function"""
    return 1-np.exp(-k*t)

@widgets.interact(k=(1, 5, 1))

def update(k=1):
    """Remove old lines from plot and plot new one"""
    global response,slope,x1a,y1a,tr11,trv1,trv2,trh1,trh2,ts11,ts1,ts2,texttr,textts
    ax.lines.remove(response)
    ax.lines.remove(slope)
    ax.lines.remove(x1a)
    ax.lines.remove(y1a)
    ax.lines.remove(tr11)
    ax.lines.remove(trv1)
    ax.lines.remove(trv2)
    ax.lines.remove(trh1)
    ax.lines.remove(trh2)
    ax.lines.remove(ts11)
    ax.lines.remove(ts1)
    ax.lines.remove(ts2)
    texttr.remove()
    textts.remove()
    response, = ax.plot(t, response_func(t,k), color='C1',lw=2)
    response.set_label('output')
    slope, = ax.plot([0,1/k], [0,1], color='C2',lw=2)
    slope.set_label('initial slope')
    x1a, = ax.plot([1/k,1/k],[0,1-np.exp(-1)],'--',color='k',lw=.8)
    y1a, = ax.plot([0,1/k],[1-np.exp(-1),1-np.exp(-1)],'--',color='k',lw=.8)
# rise time
    tr11, = ax.plot([-np.log(0.9)/k,-np.log(0.1)/k],[-0.5,-0.5],color='k',lw=.8)
    trv1, = ax.plot([-np.log(0.9)/k,-np.log(0.9)/k],[-0.5,0.1],'--',color='k',lw=.8)
    trv2, = ax.plot([-np.log(0.1)/k,-np.log(0.1)/k],[-0.5,0.9],'--',color='k',lw=.8)
    trh1, = ax.plot([0,-np.log(0.9)/k],[0.1,0.1],'--',color='k',lw=.8)
    trh2, = ax.plot([0,-np.log(0.1)/k],[0.9,0.9],'--',color='k',lw=.8)
# settling time
    ts11, = ax.plot([0,-np.log(0.02)/k],[-0.7,-0.7],color='k',lw=.8)
    ts1, = ax.plot([0,0],[-0.7,0],'--',color='k',lw=.8)
    ts2, = ax.plot([-np.log(0.02)/k,-np.log(0.02)/k],[-0.7,0.98],'--',color='k',lw=.8)
    ax.legend()
    texttr=ax.text((-np.log(0.1)/k-(-np.log(0.9)/k))/2,-0.45, '$t_r$',fontsize=13)
    textts=ax.text((-np.log(0.02)/k)/2-0.1,-0.65, '$t_s$',fontsize=13)

    plt.xticks([0,1/k,2,4], [0,'${1}/{%s}$'%k,2,4],fontsize=8)
    plt.yticks([0.1,0.5,0.63,0.9,1,1.5,2], [0.1,0.5,0.63,0.9,1,1.5,2],fontsize=8)

    num1=[k]
    den1=[1,k]
    display(Markdown('Transfer function of the system $G(s)$ is equal to:'))
    tf_sys1=c.TransferFunction(num1,den1)
    s=sym.Symbol('s')
    eq=(k/(s+k))
    display(eq)
```

    <IPython.core.display.Javascript object>

    interactive(children=(IntSlider(value=1, description='k', max=5, min=1), Output()), _dom_classes=('widget-inte…

```python

```

## Second-order systems

### Introduction

In contrast to the first-order systems presented above, in which the parameter $k$ only affected the speed of the response, changes of the analogue parameters in the second order systems may affect the actual form of the response. The following four responses are possible in these systems:
- *overdamped* response,
- *underdamped* response,
- *undapmed* response, and
- *critically damped* response.

### How to use this notebook?

Move the slider to define the values of $a$ and $b$ in the transfer function of the second-order system of the form $G(s)=\frac{b}{s^2+as+b}$ and observe the pole-zero plot and the unit step time response of the defined system.

```python
# set up plot
fig1, ax1 = plt.subplots(1,2,figsize=[9.8,4],num='Second-order system')
ax1[0].set_ylim([-3.5, 3])
ax1[1].set_ylim([0, 2.5])
# ax1.set_xlim([0, 5])
ax1[0].grid(True)
ax1[1].grid(True)
ax1[0].axhline(y=0,color='k',lw=.8)
ax1[1].axhline(y=0,color='k',lw=.8)
ax1[0].axvline(x=0,color='k',lw=.8)
ax1[1].axvline(x=0,color='k',lw=.8)
ax1[0].set_xlabel('Re')
ax1[0].set_ylabel('Im')
ax1[1].set_xlabel('$t$ [s]')
ax1[1].set_ylabel('Input, output')
ax1[0].set_title('Pole-zero plot')
ax1[1].set_title('Time response')

t = np.linspace(0, 20, 10000)

textGs = ax1[0].text(0,0,'')

ax1[1].step([0,20],[0,1],color='C0',label='input')

plotzero, = ax1[0].plot([], [])
response2, = ax1[1].plot([], [])

def response_func2(t, a, b):
    num_sys=np.array([b])
    den_sys=np.array([1,a,b])
    tf_sys=c.TransferFunction(num_sys,den_sys)
    poles_sys,zeros_sys=c.pzmap(tf_sys, Plot=False)
    T, yout = c.step_response(tf_sys,t)
    return T, yout, poles_sys, tf_sys

@widgets.interact(a=(0, 10, 1),b=(1,10,1))

def update(a=7,b=9):
    """ Update plots """
    global response2, plotzero, textGs
    ax1[0].lines.remove(plotzero)
    ax1[1].lines.remove(response2)
#     textGs.remove()
    T, yout, poles_sys, tf_sys = response_func2(t, a, b)
    plotzero, = ax1[0].plot(np.real(poles_sys), np.imag(poles_sys), 'xg', markersize=10, label = 'Poles')
#     textGs = ax1[0].text(-7,1,tf_sys)
    response2, = ax1[1].plot(T,yout,color='C1',label='output')
    s=sym.Symbol('s')
    eq=b/(s**2+a*s+b)
    coeff = [1,a,b]
    rootsdenom=np.roots(coeff)
    eq2=b/((s-rootsdenom[0])*(s-rootsdenom[1]))
    display(Markdown('Transfer function of the system $G(s)$ is equal to:'))
    display(eq),display(Markdown('or')),display(eq2)

    if np.imag(poles_sys)[0] == 0 and np.imag(poles_sys)[1] == 0 and np.real(poles_sys)[0] < 0 and np.real(poles_sys)[1] < 0 and np.real(poles_sys)[0]!=np.real(poles_sys)[1]:
        display(Markdown('The system is **overdamped**, because both poles have only negative real parts.'))
    elif math.isclose(0, np.imag(poles_sys)[0], abs_tol=10**-6) and math.isclose(0, np.imag(poles_sys)[1], abs_tol=10**-6) and np.real(poles_sys)[1] < 0 and np.real(poles_sys)[0]==np.real(poles_sys)[1]:
        display(Markdown('The system is **critically damped** beacuse there is a double pole with negative real part only.'))
    elif np.real(poles_sys)[0] == 0 and np.real(poles_sys)[1] == 0:
        display(Markdown('The system is **undamped**, because the poles have only imaginary parts.'))
    elif np.imag(poles_sys)[0] != 0 and np.imag(poles_sys)[1] != 0 and np.real(poles_sys)[0] != 0 and np.real(poles_sys)[1] != 0:
        display(Markdown('The system is **underdamped** beacuse both poles have negative real and non-zero complex part.'))
    ax1[0].legend()
    ax1[1].legend()
```

    <IPython.core.display.Javascript object>

    interactive(children=(IntSlider(value=7, description='a', max=10), IntSlider(value=9, description='b', max=10,…


# 5 - Convolutional Sequence to Sequence Learning

In this notebook we'll be implementing the [Convolutional Sequence to Sequence Learning](https://arxiv.org/abs/1705.03122) model.

## Introduction

This model is drastically different to the previous models used in these tutorials. There is are no recurrent components used at all. Instead it makes use of convolutional layers, typically used for image processing. For an introduction to convolutional layers on text for sentiment analysis, see [this](https://github.com/bentrevett/pytorch-sentiment-analysis/blob/master/4%20-%20Convolutional%20Sentiment%20Analysis.ipynb) tutorial.

In short, a convolutional layer uses *filters*. These filters have a *width* (and also a *height* in images, but usually not text). If a filter has a width of 3, then it can see 3 consecutive tokens. Each convolutional layer has many of these filters (1024 in this tutorial). Each filter will slide across the sequence, from beginning to the end, looking at all 3 consectuive tokens at a time. The idea is that each of these 1024 filters will learn to extract a different feature from the text. The result of this feature extraction will then be used by the model - potentially as input to another convolutional layer. This can then all be used to extract features from the source sentence to translate it into the target language.

## Preparing the Data

First, let's import all the required modules and set the random seeds for reproducability.

```python
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F

from torchtext.datasets import TranslationDataset, Multi30k
from torchtext.data import Field, BucketIterator

import matplotlib.pyplot as plt
import matplotlib.ticker as ticker

import spacy

import random
import math
import time
```

```python
SEED = 1234

random.seed(SEED)
torch.manual_seed(SEED)
torch.backends.cudnn.deterministic = True
```

Next, we'll load the spaCy models and define the tokenizers for the source and target languages.

```python
spacy_de = spacy.load('de')
spacy_en = spacy.load('en')
```

```python
def tokenize_de(text):
    """
    Tokenizes German text from a string into a list of strings
    """
    return [tok.text for tok in spacy_de.tokenizer(text)]

def tokenize_en(text):
    """
    Tokenizes English text from a string into a list of strings
    """
    return [tok.text for tok in spacy_en.tokenizer(text)]
```

Next, we'll set up the `Field`s which decide how the data will be processed. By default RNN models in PyTorch require the sequence to be a tensor of shape **[sequence length, batch size]** so TorchText will, by default, return batches of tensors in the same shape. However in this notebook we are using CNNs which expect the batch dimension to be first. We tell TorchText to have batches be **[batch size, sequence length]** by setting `batch_first = True`.

We also append the start and end of sequence tokens as well as lowercasing all text.

```python
SRC = Field(tokenize = tokenize_de,
            init_token = '<sos>',
            eos_token = '<eos>',
            lower = True,
            batch_first = True)

TRG = Field(tokenize = tokenize_en,
            init_token = '<sos>',
            eos_token = '<eos>',
            lower = True,
            batch_first = True)
```

Then, we load our dataset.

```python
train_data, valid_data, test_data = Multi30k.splits(exts=('.de', '.en'),
                                                    fields=(SRC, TRG))
```

We build our vocabulary as before, by converting any tokens that appear less than 2 times into `<unk>` tokens.

```python
SRC.build_vocab(train_data, min_freq = 2)
TRG.build_vocab(train_data, min_freq = 2)
```

The final bit of data preparation is defining the device and then building the iterator.

```python
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
```

```python
BATCH_SIZE = 128

train_iterator, valid_iterator, test_iterator = BucketIterator.splits(
    (train_data, valid_data, test_data),
     batch_size = BATCH_SIZE,
     device = device)
```

## Building the Model

Next up is building the model. As before, the model is made of an encoder and decoder. The encoder *encodes* the input sentence, in the source language, into a *context vector*. The decoder *decodes* the context vector to produce the output sentence in the target language.

### Encoder

Previous models in these tutorials had an encoder that compresses an entire input sentence into a single context vector, $z$. The convolutional sequence-to-sequence model is a little different - it gets two context vectors for each token in the input sentence. So, if our input sentence had 6 tokens, we would get 12 context vectors, two for each token.

The two context vectors per token are a *conved* vector and a *combined* vector. The conved vector is the result of each token being passed through a few layers - which we will explain shortly. The combined vector comes from the sum of the convolved vector and the embedding of that token. Both of these are returned by the encoder to be used by the decoder.

The image below shows the result of an input sentence - *zwei menschen fechten.* - being passed through the encoder.

First, the token is passed through a *token embedding layer* - which is standard for neural networks in natural language processing. However, as there are no recurrent connections in this model it has no idea about the order of the tokens within a sequence. To rectify this we have a second embedding layer, the *positional embedding layer*. This is a standard embedding layer where the input is not the token itself but the position of the token within the sequence - starting with the first token, the `<sos>` (start of sequence) token, in position 0.

Next, the token and positional embeddings are elementwise summed together to get a vector which contains information about the token and also its position with in the sequence - which we simply call the *embedding vector*. This is followed by a linear layer which transforms the embedding vector into a vector with the required hidden dimension size.

The next step is to pass this hidden vector into $N$ *convolutional blocks*. This is where the "magic" happens in this model and we will detail the contents of the convolutional blocks shortly. After passing through the convolutional blocks, the vector is then fed through another linear layer to transform it back from the hidden dimension size into the embedding dimension size. This is our *conved* vector - and we have one of these per token in the input sequence.

Finally, the conved vector is elementwise summed with the embedding vector via a residual connection to get a *combined* vector for each token. Again, there is a combined vector for each token in the input sequence.

### Convolutional Blocks

So, how do these convolutional blocks work? The below image shows 2 convolutional blocks with a single filter (blue) that is sliding across the tokens within the sequence. In the actual implementation we will have 10 convolutional blocks with 1024 filters in each block.

First, the input sentence is padded. This is because the convolutional layers will reduce the length of the input sentence and we want the length of the sentence coming into the convolutional blocks to equal the length of it coming into the convolutional blocks. Without padding, the length of the sequence coming out of a convolutional layer will be `filter_size - 1` shorter than the sequence entering the convolutional layer. For example, if we had a filter size of 3, the sequence will be 2 elements shorter. Thus, we pad the sentence with one padding element on each side. We can calculate the amount of padding on each side by simply doing `(filter_size - 1)/2` for odd sized filters - we will not cover even sized filters in this tutorial.

These filters are designed so the output hidden dimension of them is twice the input hidden dimension. In computer vision terminology these hidden dimensions are called *channels* - but we will stick to calling them hidden dimensions. Why do we double the size of the hidden dimension leaving the convolutional filter? This is because we are using a special activation function called *gated linear units* (GLU). GLUs have gating mechanisms (similar to LSTMs and GRUs) contained within the activation function and actually half the size of the hidden dimension - whereas usually activation functions keep the hidden dimensions the same size.

After passing through the GLU activation the hidden dimension size for each token is the same as it was when it entered the convolutional blocks. It is now elementwise summed with its own vector before it was passed through the convolutional layer.

This concludes a single convolutional block. Subsequent blocks take the output of the previous block and perform the same steps. Each block has their own parameters, they are not shared between blocks. The output of the last block goes back to the main encoder - where it is fed through a linear layer to get the conved output and then elementwise summed with the embedding of the token to get the combined output.

### Encoder Implementation

To keep the implementation simple, we only allow for odd sized kernels. This allows padding to be added equally to both sides of the source sequence.

The `scale` variable is used by the authors to "ensure that the variance throughout the network does not change dramatically". The performance of the model seems to vary wildly using different seeds if this is not used.

The positional embedding is initialized to have a "vocabulary" of 100. This means it can handle sequences up to 100 elements long, indexed from 0 to 99. This can be increased if used on a dataset with longer sequences.

```python
class Encoder(nn.Module):
    def __init__(self, input_dim, emb_dim, hid_dim, n_layers, kernel_size, dropout, device):
        super().__init__()

        assert kernel_size % 2 == 1, "Kernel size must be odd!"

        self.device = device

        self.scale = torch.sqrt(torch.FloatTensor([0.5])).to(device)

        self.tok_embedding = nn.Embedding(input_dim, emb_dim)
        self.pos_embedding = nn.Embedding(100, emb_dim)

        self.emb2hid = nn.Linear(emb_dim, hid_dim)
        self.hid2emb = nn.Linear(hid_dim, emb_dim)

        self.convs = nn.ModuleList([nn.Conv1d(in_channels = hid_dim,
                                              out_channels = 2 * hid_dim,
                                              kernel_size = kernel_size,
                                              padding = (kernel_size - 1) // 2)
                                    for _ in range(n_layers)])

        self.dropout = nn.Dropout(dropout)

    def forward(self, src):

        #src = [batch size, src sent len]

        #create position tensor
        pos = torch.arange(0, src.shape[1]).unsqueeze(0).repeat(src.shape[0], 1).to(self.device)

        #pos = [0, 1, 2, 3, ..., src sent len - 1]

        #pos = [batch size, src sent len]

        #embed tokens and positions
        tok_embedded = self.tok_embedding(src)
        pos_embedded = self.pos_embedding(pos)

        #tok_embedded = pos_embedded = [batch size, src sent len, emb dim]

        #combine embeddings by elementwise summing
        embedded = self.dropout(tok_embedded + pos_embedded)

        #embedded = [batch size, src sent len, emb dim]

        #pass embedded through linear layer to convert from emb dim to hid dim
        conv_input = self.emb2hid(embedded)

        #conv_input = [batch size, src sent len, hid dim]

        #permute for convolutional layer
        conv_input = conv_input.permute(0, 2, 1)

        #conv_input = [batch size, hid dim, src sent len]

        #begin convolutional blocks...

        for i, conv in enumerate(self.convs):

            #pass through convolutional layer
            conved = conv(self.dropout(conv_input))

            #conved = [batch size, 2 * hid dim, src sent len]

            #pass through GLU activation function
            conved = F.glu(conved, dim = 1)

            #conved = [batch size, hid dim, src sent len]

            #apply residual connection
            conved = (conved + conv_input) * self.scale

            #conved = [batch size, hid dim, src sent len]

            #set conv_input to conved for next loop iteration
            conv_input = conved

        #...end convolutional blocks

        #permute and convert back to emb dim
        conved = self.hid2emb(conved.permute(0, 2, 1))

        #conved = [batch size, src sent len, emb dim]

        #elementwise sum output (conved) and input (embedded) to be used for attention
        combined = (conved + embedded) * self.scale

        #combined = [batch size, src sent len, emb dim]

        return conved, combined
```

### Decoder

The decoder takes in the actual target sentence and tries to predict it. This model differs from the recurrent neural network models previously detailed in these tutorials as it predicts all tokens within the target sentence in parallel. There is no sequential processing, i.e. no decoding loop. This will be detailed further later on in the tutorials.

The decoder is similar to the encoder, with a few changes to both the main model and the convolutional blocks inside the model.

First, the embeddings do not have a residual connection that connects after the convolutional blocks and the transformation. Instead the embeddings are fed into the convolutional blocks to be used as residual connections there.

Second, to feed the decoder information from the encoder, the encoder conved and combined outputs are used - again, within the convolutional blocks.

Finally, the output of the decoder is a linear layer from embedding dimension to output dimension. This is used make a prediction about what the next word in the translation should be.

### Decoder Convolutional Blocks

Again, these are similar to the convolutional blocks within the encoder, with a few changes.

First, the padding. Instead of padding equally on each side to ensure the length of the sentence stays the same throughout, we only pad at the beginning of the sentence. As we are processing all of the targets simultaneously in parallel, and not sequentially, we need a method of only allowing the filters translating token $i$ to only look at tokens before word $i$. If they were allowed to look at token $i+1$ (the token they should be outputting), the model will simply learn to output the next word in the sequence by directly copying it, without actually learning how to translate.

Let's see what happens if we **incorrectly** padded equally on each side, like we do in the decoder.

The filter at the first position, which is trying use the first word in the sequence, `<sos>` to predict the second word, `two`, can now directly see the word `two`. This is the same for every position, the word the model trying to predict is the second element covered by the filter. Thus, the filters can learn to simply copy the second word at each position allowing for perfect translation without actually learning how to translate.

Second, after the GLU activation and before the residual connection, the block calculates and applies attention - using the encoded representations and the embedding of the current word. **Note**: we only show the connections to the rightmost token, but they are actually connected to all tokens - this was done for clarity. Each token input uses their own, and only their own, embedding for their own attention calculation.

The attention is calculated by first using a linear layer to change the hidden dimension to the same size as the embedding dimension. Then the embedding summed via a residual connection. This combination then has the standard attention calculation applied by finding how much it "matches" with the *encoded conved* and then this is applied by getting a weighted sum over the *encoded combined*. This is then projected back up to the hidden dimenson size and a residual connection to the initial input to the attention layer is applied.

Why do they calculate attention first with the encoded conved and then use it to calculate the weighted sum over the encoded combined? The paper argues that the encoded conved is good for getting a larger context over the encoded sequence, whereas the encoded combined has more information about the specific token and is thus therefore more useful for makng a prediction.

### Decoder Impementation

As we only pad on one side the decoder is allowed to use both odd and even sized padding. Again, the `scale` is used to reduce variance throughout the model and the position embedding is initialized to have a "vocabulary" of 100.

This model takes in the encoder representations in its `forward` method and both are passed to the `calculate_attention` method which calculates and applies attention. It also returns the actual attention values, but we are not currently using them.

```python
class Decoder(nn.Module):
    def __init__(self, output_dim, emb_dim, hid_dim, n_layers, kernel_size, dropout, pad_idx, device):
        super().__init__()

        self.kernel_size = kernel_size
        self.pad_idx = pad_idx
        self.device = device

        self.scale = torch.sqrt(torch.FloatTensor([0.5])).to(device)

        self.tok_embedding = nn.Embedding(output_dim, emb_dim)
        self.pos_embedding = nn.Embedding(100, emb_dim)

        self.emb2hid = nn.Linear(emb_dim, hid_dim)
        self.hid2emb = nn.Linear(hid_dim, emb_dim)

        self.attn_hid2emb = nn.Linear(hid_dim, emb_dim)
        self.attn_emb2hid = nn.Linear(emb_dim, hid_dim)

        self.out = nn.Linear(emb_dim, output_dim)

        self.convs = nn.ModuleList([nn.Conv1d(in_channels = hid_dim,
                                              out_channels = 2 * hid_dim,
                                              kernel_size = kernel_size)
                                    for _ in range(n_layers)])

        self.dropout = nn.Dropout(dropout)

    def calculate_attention(self, embedded, conved, encoder_conved, encoder_combined):

        #embedded = [batch size, trg sent len, emb dim]
        #conved = [batch size, hid dim, trg sent len]
        #encoder_conved = encoder_combined = [batch size, src sent len, emb dim]

        #permute and convert back to emb dim
        conved_emb = self.attn_hid2emb(conved.permute(0, 2, 1))

        #conved_emb = [batch size, trg sent len, emb dim]

        combined = (conved_emb + embedded) * self.scale

        #combined = [batch size, trg sent len, emb dim]

        energy = torch.matmul(combined, encoder_conved.permute(0, 2, 1))

        #energy = [batch size, trg sent len, src sent len]

        attention = F.softmax(energy, dim=2)

        #attention = [batch size, trg sent len, src sent len]

        attended_encoding = torch.matmul(attention, encoder_combined)

        #attended_encoding = [batch size, trg sent len, emd dim]

        #convert from emb dim -> hid dim
        attended_encoding = self.attn_emb2hid(attended_encoding)

        #attended_encoding = [batch size, trg sent len, hid dim]

        #apply residual connection
        attended_combined = (conved + attended_encoding.permute(0, 2, 1)) * self.scale

        #attended_combined = [batch size, hid dim, trg sent len]

        return attention, attended_combined

    def forward(self, trg, encoder_conved, encoder_combined):

        #trg = [batch size, trg sent len]
        #encoder_conved = encoder_combined = [batch size, src sent len, emb dim]

        #create position tensor
        pos = torch.arange(0, trg.shape[1]).unsqueeze(0).repeat(trg.shape[0], 1).to(self.device)

        #pos = [batch size, trg sent len]

        #embed tokens and positions
        tok_embedded = self.tok_embedding(trg)
        pos_embedded = self.pos_embedding(pos)

        #tok_embedded = [batch size, trg sent len, emb dim]
        #pos_embedded = [batch size, trg sent len, emb dim]

        #combine embeddings by elementwise summing
        embedded = self.dropout(tok_embedded + pos_embedded)

        #embedded = [batch size, trg sent len, emb dim]

        #pass embedded through linear layer to go through emb dim -> hid dim
        conv_input = self.emb2hid(embedded)

        #conv_input = [batch size, trg sent len, hid dim]

        #permute for convolutional layer
        conv_input = conv_input.permute(0, 2, 1)

        #conv_input = [batch size, hid dim, trg sent len]

        for i, conv in enumerate(self.convs):

            #apply dropout
            conv_input = self.dropout(conv_input)

            #need to pad so decoder can't "cheat"
            padding = torch.zeros(conv_input.shape[0],
                                  conv_input.shape[1],
                                  self.kernel_size - 1).fill_(self.pad_idx).to(self.device)

            padded_conv_input = torch.cat((padding, conv_input), dim = 2)

            #padded_conv_input = [batch size, hid dim, trg sent len + kernel size - 1]

            #pass through convolutional layer
            conved = conv(padded_conv_input)

            #conved = [batch size, 2 * hid dim, trg sent len]

            #pass through GLU activation function
            conved = F.glu(conved, dim = 1)

            #conved = [batch size, hid dim, trg sent len]

            #calculate attention
            attention, conved = self.calculate_attention(embedded,
                                                         conved,
                                                         encoder_conved,
                                                         encoder_combined)

            #attention = [batch size, trg sent len, src sent len]

            #apply residual connection
            conved = (conved + conv_input) * self.scale

            #conved = [batch size, hid dim, trg sent len]

            #set conv_input to conved for next loop iteration
            conv_input = conved

        conved = self.hid2emb(conved.permute(0, 2, 1))

        #conved = [batch size, trg sent len, emb dim]

        output = self.out(self.dropout(conved))

        #output = [batch size, trg sent len, output dim]

        return output, attention
```

### Seq2Seq

The encapsulating `Seq2Seq` module is a lot different from recurrent neural network methods used in previous notebooks, especially in the decoding.

Our `trg` has the `<eos>` element sliced off of the end of the sequence. This is because we do not input the `<eos>` token into the decoder.

The encoding is similar, insert the source sequence and receive a "context vector". However, here we have two context vectors per word in the source sequence, `encoder_conved` and `encoder_combined`.

As the decoding is done in parallel we do not need a decoding loop. All of the target sequence is input into the decoder at once and the padding is used to ensure each convolutional filter in the decoder can only see the current and previous tokens in the sequence as it slides across the sentence.

This also, however, means we cannot do teacher forcing using this model. We do not have a loop in which we can choose whether to input the predicted token or the actual token in the sequence as everything is predicted in parallel.

```python
class Seq2Seq(nn.Module):
    def __init__(self, encoder, decoder):
        super().__init__()

        self.encoder = encoder
        self.decoder = decoder

    def forward(self, src, trg):

        #src = [batch size, src sent len]
        #trg = [batch size, trg sent len - 1] (<eos> token sliced off the end)

        #calculate z^u (encoder_conved) and (z^u + e) (encoder_combined)
        #encoder_conved is output from final encoder conv. block
        #encoder_combined is encoder_conved plus (elementwise) src embedding plus positional embeddings
        encoder_conved, encoder_combined = self.encoder(src)

        #encoder_conved = [batch size, src sent len, emb dim]
        #encoder_combined = [batch size, src sent len, emb dim]

        #calculate predictions of next words
        #output is a batch of predictions for each word in the trg sentence
        #attention a batch of attention scores across the src sentence for each word in the trg sentence
        output, attention = self.decoder(trg, encoder_conved, encoder_combined)

        #output = [batch size, trg sent len - 1, output dim]
        #attention = [batch size, trg sent len - 1, src sent len]

        return output, attention
```

## Training the Seq2Seq Model

The rest of the tutorial is similar to all of the previous ones. We define all of the hyperparameters, initialize the encoder and decoder, and initialize the overall model - placing it on the GPU if we have one.

In the paper they find that it is more beneficial to use a small filter (kernel size of 3) and a high number of layers (5+).

```python
INPUT_DIM = len(SRC.vocab)
OUTPUT_DIM = len(TRG.vocab)
EMB_DIM = 256
HID_DIM = 512 # each conv. layer has 2 * hid_dim filters
ENC_LAYERS = 10 # number of conv. blocks in encoder
DEC_LAYERS = 10 # number of conv. blocks in decoder
ENC_KERNEL_SIZE = 3 # must be odd!
DEC_KERNEL_SIZE = 3 # can be even or odd
ENC_DROPOUT = 0.25
DEC_DROPOUT = 0.25
PAD_IDX = TRG.vocab.stoi['<pad>']

enc = Encoder(INPUT_DIM, EMB_DIM, HID_DIM, ENC_LAYERS, ENC_KERNEL_SIZE, ENC_DROPOUT, device)
dec = Decoder(OUTPUT_DIM, EMB_DIM, HID_DIM, DEC_LAYERS, DEC_KERNEL_SIZE, DEC_DROPOUT, PAD_IDX, device)

model = Seq2Seq(enc, dec).to(device)
```

We can also see that the model has almost twice as many parameters as the attention based model (20m to 37m).

```python
def count_parameters(model):
    return sum(p.numel() for p in model.parameters() if p.requires_grad)

print(f'The model has {count_parameters(model):,} trainable parameters')
```

    The model has 37,351,685 trainable parameters

Next, we define the optimizer and the loss function (criterion). As before we ignore the loss where the target sequence is a padding token.

```python
optimizer = optim.Adam(model.parameters())
```

```python
criterion = nn.CrossEntropyLoss(ignore_index = PAD_IDX)
```

Then, we define the training loop for the model.

We handle the sequences a little differently than previous tutorials. For all models we never put the `<eos>` into the decoder. This is handled in the RNN models by the having the decoder loop not reach having the `<eos>` as an input to the decoder. In this model, we simply slice the `<eos>` token off the end of the sequence. Thus:

$$\begin{align}
\text{trg} &= [sos, x_1, x_2, x_3, eos]\\
\text{trg[:-1]} &= [sos, x_1, x_2, x_3]
\end{align}$$

$x_i$ denotes actual target sequence element. We then feed this into the model to get a predicted sequence that should hopefully predict the `<eos>` token:

$$\begin{align}
\text{output} &= [y_1, y_2, y_3, eos]
\end{align}$$

$y_i$ denotes predicted target sequence element. We then calculate our loss using the original `trg` tensor with the `<sos>` token sliced off the front, leaving the `<eos>` token:

$$\begin{align}
\text{output} &= [y_1, y_2, y_3, eos]\\
\text{trg[1:]} &= [x_1, x_2, x_3, eos]
\end{align}$$

We then calculate our losses and update our parameters as is standard.

```python
def train(model, iterator, optimizer, criterion, clip):

    model.train()

    epoch_loss = 0

    for i, batch in enumerate(iterator):

        src = batch.src
        trg = batch.trg

        optimizer.zero_grad()

        output, _ = model(src, trg[:,:-1])

        #output = [batch size, trg sent len - 1, output dim]
        #trg = [batch size, trg sent len]

        output = output.contiguous().view(-1, output.shape[-1])
        trg = trg[:,1:].contiguous().view(-1)

        #output = [batch size * trg sent len - 1, output dim]
        #trg = [batch size * trg sent len - 1]

        loss = criterion(output, trg)

        loss.backward()

        torch.nn.utils.clip_grad_norm_(model.parameters(), clip)

        optimizer.step()

        epoch_loss += loss.item()

    return epoch_loss / len(iterator)
```

The evaluation loop is the same as the training loop, just without the gradient calculations and parameter updates.

```python
def evaluate(model, iterator, criterion):

    model.eval()

    epoch_loss = 0

    with torch.no_grad():

        for i, batch in enumerate(iterator):

            src = batch.src
            trg = batch.trg

            output, _ = model(src, trg[:,:-1])

            #output = [batch size, trg sent len - 1, output dim]
            #trg = [batch size, trg sent len]

            output = output.contiguous().view(-1, output.shape[-1])
            trg = trg[:,1:].contiguous().view(-1)

            #output = [batch size * trg sent len - 1, output dim]
            #trg = [batch size * trg sent len - 1]

            loss = criterion(output, trg)

            epoch_loss += loss.item()

    return epoch_loss / len(iterator)
```

Again, we have a function that tells us how long each epoch takes.

```python
def epoch_time(start_time, end_time):
    elapsed_time = end_time - start_time
    elapsed_mins = int(elapsed_time / 60)
    elapsed_secs = int(elapsed_time - (elapsed_mins * 60))
    return elapsed_mins, elapsed_secs
```

Finally, we train our model.

Although we have almost twice as many parameters as the attention based RNN model, it actually takes around half the time as the standard version and about the same time as the packed padded sequences version. This is due to all calculations being done in parallel using the convolutional filters instead of sequentially using RNNs.

**Note**: this model always has a teacher forcing ratio of 1, i.e. it will always use the ground truth next token from the target sequence. This means we cannot compare perplexity values against the previous models when they are using a teacher forcing ratio that is not 1. See [here](https://github.com/bentrevett/pytorch-seq2seq/issues/39#issuecomment-529408483) for the results of the attention based RNN using a teacher forcing ratio of 1.

```python
N_EPOCHS = 10
CLIP = 1

best_valid_loss = float('inf')

for epoch in range(N_EPOCHS):

    start_time = time.time()

    train_loss = train(model, train_iterator, optimizer, criterion, CLIP)
    valid_loss = evaluate(model, valid_iterator, criterion)

    end_time = time.time()

    epoch_mins, epoch_secs = epoch_time(start_time, end_time)

    if valid_loss < best_valid_loss:
        best_valid_loss = valid_loss
        torch.save(model.state_dict(), 'tut5-model.pt')

    print(f'Epoch: {epoch+1:02} | Time: {epoch_mins}m {epoch_secs}s')
    print(f'\tTrain Loss: {train_loss:.3f} | Train PPL: {math.exp(train_loss):7.3f}')
    print(f'\t Val. Loss: {valid_loss:.3f} |  Val. PPL: {math.exp(valid_loss):7.3f}')
```

    Epoch: 01 | Time: 0m 30s
    	Train Loss: 4.202 | Train PPL:  66.837
    	 Val. Loss: 2.907 |  Val. PPL:  18.306
    Epoch: 02 | Time: 0m 31s
    	Train Loss: 3.018 | Train PPL:  20.446
    	 Val. Loss: 2.378 |  Val. PPL:  10.779
    Epoch: 03 | Time: 0m 30s
    	Train Loss: 2.598 | Train PPL:  13.438
    	 Val. Loss: 2.121 |  Val. PPL:   8.340
    Epoch: 04 | Time: 0m 31s
    	Train Loss: 2.360 | Train PPL:  10.591
    	 Val. Loss: 1.976 |  Val. PPL:   7.210
    Epoch: 05 | Time: 0m 30s
    	Train Loss: 2.208 | Train PPL:   9.102
    	 Val. Loss: 1.899 |  Val. PPL:   6.678
    Epoch: 06 | Time: 0m 31s
    	Train Loss: 2.095 | Train PPL:   8.124
    	 Val. Loss: 1.845 |  Val. PPL:   6.328
    Epoch: 07 | Time: 0m 31s
    	Train Loss: 2.008 | Train PPL:   7.450
    	 Val. Loss: 1.801 |  Val. PPL:   6.059
    Epoch: 08 | Time: 0m 31s
    	Train Loss: 1.933 | Train PPL:   6.909
    	 Val. Loss: 1.776 |  Val. PPL:   5.908
    Epoch: 09 | Time: 0m 31s
    	Train Loss: 1.872 | Train PPL:   6.499
    	 Val. Loss: 1.748 |  Val. PPL:   5.743
    Epoch: 10 | Time: 0m 31s
    	Train Loss: 1.819 | Train PPL:   6.166
    	 Val. Loss: 1.731 |  Val. PPL:   5.649

We then load the parameters which obtained the lowest validation loss and calculate the loss over the test set.

```python
model.load_state_dict(torch.load('tut5-model.pt'))

test_loss = evaluate(model, test_iterator, criterion)

print(f'| Test Loss: {test_loss:.3f} | Test PPL: {math.exp(test_loss):7.3f} |')
```

    | Test Loss: 1.800 | Test PPL:   6.047 |

## Inference

```python
def translate_sentence(sentence, src_field, trg_field, model, device, max_len = 50):

    model.eval()

    if isinstance(sentence, str):
        nlp = spacy.load('en')
        tokens = [token.text.lower() for token in nlp(sentence)]
    else:
        tokens = [token.lower() for token in sentence]

    tokens = ['<sos>'] + tokens + ['<eos>']

    src_indexes = [src_field.vocab.stoi[token] for token in tokens]

    src_tensor = torch.LongTensor(src_indexes).unsqueeze(0).to(device)

    with torch.no_grad():
        encoder_conved, encoder_combined = model.encoder(src_tensor)

    trg_indexes = [trg_field.vocab.stoi[trg_field.init_token]]

    for i in range(max_len):

        trg_tensor = torch.LongTensor(trg_indexes).unsqueeze(0).to(device)

        with torch.no_grad():
            output, attention = model.decoder(trg_tensor, encoder_conved, encoder_combined)

        pred_token = output.argmax(2)[:,-1].item()

        trg_indexes.append(pred_token)

        if pred_token == trg_field.vocab.stoi[trg_field.eos_token]:
            break

    trg_tokens = [trg_field.vocab.itos[i] for i in trg_indexes]

    return trg_tokens[1:], attention
```

```python
def display_attention(sentence, translation, attention):

    fig = plt.figure(figsize=(10,10))
    ax = fig.add_subplot(111)

    attention = attention.squeeze(0).cpu().detach().numpy()

    cax = ax.matshow(attention, cmap='bone')

    ax.tick_params(labelsize=15)
    ax.set_xticklabels(['']+['<sos>']+[t.lower() for t in sentence]+['<eos>'],
                       rotation=45)
    ax.set_yticklabels(['']+translation)

    ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
    ax.yaxis.set_major_locator(ticker.MultipleLocator(1))

    plt.show()
    plt.close()
```

```python
example_idx = 10

src = vars(train_data.examples[example_idx])['src']
trg = vars(train_data.examples[example_idx])['trg']

print(f'src = {src}')
print(f'trg = {trg}')
```

    src = ['eine', 'ballettklasse', 'mit', 'fünf', 'mädchen', ',', 'die', 'nacheinander', 'springen', '.']
    trg = ['a', 'ballet', 'class', 'of', 'five', 'girls', 'jumping', 'in', 'sequence', '.']

```python
translation, attention = translate_sentence(src, SRC, TRG, model, device)

print(f'predicted trg = {translation}')
```

    predicted trg = ['a', 'ballet', 'with', 'five', 'girls', 'jumping', 'in', 'sequence', '.', '<eos>']

```python
display_attention(src, translation, attention)
```

```python
example_idx = 1

src = vars(valid_data.examples[example_idx])['src']
trg = vars(valid_data.examples[example_idx])['trg']

print(f'src = {src}')
print(f'trg = {trg}')
```

    src = ['ein', 'mann', 'schläft', 'in', 'einem', 'grünen', 'raum', 'auf', 'einem', 'sofa', '.']
    trg = ['a', 'man', 'sleeping', 'in', 'a', 'green', 'room', 'on', 'a', 'couch', '.']

```python
translation, attention = translate_sentence(src, SRC, TRG, model, device)

print(f'predicted trg = {translation}')
```

    predicted trg = ['a', 'man', 'sleeping', 'in', 'a', 'green', 'room', 'on', 'a', 'couch', '.', '<eos>']

```python
display_attention(src, translation, attention)
```

```python
example_idx = 20

src = vars(test_data.examples[example_idx])['src']
trg = vars(test_data.examples[example_idx])['trg']

print(f'src = {src}')
print(f'trg = {trg}')
```

    src = ['leute', ',', 'die', 'vor', 'einem', 'gebäude', 'stehen', '.']
    trg = ['people', 'standing', 'outside', 'of', 'a', 'building', '.']

```python
translation, attention = translate_sentence(src, SRC, TRG, model, device)

print(f'predicted trg = {translation}')
```

    predicted trg = ['people', 'standing', 'in', 'front', 'of', 'a', 'building', '.', '<eos>']

```python
display_attention(src, translation, attention)
```

5 - Convolutional Sequence to Sequence Learning.ipynb


```python
%matplotlib widget
```

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

```python
from vsc_lcl import vsc_lcl_class
```

```python

```

## Instantiate system

```python
syst = vsc_lcl_class()
syst.Dt = 5e-6
syst.decimation = 1
syst.N_store = 100_000
syst.update()
```

## Solve steady state

```python
syst.initialize([{'eta_q_g01':0.8693333333333333,'G_d_g01':0.01}],xy0=100)
ssa.eval_ss(syst);
```

```python
syst.report_x()
syst.report_y()
syst.report_u()
syst.report_z()
```

    i_tD_g01 = -0.20
    i_tQ_g01 =  1.63
    v_mD_g01 =  0.65
    v_mQ_g01 = 326.02
    i_sD_g01 =  0.20
    i_sQ_g01 = -1.63
    i_1_D =  0.20
    i_1_Q = -1.63
    v_sD_g01 =  0.00
    v_sQ_g01 = 326.00
    v_md_g01 =  0.65
    v_mq_g01 = 326.02
    v_sd_g01 =  0.00
    v_sq_g01 = 326.00
    i_td_g01 = -0.20
    i_tq_g01 =  1.63
    i_sd_g01 =  0.20
    i_sq_g01 = -1.63
    eta_D_g01 =  0.00
    eta_Q_g01 =  0.87
    v_dc_g01 = 750.00
    v_1_D =  0.00
    v_1_Q = 326.00
    phi_g01 =  0.00
    eta_d_g01 =  0.00
    eta_q_g01 =  0.87
    v_t_D =  0.00
    v_t_Q = 326.00
    damp_D =  0.01
    damp_Q =  3.26
    i_sd_g01 =  0.20
    i_sq_g01 = -1.63
    v_md_g01 =  0.65
    v_mq_g01 = 326.02
    i_td_g01 = -0.20
    i_tq_g01 =  1.63

```python
ssa.damp_report(syst)
```

      Real
      Imag
      Freq.
      Damp

      Mode 1
      -1265.707963
      20276.036010
      3227.031357
      0.062303

      Mode 2
      -1265.707963
      -20276.036010
      3227.031357
      0.062303

      Mode 3
      -1265.707963
      19647.717479
      3127.031357
      0.064287

      Mode 4
      -1265.707963
      -19647.717479
      3127.031357
      0.064287

      Mode 5
      -31.415927
      314.159265
      50.000000
      0.099504

      Mode 6
      -31.415927
      -314.159265
      50.000000
      0.099504

```python
ssa.participation(syst).abs().round(2)
```

      Mode 1
      Mode 2
      Mode 3
      Mode 4
      Mode 5
      Mode 6

      i_tD_g01
      0.13
      0.13
      0.13
      0.13
      0.25
      0.25

      i_tQ_g01
      0.13
      0.13
      0.13
      0.13
      0.25
      0.25

      v_mD_g01
      0.25
      0.25
      0.25
      0.25
      0.00
      0.00

      v_mQ_g01
      0.25
      0.25
      0.25
      0.25
      0.00
      0.00

      i_sD_g01
      0.13
      0.13
      0.13
      0.13
      0.25
      0.25

      i_sQ_g01
      0.13
      0.13
      0.13
      0.13
      0.25
      0.25

### Open loop

```python
Δt = 50.0e-6
times = np.arange(0.0,0.2,Δt)
```

```python
syst.initialize([{'eta_q_g01':0.8693333333333333,'G_d_g01':0.0}],xy0=100)
eta_q_g01_0 = syst.get_value('eta_q_g01')
it = 0
for t in times:

    eta_q_g01 = eta_q_g01_0
    if t>5e-3:
        eta_q_g01 = eta_q_g01_0*1.05
   # if t>10e-3:
   #     eta_q_g01 = eta_q_g01_0

    events=[{'t_end':t,'eta_q_g01':eta_q_g01}]
    syst.run(events)

    it += 1

syst.post();
```

```python
fig, axes = plt.subplots(nrows=2, ncols=1, figsize=(7, 7))

axes[0].plot(syst.T,syst.get_values('i_sd_g01'),label='i_sd_g01')
axes[0].plot(syst.T,syst.get_values('i_sq_g01'),label='i_sq_g01')

for ax in axes:
    ax.grid()
    ax.legend()
ax.set_xlabel('Time (s)')
```

    Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …

    No handles with labels found to put in legend.

    Text(0.5, 0, 'Time (s)')

```python
syst.N_store
```

    100000

### CTRL1

```python
syst = vsc_lcl_class()
syst.Dt = 5e-6
syst.decimation =1
syst.N_store =100_000
syst.update()

Δt = 50.0e-6
times = np.arange(0.0,0.2,Δt)

syst.initialize([{'G_d_g01':0.005}],xy0=1000)
eta_q_g01_0 = syst.get_value('eta_q_g01')
it = 0

i_sd,i_sq,v_sd,v_sq = syst.get_mvalue(['i_sd_g01','i_sq_g01','v_sd_g01','v_sq_g01'])
v_dc = syst.get_value('v_dc_g01')
eta_d = syst.get_value('eta_d_g01')
eta_q = syst.get_value('eta_q_g01')

# control design
R_t_g01,L_t_g01  = syst.get_value('R_t_g01'),syst.get_value('L_t_g01')
R_s_g01,L_s_g01,C_m_g01  = syst.get_value('R_s_g01'),syst.get_value('L_s_g01'),syst.get_value('C_m_g01')
R = R_t_g01 + R_s_g01
L = L_t_g01 + L_s_g01
tau_ctrl_1 = 5e-3; #Time constant of CTRL 1
K_pi = L/tau_ctrl_1; #Proportional gain of CTRL 1
K_ii = R/tau_ctrl_1; #Integral gain of CTRL 1
xi = np.zeros((2,1))

#u_d = K_pi*epsilon_d + K_ii*xi_d
#u_q = K_pi*epsilon_q + K_ii*xi_q
#u_d = eta_d*v_dc/2 - v_sd + L*i_sq*omega => eta_d = (u_d + v_sd - L*i_sq*omega)*2/v_dc
#u_q = eta_q*v_dc/2 - v_sq - L*i_sd*omega => eta_q = (u_q + v_sq + L*i_sd*omega)*2/v_dc
omega = 2*np.pi*50
u_d_0 = eta_d*v_dc/2 - v_sd + L*i_sq*omega
u_q_0 = eta_q*v_dc/2 - v_sq - L*i_sd*omega
i_sd_ref_0 = i_sd
i_sq_ref_0 = i_sq

# simulation
for t in times:

    # measurements
    i_sd = syst.get_value('i_sd_g01')
    i_sq = syst.get_value('i_sq_g01')
    v_sd = syst.get_value('v_sd_g01')
    v_sq = syst.get_value('v_sq_g01')
    v_dc = syst.get_value('v_dc_g01')

    i_sd_ref = i_sd_ref_0
    i_sq_ref = i_sq_ref_0
    if t>10e-3: i_sd_ref = 20
    if t>100e-3: i_sq_ref = 30

    xi_d = xi[0,0]
    xi_q = xi[1,0]

    epsilon_d = i_sd_ref - i_sd
    epsilon_q = i_sq_ref - i_sq

    u_d = K_pi*epsilon_d + K_ii*xi_d + u_d_0
    u_q = K_pi*epsilon_q + K_ii*xi_q + u_q_0

    eta_d = (u_d + v_sd - L*i_sq*omega)*2/v_dc
    eta_q = (u_q + v_sq + L*i_sd*omega)*2/v_dc

    xi[0,0] += Δt*epsilon_d
    xi[1,0] += Δt*epsilon_q

    events=[{'t_end':t,'eta_d_g01':eta_d,'eta_q_g01':eta_q}]
    syst.run(events)

    it += 1

syst.post();
```

```python
plt.close('all')
fig, axes = plt.subplots(nrows=2, ncols=1, figsize=(7, 7))

axes[0].plot(syst.T,syst.get_values('i_sd_g01'),label='i_sd_g01')
axes[0].plot(syst.T,syst.get_values('i_sq_g01'),label='i_sq_g01')

axes[1].plot(syst.T,syst.get_values('eta_D_g01'),label='eta_D_g01')
axes[1].plot(syst.T,syst.get_values('eta_Q_g01'),label='eta_Q_g01')

for ax in axes:
    ax.grid()
    ax.legend()
ax.set_xlabel('Time (s)')
```

    Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …

    Text(0.5, 0, 'Time (s)')

```python

```

### CTRL1 + Active damping

```python
syst = vsc_lcl_class()
syst.Dt = 5e-6
syst.decimation =1
syst.N_store =100_000
syst.update()

Δt = 50.0e-6
times = np.arange(0.0,0.2,Δt)

syst.initialize([{'G_d_g01':0.0}],xy0=1000)
eta_q_g01_0 = syst.get_value('eta_q_g01')
it = 0

i_sd,i_sq,i_td,i_tq,v_sd,v_sq = syst.get_mvalue(['i_sd_g01','i_sq_g01','i_td_g01','i_tq_g01','v_sd_g01','v_sq_g01'])
v_dc = syst.get_value('v_dc_g01')
eta_d = syst.get_value('eta_d_g01')
eta_q = syst.get_value('eta_q_g01')

# control design
R_t_g01,L_t_g01  = syst.get_value('R_t_g01'),syst.get_value('L_t_g01')
R_s_g01,L_s_g01,C_m_g01  = syst.get_value('R_s_g01'),syst.get_value('L_s_g01'),syst.get_value('C_m_g01')
R = R_t_g01 + R_s_g01
L = L_t_g01 + L_s_g01
tau_ctrl_1 = 5e-3; #Time constant of CTRL 1
K_pi = L/tau_ctrl_1; #Proportional gain of CTRL 1
K_ii = R/tau_ctrl_1; #Integral gain of CTRL 1
G_v = 1.0  #Active damping
# en pu G_d = L/C*G_v

xi = np.zeros((2,1))

#u_d = K_pi*epsilon_d + K_ii*xi_d
#u_q = K_pi*epsilon_q + K_ii*xi_q
#u_d = eta_d*v_dc/2 - v_sd + L*i_sq*omega => eta_d = (u_d + v_sd - L*i_sq*omega)*2/v_dc
#u_q = eta_q*v_dc/2 - v_sq - L*i_sd*omega => eta_q = (u_q + v_sq + L*i_sd*omega)*2/v_dc
omega = 2*np.pi*50
u_d_0 = eta_d*v_dc/2 - v_sd + L*i_sq*omega + G_v*(i_td - i_sd)
u_q_0 = eta_q*v_dc/2 - v_sq - L*i_sd*omega + G_v*(i_tq - i_sq)
i_sd_ref_0 = i_sd
i_sq_ref_0 = i_sq

# simulation
for t in times:

    # measurements
    i_sd = syst.get_value('i_sd_g01')
    i_sq = syst.get_value('i_sq_g01')
    v_sd = syst.get_value('v_sd_g01')
    v_sq = syst.get_value('v_sq_g01')
    i_td = syst.get_value('i_td_g01')
    i_tq = syst.get_value('i_tq_g01')
    v_dc = syst.get_value('v_dc_g01')

    i_sd_ref = i_sd_ref_0
    i_sq_ref = i_sq_ref_0
    if t>10e-3: i_sd_ref = 20
    if t>100e-3: i_sq_ref = 30

    xi_d = xi[0,0]
    xi_q = xi[1,0]

    epsilon_d = i_sd_ref - i_sd
    epsilon_q = i_sq_ref - i_sq

    u_d = K_pi*epsilon_d + K_ii*xi_d + u_d_0
    u_q = K_pi*epsilon_q + K_ii*xi_q + u_q_0

    eta_d = (u_d + v_sd - L*i_sq*omega - G_v*(i_td - i_sd))*2/v_dc
    eta_q = (u_q + v_sq + L*i_sd*omega - G_v*(i_tq - i_sq))*2/v_dc

    xi[0,0] += Δt*epsilon_d
    xi[1,0] += Δt*epsilon_q

    events=[{'t_end':t,'eta_d_g01':eta_d,'eta_q_g01':eta_q}]
    syst.run(events)

    it += 1

syst.post();
```

```python
plt.close('all')
fig, axes = plt.subplots(nrows=2, ncols=1, figsize=(7, 7))

axes[0].plot(syst.T,syst.get_values('i_sd_g01'),label='i_sd_g01')
axes[0].plot(syst.T,syst.get_values('i_sq_g01'),label='i_sq_g01')

axes[1].plot(syst.T,syst.get_values('eta_D_g01'),label='eta_D_g01')
axes[1].plot(syst.T,syst.get_values('eta_Q_g01'),label='eta_Q_g01')

for ax in axes:
    ax.grid()
    ax.legend()
ax.set_xlabel('Time (s)')
```

    Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …

    Text(0.5, 0, 'Time (s)')

```python

```

### CTRL1 + Active damping + delay

```python
syst = vsc_lcl_class()
syst.Dt = 5e-6
syst.decimation =1
syst.N_store =100_000
syst.update()

Δt = 500.0e-6
times = np.arange(0.0,0.2,Δt)

syst.initialize([{'G_d_g01':0.0, 'C_m_g01':4e-6}],xy0=1000)
eta_q_g01_0 = syst.get_value('eta_q_g01')
it = 0

i_sd,i_sq,i_td,i_tq,v_sd,v_sq = syst.get_mvalue(['i_sd_g01','i_sq_g01','i_td_g01','i_tq_g01','v_sd_g01','v_sq_g01'])
v_dc = syst.get_value('v_dc_g01')
eta_d = syst.get_value('eta_d_g01')
eta_q = syst.get_value('eta_q_g01')

# control design
R_t_g01,L_t_g01  = syst.get_value('R_t_g01'),syst.get_value('L_t_g01')
R_s_g01,L_s_g01,C_m_g01  = syst.get_value('R_s_g01'),syst.get_value('L_s_g01'),syst.get_value('C_m_g01')
R = R_t_g01 + R_s_g01
L = L_t_g01 + L_s_g01
tau_ctrl_1 = 5e-3; #Time constant of CTRL 1
K_pi = L/tau_ctrl_1; #Proportional gain of CTRL 1
K_ii = R/tau_ctrl_1; #Integral gain of CTRL 1
G_v = 0.0  #Active damping

xi = np.zeros((2,1))

#u_d = K_pi*epsilon_d + K_ii*xi_d
#u_q = K_pi*epsilon_q + K_ii*xi_q
#u_d = eta_d*v_dc/2 - v_sd + L*i_sq*omega => eta_d = (u_d + v_sd - L*i_sq*omega)*2/v_dc
#u_q = eta_q*v_dc/2 - v_sq - L*i_sd*omega => eta_q = (u_q + v_sq + L*i_sd*omega)*2/v_dc
omega = 2*np.pi*50
u_d_0 = eta_d*v_dc/2 - v_sd + L*i_sq*omega + G_v*(i_td - i_sd)
u_q_0 = eta_q*v_dc/2 - v_sq - L*i_sd*omega + G_v*(i_tq - i_sq)
i_sd_ref_0 = i_sd
i_sq_ref_0 = i_sq
eta_d_prev = eta_d
eta_q_prev = eta_q
damp_d_list = []
damp_q_list = []
# simulation
for t in times:

    # measurements
    i_sd = syst.get_value('i_sd_g01')
    i_sq = syst.get_value('i_sq_g01')
    v_sd = syst.get_value('v_sd_g01')
    v_sq = syst.get_value('v_sq_g01')
    i_td = syst.get_value('i_td_g01')
    i_tq = syst.get_value('i_tq_g01')
    v_dc = syst.get_value('v_dc_g01')

    i_sd_ref = i_sd_ref_0
    i_sq_ref = i_sq_ref_0
    if t>10e-3: i_sd_ref = 20
    if t>100e-3: i_sq_ref = 30

    xi_d = xi[0,0]
    xi_q = xi[1,0]

    epsilon_d = i_sd_ref - i_sd
    epsilon_q = i_sq_ref - i_sq

    u_d = K_pi*epsilon_d + K_ii*xi_d + u_d_0
    u_q = K_pi*epsilon_q + K_ii*xi_q + u_q_0

    i_m_d_0  = i_td - i_sd
    i_m_q_0  = i_tq - i_sq
    i_m_d_90 = (i_tq - i_sq)
    i_m_q_90 = (i_td - i_sd)

    K_0 =  -0.6453
    K_90 = -(1-0.6453)

    K_0  = -0.5286373998102673
    K_90 = -0.8488477481397001

    K_0  = -1.0
    K_90 = -0.0

    damp_d =  G_v*(K_0*i_m_d_0+K_90*i_m_d_90)
    damp_q =  G_v*(K_0*i_m_q_0-K_90*i_m_q_90)

    eta_d = (u_d + v_sd - L*i_sq*omega + damp_d)*2/v_dc
    eta_q = (u_q + v_sq + L*i_sd*omega + damp_q)*2/v_dc

    xi[0,0] += Δt*epsilon_d
    xi[1,0] += Δt*epsilon_q

    events=[{'t_end':t,'eta_d_g01':eta_d_prev,'eta_q_g01':eta_q_prev}]
    syst.run(events)

    eta_d_prev = eta_d
    eta_q_prev = eta_q

    damp_d_list += [damp_d]
    damp_q_list += [damp_q]

    it += 1

syst.post();

Damp_d = np.array(damp_d_list)
Damp_q = np.array(damp_q_list)
```

```python
plt.close('all')
fig, axes = plt.subplots(nrows=2, ncols=1, figsize=(7, 7),sharex=True)

axes[0].plot(syst.T,syst.get_values('i_sd_g01'),label='i_sd_g01')
axes[0].plot(syst.T,syst.get_values('i_sq_g01'),label='i_sq_g01')

#axes[0].plot(syst.T,syst.get_values('damp_D')-syst.get_values('damp_D')[0],label='damp_D')
axes[1].plot(syst.T,syst.get_values('damp_Q'),label='damp_Q')

#axes[1].plot(syst.T,syst.get_values('eta_D_g01'),label='eta_D_g01')
#axes[1].plot(syst.T,syst.get_values('eta_Q_g01'),label='eta_Q_g01')
#axes[0].plot(times,Damp_d-Damp_d[0],label='Damp_d')
axes[1].plot(times,Damp_q-Damp_q[0],label='Damp_q')
for ax in axes:
    ax.grid()
    ax.legend()
ax.set_xlabel('Time (s)')
```

    Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …

    Text(0.5, 0, 'Time (s)')

1 = K_d_0**2+K_d_90**2
phase = Δt/(T_2damp*np.pi*2)
K_d_0/K_d_90 = np.tan(phase) = tangente
K_d_0 = tangente*K_d_90
1 =  (tangente*K_d_90)**2 + K_d_90**2
1 = (tangente**2+1)*K_d_90**2

```python
T_2damp = 1/(3227.031357)
phase = Δt/(T_2damp)*2*np.pi
tangente = np.tan(phase)

K_90 = (1/((1/tangente)**2+1))**0.5
K_0 = (1 - K_90**2)**0.5

print(f'    K_0  = {-K_0}')
print(f'    K_90 = {-K_90}')
```

        K_0  = -0.7562459294598116
        K_90 = -0.6542874705933666

```python
T_2damp = 1/(3227.031357)
phase = Δt/(T_2damp)*2*np.pi
```

```python
np.rad2deg(phase)*20/3.227
```

    3600.034981468857

```python
T_2damp
```

    0.00030988233127354764

## CTRL1 in state feedback

```python
import pydae.ssa as ssa
import scipy.signal as sctrl
```

```python
ssa.eval_ss(syst);
```

```python
syst = vsc_lcl_class()
syst.Dt = 5e-6
syst.decimation =1
syst.N_store =100_000
syst.update()

Δt = 50e-6
#x_d_ctrl_list = ['i']   # states to consider in the reduction
z_ctrl_list = [ 'i_sd_g01', 'i_sq_g01']     # outputs to consider in the controller
u_ctrl_list = ['eta_d_g01','eta_q_g01']     # intputs to consider in the controller
z_ctrl_idxs = [syst.outputs_list.index(item) for item in z_ctrl_list]
u_ctrl_idxs = [syst.inputs_run_list.index(item) for item in u_ctrl_list]

syst.Δt = Δt

## Calculate equilibirum point
syst.initialize([{'G_d_g01':0.0,'eta_d_g01':0.0,'eta_q_g01':-0.8693333,'v_1_Q':-326,'v_1_D':0.0}],xy0=1000)
ssa.eval_ss(syst)

# linear continous plant
A_p = syst.A
B_p = syst.B
C_p = syst.C
D_p = syst.D

# plant discretization
A_d,B_d,C_d,D_d,Dt = sctrl.cont2discrete((A_p,B_p,C_p,D_p),Δt,method='zoh')

N_z_d,N_x_d = C_d.shape  # discreticed plant dimensions
N_x_d,N_u_d = B_d.shape

# convenient matrices
O_ux = np.zeros((N_u_d,N_x_d))
O_xu = np.zeros((N_x_d,N_u_d))
O_uu = np.zeros((N_u_d,N_u_d))
I_uu = np.eye(N_u_d)

syst.A_d = A_d
syst.B_d = B_d

# Controller ##################################################################################
B_c = B_d[:,u_ctrl_idxs]
C_c = C_d[z_ctrl_idxs,:]
D_c = D_d[z_ctrl_idxs,:]

N_x_c,N_u_d = B_c.shape
N_z_c,N_x_c = C_c.shape

O_ux = np.zeros((N_u_d,N_x_d))
O_xu = np.zeros((N_x_d,N_u_d))
O_uu = np.zeros((N_u_d,N_u_d))
I_uu = np.eye(N_u_d)

# discretized plant:
# Δx_d = A_d*Δx_d + B_d*Δu_d
# Δz_c = C_c*Δx_d + D_c*Δu_d

# dinamic extension:
# Δx_d = A_d*Δx_d + B_d*Δu_d
# Δx_i = Δx_i + Δt*(Δz_c-Δz_c_ref) = Δx_i + Δt*C_c*Δx_d - Dt*Δz_c_ref
# Δz_c = z_c - z_c_0
# Δz_c_ref = z_c_ref - z_c_0
# (Δz_c-Δz_c_ref) = z_c - z_c_ref

A_e = np.block([
                [    A_d, O_xu],    # Δx_d
                [ Δt*C_c, I_uu],    # Δx_i
               ])

B_e = np.block([
                [   B_c],
                [   O_uu],
               ])

# weighting matrices
Q_c = np.eye(A_e.shape[0])
Q_c[-1,-1] = 1e4
Q_c[-2,-2] = 1e4

R_c = np.eye(B_c.shape[1])*100000

K_c,S_c,E_c = ssa.dlqr(A_e,B_e,Q_c,R_c)

E_cont = np.log(E_c)/Δt
```

```python
-E_c.real/np.abs(E_c)
```

    array([-0.4895141 , -0.4895141 , -0.51666282, -0.51666282, -0.99987441,
           -0.99987441, -0.99999999, -0.99999999])

```python
E_c
```

    array([0.37274688+6.63992221e-01j, 0.37274688-6.63992221e-01j,
           0.39312068+6.51460819e-01j, 0.39312068-6.51460819e-01j,
           0.97147951+1.53979236e-02j, 0.97147951-1.53979236e-02j,
           0.99690471+1.40510238e-04j, 0.99690471-1.40510238e-04j])

```python
syst = vsc_lcl_class()
syst.Dt = 5e-6
syst.decimation =10
syst.N_store =1_000_000
syst.update()
times = np.arange(0.0,0.2,Δt)

syst.initialize([{'G_d_g01':0.0,'eta_d_g01':0.0,'eta_q_g01':-0.8693333,'v_1_Q':-326,'v_1_D':0.0}],xy0=1000)
i_sd = syst.get_value('i_sd_g01')
i_sq = syst.get_value('i_sq_g01')
v_sd = syst.get_value('v_sd_g01')
v_sq = syst.get_value('v_sq_g01')
i_td = syst.get_value('i_td_g01')
i_tq = syst.get_value('i_tq_g01')
v_md = syst.get_value('v_md_g01')
v_mq = syst.get_value('v_mq_g01')
v_dc = syst.get_value('v_dc_g01')
eta_d = syst.get_value('eta_d_g01')
eta_q = syst.get_value('eta_q_g01')
eta_d_prev = eta_d
eta_q_prev = eta_q
i_sd_ref_0 = i_sd
i_sq_ref_0 = i_sq
x_d_0 = np.array([i_td,i_tq,v_md,v_mq,i_sd,i_sq]).reshape(6,1)
u_d_0 = np.array([eta_d,eta_q]).reshape(2,1)
syst.Δxi = np.zeros((2,1))
it = 0
for t in times:

    # measurements
    i_sd = syst.get_value('i_sd_g01')
    i_sq = syst.get_value('i_sq_g01')
    v_sd = syst.get_value('v_sd_g01')
    v_sq = syst.get_value('v_sq_g01')
    i_td = syst.get_value('i_td_g01')
    i_tq = syst.get_value('i_tq_g01')
    v_md = syst.get_value('v_md_g01')
    v_mq = syst.get_value('v_mq_g01')
    v_dc = syst.get_value('v_dc_g01')

    x_d = np.array([i_td,i_tq,v_md,v_mq,i_sd,i_sq]).reshape(6,1)
    Δx_d = x_d - x_d_0

    Δx_i = syst.Δxi

    i_sd_ref = i_sd_ref_0
    i_sq_ref = i_sq_ref_0
    if t>10e-3: i_sd_ref = 20
    if t>100e-3: i_sq_ref = 30

    epsilon_d = i_sd - i_sd_ref
    epsilon_q = i_sq - i_sq_ref

    epsilon = np.block([[epsilon_d],[epsilon_q]])

    Δx_e = np.block([[Δx_d], [Δx_i]])

    Δu_d = -K_c @ Δx_e

    u_d = Δu_d + u_d_0

    syst.Δxi += Δt*epsilon

    eta_d = u_d[0,0]
    eta_q = u_d[1,0]

    events=[{'t_end':t,'eta_d_g01':eta_d_prev,'eta_q_g01':eta_q_prev}]
    syst.run(events)

    eta_d_prev = eta_d
    eta_q_prev = eta_q
    it += 1

syst.post();
```

```python
plt.close('all')
fig, axes = plt.subplots(nrows=2, ncols=1, figsize=(7, 7),sharex=True)

axes[0].plot(syst.T,syst.get_values('i_sd_g01'),label='i_sd_g01')
axes[0].plot(syst.T,syst.get_values('i_sq_g01'),label='i_sq_g01')

for ax in axes:
    ax.grid()
    ax.legend()
ax.set_xlabel('Time (s)')
```

    Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …

    No handles with labels found to put in legend.

    Text(0.5, 0, 'Time (s)')

```python
L_t
```

```python
K_c
```

    matrix([[ 3.05930459e-02,  2.28423291e-04, -5.37914607e-04,
             -4.28085968e-06, -2.70547919e-02, -2.11027070e-04,
              2.11938528e-01, -1.06665661e-01],
            [-2.28423291e-04,  3.05930459e-02,  4.28085968e-06,
             -5.37914607e-04,  2.11027070e-04, -2.70547919e-02,
              1.06665661e-01,  2.11938528e-01]])

```python

```

```python
syst = vsc_lcl_class()
syst.Dt = 5e-6
syst.decimation =1
syst.N_store =100_000
syst.update()

Δt = 100e-6
#x_d_ctrl_list = ['i']   # states to consider in the reduction
z_ctrl_list = [ 'i_sd_g01', 'i_sq_g01']     # outputs to consider in the controller
u_ctrl_list = ['eta_d_g01','eta_q_g01']     # intputs to consider in the controller
z_ctrl_idxs = [syst.outputs_list.index(item) for item in z_ctrl_list]
u_ctrl_idxs = [syst.inputs_run_list.index(item) for item in u_ctrl_list]

syst.Δt = Δt

## Calculate equilibirum point
syst.initialize([{'G_d_g01':0.0,'eta_d_g01':0.0,'eta_q_g01':-0.8693333,'v_1_Q':-326,'v_1_D':0.0, 'C_m_g01':4e-6}],xy0=1000)
ssa.eval_ss(syst)

# linear continous plant
A_p = syst.A
B_p = syst.B
C_p = syst.C
D_p = syst.D

# plant discretization
A_d,B_d,C_d,D_d,Dt = sctrl.cont2discrete((A_p,B_p,C_p,D_p),Δt,method='zoh')

N_z_d,N_x_d = C_d.shape  # discreticed plant dimensions
N_x_d,N_u_d = B_d.shape

# convenient matrices
O_ux = np.zeros((N_u_d,N_x_d))
O_xu = np.zeros((N_x_d,N_u_d))
O_uu = np.zeros((N_u_d,N_u_d))
I_uu = np.eye(N_u_d)

syst.A_d = A_d
syst.B_d = B_d

# Controller ##################################################################################
B_c = B_d[:,u_ctrl_idxs]
C_c = C_d[z_ctrl_idxs,:]
D_c = D_d[z_ctrl_idxs,:][:,u_ctrl_idxs]

N_x_c,N_u_d = B_c.shape
N_z_c,N_x_c = C_c.shape

O_ux = np.zeros((N_u_d,N_x_d))
O_xu = np.zeros((N_x_d,N_u_d))
O_uu = np.zeros((N_u_d,N_u_d))
I_uu = np.eye(N_u_d)

# discretized plant:
# Δx_d = A_d*Δx_d + B_d*Δu_d
# Δz_c = C_c*Δx_d + D_c*Δu_d

# dinamic extension:
# Δx_d = A_d*Δx_d + B_d*Δu_d
# Δx_i = Δx_i + Δt*(Δz_c-Δz_c_ref) = Δx_i + Δt*C_c*Δx_d - Dt*Δz_c_ref
# Δz_c = z_c - z_c_0
# Δz_c_ref = z_c_ref - z_c_0
# (Δz_c-Δz_c_ref) = z_c - z_c_ref
omega_b = 2*np.pi*50

W =  np.block([
                [   np.cos(omega_b*Δt), -np.sin(omega_b*Δt)],
                [   np.sin(omega_b*Δt),  np.cos(omega_b*Δt)],
               ])

A_e = np.block([
                [    A_d,  B_c@W, O_xu],    # Δx_d
                [   O_ux,   O_uu, O_uu],    # Δx_r
                [ Δt*C_c, Δt*D_c, I_uu],    # Δx_i
               ])

B_e = np.block([
                [   O_xu],
                [   I_uu],
                [   O_uu],
               ])

A_ctrl = A_e[N_x_d:,N_x_d:]
B_ctrl = B_e[N_x_d:]

# weighting matrices
Q_c = np.eye(A_e.shape[0])
Q_c[-1,-1] = 1e7
Q_c[-2,-2] = 1e7

R_c = np.eye(B_c.shape[1])

K_c,S_c,E_c = ssa.dlqr(A_e,B_e,Q_c,R_c)

E_cont = np.log(E_c)/Δt

syst.A_ctrl = A_ctrl
syst.B_ctrl = B_ctrl
syst.K_c = K_c
syst.N_x_d = N_x_d  # number of plant states
syst.N_u_d = N_u_d  # number of plant inputs
syst.N_z_c = N_z_c  # number of plant outputs considered for the controller
```

```python
E_cont.imag/2/np.pi
```

    array([ 4961.58042816, -4961.58042816,   181.10033477,  -181.10033477,
             131.13747671,  -131.13747671,  4938.38239422, -4938.38239422,
               0.        ,  5000.        ])

```python
W
```

    array([[ 0.99950656, -0.03141076],
           [ 0.03141076,  0.99950656]])

```python
-E_cont.real/np.abs(E_cont)
```

    array([0.02782544, 0.02782544, 0.75202455, 0.75202455, 0.82785763,
           0.82785763, 0.95872622, 0.95872622, 1.        , 0.99213562])

```python
E_cont
```

    array([   -867.78091113+31174.52924662j,    -867.78091113-31174.52924662j,
             -1298.24512951 +1137.88696253j,   -1298.24512951 -1137.88696253j,
             -1216.03836363  +823.96106691j,   -1216.03836363  -823.96106691j,
           -104624.92913934+31028.77170057j, -104624.92913934-31028.77170057j,
           -248040.19728861    +0.j        , -249017.20157367+31415.9265359j ])

```python
syst = vsc_lcl_class()
syst.Dt = 5e-6
syst.decimation =1
syst.N_store =100_000
syst.update()
times = np.arange(0.0,0.1,Δt)

syst.initialize([{'G_d_g01':0.0,'eta_d_g01':0.0,'eta_q_g01':-0.8693333,'v_1_Q':-326,'v_1_D':0.0, 'C_m_g01':4e-6}],xy0=1000)
ssa.eval_A(syst)
i_sd = syst.get_value('i_sd_g01')
i_sq = syst.get_value('i_sq_g01')
v_sd = syst.get_value('v_sd_g01')
v_sq = syst.get_value('v_sq_g01')
i_td = syst.get_value('i_td_g01')
i_tq = syst.get_value('i_tq_g01')
v_md = syst.get_value('v_md_g01')
v_mq = syst.get_value('v_mq_g01')
v_dc = syst.get_value('v_dc_g01')
eta_d = syst.get_value('eta_d_g01')
eta_q = syst.get_value('eta_q_g01')
i_sd_ref_0 = i_sd
i_sq_ref_0 = i_sq
v_sq_0 = v_sq
v_sd_0 = v_sd
x_d_0 = np.array([i_td,i_tq,v_md,v_mq,i_sd,i_sq]).reshape(6,1)
u_d_0 = np.array([eta_d,eta_q]).reshape(2,1)
x_r_0 = u_d_0
syst.Δx_e = np.zeros((10,1))
it = 0
for t in times:

    Δx_e = syst.Δx_e
    # measurements
    i_sd = syst.get_value('i_sd_g01')
    i_sq = syst.get_value('i_sq_g01')
    v_sd = syst.get_value('v_sd_g01')
    v_sq = syst.get_value('v_sq_g01')
    i_td = syst.get_value('i_td_g01')
    i_tq = syst.get_value('i_tq_g01')
    v_md = syst.get_value('v_md_g01')
    v_mq = syst.get_value('v_mq_g01')
    v_dc = syst.get_value('v_dc_g01')

    x_d = np.array([i_td,i_tq,v_md,v_mq,i_sd,i_sq]).reshape(6,1)

    Δx_d = x_d - x_d_0
    Δx_r = syst.Δx_e[N_x_c:-N_u_d,:]
    Δx_i = syst.Δx_e[(N_x_c+N_u_d):,:]

    i_sd_ref = i_sd_ref_0
    i_sq_ref = i_sq_ref_0
    v_sq = v_sq_0
    v_sd = v_sd_0
    if t>20e-3: i_sd_ref = 20
    if t>30e-3: i_sq_ref = 30
    if t>45e-3: v_sd =  163
    if t>45e-3: v_sq = -163
    epsilon_d = i_sd - i_sd_ref
    epsilon_q = i_sq - i_sq_ref

    epsilon = np.block([[epsilon_d],[epsilon_q]])

    Δu_r = -K_c @ Δx_e + np.block([[ (v_sd-v_sd_0)*2/v_dc],[(v_sq-v_sq_0)*2/v_dc]])

    Δx_r = Δu_r
    Δx_i += Δt*epsilon

    Δx_e = np.block([[Δx_d],[Δx_r],[Δx_i]])

    syst.Δx_e  =  Δx_e

    x_r = Δx_r + x_r_0

    eta_dq = W@x_r
    eta_d = eta_dq[0,0]
    eta_q = eta_dq[1,0]

    events=[{'t_end':t,'eta_d_g01':eta_d,'eta_q_g01':eta_q,'v_1_Q':v_sq,'v_1_D':v_sd}]
    syst.run(events)

#    eta_d_prev = eta_d
#    eta_q_prev = eta_q
    it += 1

syst.post();
```

```python
plt.close('all')
fig, axes = plt.subplots(nrows=2, ncols=1, figsize=(7, 7),sharex=True)

axes[0].plot(syst.T,syst.get_values('i_sd_g01'),label='i_sd_g01')
axes[0].plot(syst.T,syst.get_values('i_sq_g01'),label='i_sq_g01')

axes[1].plot(syst.T,syst.get_values('eta_D_g01'),label='eta_D_g01')
axes[1].plot(syst.T,syst.get_values('eta_Q_g01'),label='eta_Q_g01')
for ax in axes:
    ax.grid()
    ax.legend()
ax.set_xlabel('Time (s)')
```

    Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …

    Text(0.5, 0, 'Time (s)')

```python
ssa.damp_report(syst)

```

      Real
      Imag
      Freq.
      Damp

      Mode 1
      -15.707963
      20314.153097
      3233.09788
      0.000773

      Mode 2
      -15.707963
      -20314.153097
      3233.09788
      0.000773

      Mode 3
      -15.707963
      19685.834566
      3133.09788
      0.000798

      Mode 4
      -15.707963
      -19685.834566
      3133.09788
      0.000798

      Mode 5
      -31.415927
      314.159265
      50.00000
      0.099504

      Mode 6
      -31.415927
      -314.159265
      50.00000
      0.099504

```python
import sympy as sym

x_d_1,x_d_2,x_d_3,x_d_4,x_d_5,x_d_6 = sym.symbols('Dx_d_1,Dx_d_2,Dx_d_3,Dx_d_4,Dx_d_5,Dx_d_6')
x_r_1,x_r_2 = sym.symbols('Dx_r_1,Dx_r_2')
x_i_1,x_i_2 = sym.symbols('Dx_i_1,Dx_i_2')

x_e = sym.Matrix([x_d_1,x_d_2,x_d_3,x_d_4,x_d_5,x_d_6,x_r_1,x_r_2,x_i_1,x_i_2])
u_r = -K_c * x_e
```

```python
print(u_r[0])
```

    0.023914820787791*Dx_d_1 + 0.00124312201525765*Dx_d_2 + 0.00210868343440974*Dx_d_3 + 9.76673467759675e-5*Dx_d_4 - 0.0363072666129747*Dx_d_5 - 0.0013695946212219*Dx_d_6 - 10.022203616505*Dx_i_1 + 1.45742192739155*Dx_i_2 - 0.327372378893965*Dx_r_1 - 0.000502327486946109*Dx_r_2

```python
print(u_r[0])
```

    0.023914820787791*Dx_d_1 + 0.00124312201525765*Dx_d_2 + 0.00210868343440974*Dx_d_3 + 9.76673467759675e-5*Dx_d_4 - 0.0363072666129747*Dx_d_5 - 0.0013695946212219*Dx_d_6 - 10.022203616505*Dx_i_1 + 1.45742192739155*Dx_i_2 - 0.327372378893965*Dx_r_1 - 0.000502327486946109*Dx_r_2

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

    -0.00124312201525856*Dx_d_1 + 0.0239148207877911*Dx_d_2 - 9.76673467759516e-5*Dx_d_3 + 0.00210868343440976*Dx_d_4 + 0.00136959462122251*Dx_d_5 - 0.0363072666129741*Dx_d_6 - 1.45742192739181*Dx_i_1 - 10.0222036165041*Dx_i_2 + 0.000502327486927267*Dx_r_1 - 0.327372378893964*Dx_r_2

```python
syst.get_value('C_m_g01')
```

    4e-06

```python

```