# Table of Contents

1  TP 1 - Programmation pour la préparation à l'agrégation maths option info

1.1  Remarques sur le style

2  Fonctions

2.1  Exercice 4

2.2  Exercice 5

2.3  Exercice 6

3  Récursivité

3.1  Exercice 7

3.2  Exercice 8

3.3  Exercice 9

3.4  Exercice 10

3.5  Exercice 11

4  Listes

4.1  Exercice 12

4.2  Exercice 13

4.3  Exercice 14

4.4  Exercice 15

4.5  Exercice 16

4.6  Exercice 17

5  Exponentiation rapide

5.1  Exercice 18

5.2  Exercice 19

5.3  Exercice 20

5.4  Exercice 21

5.5  Exercice 22

5.6  Exercice 23

6  Formule du calcul propositionnel

6.1  Exercice 24

6.2  Exercice 25

6.3  Exercice 26

6.4  Exercice 27

6.5  Exercice 28

7  Conclusion

# TP 1 - Programmation pour la préparation à l'agrégation maths option info

## Remarques sur le style
Ici, pour vous montrer, j'ai utilisé :
- En Python 3, avec des annotations de types.
- Notez que ce ne sont que des annotations.
- Pour plus, il faudrait utiliser [quelque chose comme `beartype`](https://stackoverflow.com/a/37961120/2809027).

Notez que ce n'est absolument *pas nécessaire*, c'était juste pour montrer que ce genre d'annotations peut aider à passer de Caml à Python.

# Fonctions

## Exercice 4

```python
def successeur(i : int) -> int:
    assert isinstance(i, int), "Erreur : i = {} doit etre entier.".format(i)
    assert i >= 0, "Erreur : i = {} doit etre >= 0.".format(i)
    return i + 1
```

```python
successeur(3)
successeur(2 * 2)
successeur(2.5)
```

## Exercice 5

```python
def produit3(x, y, z):
    return x * y * z

produit3_2 = lambda x, y, z: x * y * z

produit3_3 = lambda x: lambda y: lambda z: x * y * z
```

```python
produit3(1, 2, 3)
produit3_2(1, 2, 3)
produit3_3(1)(2)  # lambda z : 1 * 2 * z
f = produit3_3(1)(2)  # lambda z : 1 * 2 * z
f(3)
produit3_3(1)(2)(3)
```

    6

    6

    <function __main__.<lambda>.<locals>.<lambda>.<locals>.<lambda>(z)>

    6

    6

## Exercice 6

```python
def exercice6(n : int) -> None:
    for i in range(1, n + 1):
        print(i)
    for i in range(n, 0, -1):
        print(i)
```

```python
exercice6(4)
```

    1
    2
    3
    4
    4
    3
    2
    1

# Récursivité

C'est simple. Pas besoin d'un mot clé `rec` quand on définit une fonction récursive en Python.

## Exercice 7

```python
def factorielle(n : int) -> int:
    if n <= 0:
        return 0
    elif n == 1:
        return 1
    else:
        return n * factorielle(n - 1)
```

```python
for i in range(8):
    print("{}! = {:>5}".format(i, factorielle(i)))
    # Note : ce {:>5} signifie que l'affichage utilise au moins
    # 5 caractères, pour avoir un joli alignement :
```

    0! =     0
    1! =     1
    2! =     2
    3! =     6
    4! =    24
    5! =   120
    6! =   720
    7! =  5040

## Exercice 8

Pour ce genre de fonction, j'aime bien afficher les appels récursifs. On peut rendre ça optionnel, avec comme ici un argument nommé `log`, à valeur par défaut `log=False`.

```python
def pgcd(x : int, y : int, log: bool=False) -> int:
    if log: print("x = {:>7}, y = {:>7}".format(x, y))
    assert x >= 0 and y >= 0
    if y == 1:
        return 1
    elif y == x or y == 0:
        return x
    if x < y:
        return pgcd(x, y % x, log=log)
    else:
        return pgcd(y, x % y, log=log)
```

```python
pgcd(10, 5)
```

    5

On peut faire des essais pour afficher notre calcul de PGCD, sur des entiers aléatoires entre $1$ et $100$.

Pour se rassurer, on peut chercher dans la bibliothèque standard, à partir de Python 3.6 il y a la fonction `math.gcd` pour calculer le PGCD, sinon `fractions.gcd` ou encore dans le module (non standard) SymPy `sympy.gcd`.

```python
try:
    from math import gcd
except ImportError:
    try:
        from fractions import gcd
    except ImportError:
        from sympy import gcd

from random import randint
for _ in range(10):
    x = randint(1, 100)
    y = randint(1, 100)
    d = pgcd(x, y)
    print("{:>3} ^ {:>3} = {:>3}".format(x, y, d))
    assert d == gcd(x, y), "Erreur : mauvais calcul de pgcd(x={}, y={}) = {}, alors qu'il devrait etre = {}.".format(x, y, pgcd(x, y), gcd(x, y))
```

     61 ^  61 =  61
     44 ^  85 =   1
     48 ^  79 =   1
     84 ^  83 =   1
     17 ^  20 =   1
     39 ^   2 =   1
     30 ^  68 =   2
     14 ^  87 =   1
     46 ^  63 =   1
     83 ^  84 =   1

## Exercice 9

Rien de particulier à faire, la récursivité fonctionne sans réfléchir :

```python
def fibonacci(n : int) -> int:
    assert n >= 0
    if n == 0 or n == 1:
        return 1
    else:
        return fibonacci(n - 1) + fibonacci(n - 2)
```

Dans IPython (ou Jupyter) on peut facilement mesurer le temps (moyen) d'exécution d'une fonction sur une entrée donnée, avec la *macro* `%timeit`.

```python
%timeit fibonacci(35)
```

    4.92 s ± 90.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Dans Python, on peut faire pareil avec `timeit()` du module `timeit`.

```python
from timeit import timeit
timeit("fibonacci(35)", globals=globals())
```

Pour la version qui est en complexité (temporelle) linéaire dans l'entrée $n$, il suffit de déplier la récursion en calculant $F_{n+1}$ progressivement en partant de $F_0, F_1$.

```python
def fibonacci_lin(n : int) -> int:
    un, uns = 1, 1
    for _ in range(n):
        un, uns = uns, un + uns
    return un
```

On peut vérifier que cette deuxième implémentation fonctionne comme la première, et ensuite vérifier qu'elle est (bien) plus rapide.

```python
for i in range(10):
    assert fibonacci(i) == fibonacci_lin(i)
```

```python
%timeit fibonacci_lin(35)
```

    1.79 µs ± 27.3 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

Voilà la différence.

## Exercice 10

Aucune hypothèse n'est faite sur les arguments de la fonction, on supposera seulement qu'elle est itérable sur sa sortie.

```python
from types import FunctionType  # inutile, juste pour faire joli

# première version, f : type x -> type x (simple)
def itere(f : FunctionType, n : int) -> FunctionType:
    def fn(x):
        for _ in range(n):
            x = f(x)
        return x
    return fn
```

```python
itere(lambda x: x + 1, 10)(1)
```

    11

```python
# deuxième version, f : tuple -> tuple (simple)
def itere2(f : FunctionType, n : int) -> FunctionType:
    def fn(*args):
        for _ in range(n):
            if isinstance(args, tuple):
                args = f(*args)
            else:
                args = f(args)
        if isinstance(args, tuple) and len(args) == 1:
            return args[0]
        else:
            return args
    return fn
```

```python
def plusun(x):
    return x + 1

itere2(plusun, 10)(1)

def foisdeux(x, y):
    return x * 2, y * 2

itere2(foisdeux, 10)(1, 2)
```

    11

    (1024, 2048)

## Exercice 11

C'est assez naturel, on affiche la suite de mouvement à faire, comme une suite de `"T1 -> T2"` par exemple.

```python
def hanoi(x : str, y : str, z : str, n : int) -> None:
    def hanoiaux(orig : str, dest : str, inter : str, n : int):
        if n == 0:
            return 0
        c1 = hanoiaux(orig, inter, dest, n - 1)
        print("{} -> {}".format(orig, dest))
        c2 = hanoiaux(inter, dest, orig, n - 1)
        return c1 + 1 + c2
    return hanoiaux(x, z, y, n)
```

```python
hanoi("T1", "T2", "T3", 1)
```

    T1 -> T3

    1

```python
hanoi("T1", "T2", "T3", 2)
```

    T1 -> T2
    T1 -> T3
    T2 -> T3

    3

```python
hanoi("T1", "T2", "T3", 5)  # 31 = 2^5 - 1
```

    T1 -> T3
    T1 -> T2
    T3 -> T2
    T1 -> T3
    T2 -> T1
    T2 -> T3
    T1 -> T3
    T1 -> T2
    T3 -> T2
    T3 -> T1
    T2 -> T1
    T3 -> T2
    T1 -> T3
    T1 -> T2
    T3 -> T2
    T1 -> T3
    T2 -> T1
    T2 -> T3
    T1 -> T3
    T2 -> T1
    T3 -> T2
    T3 -> T1
    T2 -> T1
    T2 -> T3
    T1 -> T3
    T1 -> T2
    T3 -> T2
    T1 -> T3
    T2 -> T1
    T2 -> T3
    T1 -> T3

    31

----
# Listes
## Exercice 12
Les listes en Python sont des `list`.

Elles ne fonctionnent **pas** comme des listes simplement chaînées comme en Caml : ce sont des tableaux, donc l'accès à `T[i]` pour n'importe quel `i` est en $\mathcal{O}(1)$ pour n'importe quel `i` et `n`.

```python
def concatene(liste1 : list, liste2 : list) -> list:
    # return liste1 + liste2  # solution facile
    n1, n2 = len(liste1), len(liste2)
    res = []
    for i in range(n1 + n2):
        if i < n1:
            res.append(liste1[i])
        else:
            res.append(liste2[i - n1])
    return res
```

```python
concatene([1, 2, 3], [4, 5])
```

    [1, 2, 3, 4, 5]

## Exercice 13

```python
from types import FunctionType

def applique(f : FunctionType, liste : list) -> list:
    res = [f(x) for x in liste]  # solution facile
    res = map(f, liste)  # encore plus facile
    # en itérant la liste :
    res = [ ]  # tableau vide
    for x in liste:
        res.append(f(x))
    # en itérant ses valeurs :
    res = [ ]  # tableau
    for i in range(len(liste)):
        res.append(f(liste[i]))
    return res
```

```python
applique(lambda x : x + 1, [1, 2, 3])
```

    [2, 3, 4]

## Exercice 14

```python
def liste_carree(liste : list) -> list:
    return applique(lambda x: x**2, liste)
```

```python
liste_carree([1, 2, 3])
```

    [1, 4, 9]

## Exercice 15

```python
def miroir_quad(liste : list) -> list:
    # pour le faire naïvement en Python
    # on fait une concaténation de liste
    if liste:
        return miroir_quad(liste[1:]) + [ liste[0] ]
    else:
        return []

def miroir_lin(liste : list) -> list:
    return [liste[-i] for i in range(1, len(liste) + 1)]
```

```python
miroir_quad([1, 2, 3])
```

    [3, 2, 1]

```python
%timeit miroir_quad(list(range(1000)))
```

    3.13 ms ± 93.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

```python
miroir_lin([1, 2, 3])
```

    [3, 2, 1]

```python
%timeit miroir_lin(list(range(1000)))
```

    77.2 µs ± 7.47 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

## Exercice 16

```python
def insertion_dans_liste_triee(entiers : list, x : int) -> list:
    i = 0
    while i < len(entiers) and entiers[i] < x:
        i += 1
    return entiers[:i] + [x] + entiers[i:]
```

```python
insertion_dans_liste_triee([1, 2, 5, 6], 4)
```

    [1, 2, 4, 5, 6]

```python
def tri_insertion(entiers : list) -> list:
    assert all([isinstance(i, int) for _ in entiers])
    def trix(e : list, acc : list) -> list:
        if len(e) == 0: return acc
        else: return trix(e[1:], insertion_dans_liste_triee(acc, e[0]))
    return trix(entiers, [])
```

```python
tri_insertion([5, 2, 6, 1, 4])
```

    [1, 2, 4, 5, 6]

## Exercice 17

```python
def ordre_decroissant(x, y):
    return x > y
```

```python
from types import FunctionType

def insertion_dans_liste_triee2(entiers : list, x : int, ordre : FunctionType) -> list:
    i = 0
    while i < len(entiers) and ordre(entiers[i], x):
        i += 1
    return entiers[:i] + [x] + entiers[i:]
```

```python
insertion_dans_liste_triee2([6, 5, 2, 1], 4, ordre_decroissant)
```

    [6, 5, 4, 2, 1]

```python
def tri_insertion2(entiers : list, ordre : FunctionType) -> list:
    assert all([isinstance(i, int) for _ in entiers])
    def trix(e : list, acc : list) -> list:
        if len(e) == 0: return acc
        else: return trix(e[1:], insertion_dans_liste_triee2(acc, e[0], ordre))
    return trix(entiers, [])
```

```python
tri_insertion([5, 2, 6, 1, 4])
tri_insertion2([5, 2, 6, 1, 4], ordre_decroissant)
```

    [1, 2, 4, 5, 6]

    [6, 5, 4, 2, 1]

----
# Exponentiation rapide
## Exercice 18

Une approche naïve :

```python
def exp(x : int, n : int) -> int:
    res = 1
    for _ in range(n):
        res *= x
    return res
```

```python
x = 3
for n in range(8):
    print("  {} ** {} = {:>5}".format(x, n, exp(x, n)))
```

      3 ** 0 =     1
      3 ** 1 =     3
      3 ** 2 =     9
      3 ** 3 =    27
      3 ** 4 =    81
      3 ** 5 =   243
      3 ** 6 =   729
      3 ** 7 =  2187

## Exercice 19

Une approche maline (optimale, en fait).

```python
def exp2(x : int, n : int) -> int:
    assert n >= 0
    if n == 0:
        return 1
    elif n == 1:
        return x
    elif n % 2 == 0:
        return exp2(x ** 2, n // 2)
    elif n % 2 == 1:
        return exp2(x ** 2, (n - 1) // 2) * x
```

```python
x = 3
for n in range(8):
    print("  {} ** {} = {:>5}".format(x, n, exp2(x, n)))
```

      3 ** 0 =     1
      3 ** 1 =     3
      3 ** 2 =     9
      3 ** 3 =    27
      3 ** 4 =    81
      3 ** 5 =   243
      3 ** 6 =   729
      3 ** 7 =  2187

## Exercice 20
On ne dispose pas de typage pour faire ça aussi joliment qu'en Caml...

```python
def mult_float(x : float, y : float) -> float:
    return x * y

monoide_flottants = {
    'mult': mult_float,   # (*.) en Caml
    'neutre': 1.
}
```

```python
import numpy as np

def mult_ndarray(A : np.array, B : np.array) -> np.array:
    return np.dot(A, B)

# Pas possible d'avoir un neutre pour une taille quelconque !
monoide_nparray = lambda n, m: {
    'mult': mult_ndarray,
    'neutre': np.eye(n, m)
}
```

```python
mult_ndarray([[1, 1], [1, 1]], [[1, 2], [3, 4]])
```

    array([[4, 6],
           [4, 6]])

Manuellement ce n'est pas trop dur :

```python
def mult_mat(A : list, B : list) -> list:
    n, m = len(A), len(A[0])  # A est (n, m)
    m2, p = len(B), len(B[0]) # B est (m2, p)
    assert m == m2
    C = [[0 for _ in range(p)] for _ in range(n)]  # C est (n, p)
    for i in range(n):
        for j in range(p):
            for k in range(m):
                C[i][j] += A[i][k] * B[k][j]
    return C

# Pas possible d'avoir un neutre pour une taille quelconque !
monoide_mat = lambda n, m: {
    'mult': mult_mat,
    'neutre': [[int(i==j) for j in range(m)] for i in range(n)]  # I est (n, m)
}
```

```python
mult_mat([[1, 1], [1, 1]], [[1, 2], [3, 4]])
```

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

## Exercice 21

```python
def exp_rapide(monoide : dict, x, n : int):
    mult = monoide['mult']
    assert n >= 0
    if n == 0:
        return monoide['neutre']
    elif n == 1:
        return x
    elif n % 2 == 0:
        return exp_rapide(monoide, mult(x, x), n // 2)
    elif n % 2 == 1:
        return mult(exp_rapide(monoide, mult(x, x), (n - 1) // 2), x)
```

## Exercice 22

```python
def exp_rapide_float(x : float, n : int) -> float:
    return exp_rapide(monoide_flottants, x, n)
```

```python
exp_rapide_float(2.0, 8)
```

    256.0

```python
exp_rapide_float(0.2, 8)
```

    2.560000000000002e-06

Et pour les matrices, un petit piège à cause des tailles :

```python
def exp_rapide_mat(A : list, k : int) -> float:
    n, m = len(A), len(A[0])
    mono = monoide_mat(n, m)
    return exp_rapide(mono, A, k)
```

```python
for k in range(5):
    exp_rapide_mat([[1, 1], [1, 1]], k)
```

    [[1, 0], [0, 1]]

    [[1, 1], [1, 1]]

    [[2, 2], [2, 2]]

    [[4, 4], [4, 4]]

    [[8, 8], [8, 8]]

```python
for k in range(5):  # nilpotente
    exp_rapide_mat([[0, 1, 2], [0, 0, 1], [0, 0, 0]], k)
```

    [[1, 0, 0], [0, 1, 0], [0, 0, 1]]

    [[0, 1, 2], [0, 0, 1], [0, 0, 0]]

    [[0, 0, 1], [0, 0, 0], [0, 0, 0]]

    [[0, 0, 0], [0, 0, 0], [0, 0, 0]]

    [[0, 0, 0], [0, 0, 0], [0, 0, 0]]

## Exercice 23

```python
from operator import mul

def exp_rapide_2(x, n : int):
    def mul_par_x(y):
        return x * y
    return itere(mul_par_x, n)(x)
```

```python
exp_rapide_2(2.0, 8)
```

    512.0

----
# Formule du calcul propositionnel

**Spoiler alert** : en Python, c'est chaud.

**ATTENTION** Il faut un peut d'habitude et d'idées pour réussir à faire tout ça aussi efficacement (enfin presque) qu'en OCaml.

On va faire des choses non typées, avec des dictionnaires, pour bidouiller.

## Exercice 24

``(not p ^ ((q ^ not p) v (r v q)))`` s'écrit en Caml `` (Conj(Not(V("p")),Disj(Conj(V("q"),Not(V("p"))),Disj(V("r"),V("q")))))``.

On va imbriquer des dictionnaires, les types, `V`, `Not`, `Conj` et `Disj` seront des clés, et les valeurs seront des formules ou couples de formules.
Mais on va cacher ça et l'utilisateur verra exactement la même chose qu'en Caml !

```python
V = lambda x: {'V': x}
Not = lambda x: {'Not': x}
Disj = lambda x, y: {'Disj': (x, y)}
Conj = lambda x, y: {'Conj': (x, y)}

p = V('p')
r = V('r')
q = V('q')
not_p = Not(p)

f = Conj(Not(p), Disj(Conj(q, not_p), Disj(r, q)))
f
```

    {'Conj': ({'Not': {'V': 'p'}},
      {'Disj': ({'Conj': ({'V': 'q'}, {'Not': {'V': 'p'}})},
        {'Disj': ({'V': 'r'}, {'V': 'q'})})})}

```python
(Conj(Not(V("p")),Disj(Conj(V("q"),Not(V("p"))),Disj(V("r"),V("q")))))
```

    {'Conj': ({'Not': {'V': 'p'}},
      {'Disj': ({'Conj': ({'V': 'q'}, {'Not': {'V': 'p'}})},
        {'Disj': ({'V': 'r'}, {'V': 'q'})})})}

## Exercice 25

Ensuite on fait une filtration "à la main" sur les clés du dictionnaire (de niveau 1, on ne récurse pas quand on teste `'V' in formule`).

```python
def taille(formule : dict) -> str:
    if 'V' in formule:
        return 0
    elif 'Not' in formule:
        return 1 + taille(formule['Not'])
    elif 'Conj' in formule:
        x, y = formule['Conj']
        return 1 + taille(x) + taille(y)
    elif 'Disj' in formule:
        x, y = formule['Disj']
        return 1 + taille(x) + taille(y)
```

```python
taille(f)
```

    6

## Exercice 26

```python
def formule_to_string(formule : dict) -> str:
    if 'V' in formule:
        return formule['V']
    elif 'Not' in formule:
        return "~" + formule_to_string(formule['Not'])
    elif 'Conj' in formule:
        x, y = formule['Conj']
        return "(" + formule_to_string(x) + " ^ " + formule_to_string(y) + ")"
    elif 'Disj' in formule:
        x, y = formule['Disj']
        return "(" + formule_to_string(x) + " V " + formule_to_string(y) + ")"
```

```python
def affiche(formule):
    print(formule_to_string(formule))
```

```python
affiche(f)
```

    (~p ^ ((q ^ ~p) V (r V q)))

Et voilà. Pas tellement plus dur hein !

## Exercice 27
Les valeurs des variables seront données comme un dictionnaire associant nom de variable à valeurs booléennes.
Et comme on veut frimer, on prend un [`defaultdict`](https://docs.python.org/3/library/collections.html#collections.defaultdict).

```python
from collections import defaultdict

d = defaultdict(lambda: False, {'p': True, 'q': False})
d['p']  # -> True car présent et True
d['q']  # -> False car présent et False
d['x']  # -> False car absent
```

    True

    False

    False

```python
valeurs_1 = defaultdict(lambda: False, {'p': True})
```

```python
valeurs_2 = defaultdict(lambda: False, {'r': True})
```

```python
def eval(valeurs : dict, formule : dict) -> bool:
    if 'V' in formule:
        return valeurs[formule['V']]
    elif 'Not' in formule:
        return not eval(valeurs, formule['Not'])
    elif 'Conj' in formule:
        x, y = formule['Conj']
        return eval(valeurs, x) and eval(valeurs, y)
    elif 'Disj' in formule:
        x, y = formule['Disj']
        return eval(valeurs, x) or eval(valeurs, y)
```

```python
eval(valeurs_1, f)
```

    False

```python
eval(valeurs_2, f)
```

    True

## Exercice 28

```python
def extrait_variables_x(formule : dict) -> list:
    if 'V' in formule:
        return [formule['V']]
    elif 'Not' in formule:
        return extrait_variables_x(formule['Not'])
    elif 'Conj' in formule:
        x, y = formule['Conj']
        return extrait_variables_x(x) + extrait_variables_x(y)
    elif 'Disj' in formule:
        x, y = formule['Disj']
        return extrait_variables_x(x) + extrait_variables_x(y)

# on enlève les doublons
def extrait_variables(formule : dict) -> list:
    return list(set(extrait_variables_x(formule)))
```

```python
extrait_variables(f)
```

    ['p', 'r', 'q']

On trouve facilement les $n$ variables d'une formule.

Ensuite, un [`itertools.product`](https://docs.python.org/3/library/itertools.html#itertools.product) permet de générer les $2^n$ valuations.

```python
from itertools import product

def toutes_valeurs(variables):
    for valeurs in product([False, True], repeat=len(variables)):
        yield defaultdict(lambda: False, {k:v for k,v in zip(variables, valeurs)})
```

```python
list(toutes_valeurs(extrait_variables(f)))
```

    [defaultdict(<function __main__.toutes_valeurs.<locals>.<lambda>>,
                 {'p': False, 'q': False, 'r': False}),
     defaultdict(<function __main__.toutes_valeurs.<locals>.<lambda>>,
                 {'p': False, 'q': True, 'r': False}),
     defaultdict(<function __main__.toutes_valeurs.<locals>.<lambda>>,
                 {'p': False, 'q': False, 'r': True}),
     defaultdict(<function __main__.toutes_valeurs.<locals>.<lambda>>,
                 {'p': False, 'q': True, 'r': True}),
     defaultdict(<function __main__.toutes_valeurs.<locals>.<lambda>>,
                 {'p': True, 'q': False, 'r': False}),
     defaultdict(<function __main__.toutes_valeurs.<locals>.<lambda>>,
                 {'p': True, 'q': True, 'r': False}),
     defaultdict(<function __main__.toutes_valeurs.<locals>.<lambda>>,
                 {'p': True, 'q': False, 'r': True}),
     defaultdict(<function __main__.toutes_valeurs.<locals>.<lambda>>,
                 {'p': True, 'q': True, 'r': True})]

```python
def str_of_bool(x : bool) -> str:
    # return str(int(x))
    return '1' if x else '0'
```

```python
def table_verite(formule : dict) -> None:
    variables = extrait_variables(formule)
    # D'abord la formule
    for k in variables:
        print(k, end=' ')
    print('| ', end='')
    affiche(f)
    # Puis toutes ces valeurs possibles
    for valeurs in toutes_valeurs(variables):
        for k in variables:
            print(str_of_bool(valeurs[k]), end=' ')
        print('| ', end='')
        print(str_of_bool(eval(valeurs, formule)))
```

```python
table_verite(f)
```

    p r q | (~p ^ ((q ^ ~p) V (r V q)))
    0 0 0 | 0
    0 0 1 | 1
    0 1 0 | 1
    0 1 1 | 1
    1 0 0 | 0
    1 0 1 | 0
    1 1 0 | 0
    1 1 1 | 0

Note : ce code est encore plus concis que celui donné dans la solution en Caml.

> On peut vérifier, par exemple sur [Wolfram|Alpha](https%3A%2F%2Fwww.wolframalpha.com%2Finput%2F%3Fi%3D%28%7Ep%2B%2526%2B%28%28q%2B%2526%2B%7Ep%29%2B%257C%2B%28r%2B%257C%2Bq%29%29%29) que l'on obtient bien le bon résultat...

----

# Conclusion

Comme vous le voyez, on arrive à répondre aux mêmes questions dans les deux langages, et il n'y a pas de grosses différences en pratique dans la mise en oeuvre.

Là où Caml excelle pour les types définis, le filtrage et la récursion, Python gagne en simplicité sur l'affichage, sa librairie standard et les dictionnaires et ensembles...


 Validation Measures

#### Read the data

```python
import pickle as pkl

with open('../data/titanic_tansformed.pkl', 'rb') as f:
    df_data = pkl.load(f)
```

```python
df_data.head(8)
```

      Survived
      Age
      SibSp
      Parch
      Fare
      1
      2
      3
      female
      male
      C
      Q
      S

      0
      0
      22.0
      1
      0
      7.2500
      0
      0
      1
      0
      1
      0
      0
      1

      1
      1
      38.0
      1
      0
      71.2833
      1
      0
      0
      1
      0
      1
      0
      0

      2
      1
      26.0
      0
      0
      7.9250
      0
      0
      1
      1
      0
      0
      0
      1

      3
      1
      35.0
      1
      0
      53.1000
      1
      0
      0
      1
      0
      0
      0
      1

      4
      0
      35.0
      0
      0
      8.0500
      0
      0
      1
      0
      1
      0
      0
      1

      5
      0
      25.0
      0
      0
      8.4583
      0
      0
      1
      0
      1
      0
      1
      0

      6
      0
      54.0
      0
      0
      51.8625
      1
      0
      0
      0
      1
      0
      0
      1

      7
      0
      2.0
      3
      1
      21.0750
      0
      0
      1
      0
      1
      0
      0
      1

## Logistic Regression

```python
data = df_data.drop("Survived",axis=1)
label = df_data["Survived"]
```

```python
data.head()
```

      Age
      SibSp
      Parch
      Fare
      1
      2
      3
      female
      male
      C
      Q
      S

      0
      22.0
      1
      0
      7.2500
      0
      0
      1
      0
      1
      0
      0
      1

      1
      38.0
      1
      0
      71.2833
      1
      0
      0
      1
      0
      1
      0
      0

      2
      26.0
      0
      0
      7.9250
      0
      0
      1
      1
      0
      0
      0
      1

      3
      35.0
      1
      0
      53.1000
      1
      0
      0
      1
      0
      0
      0
      1

      4
      35.0
      0
      0
      8.0500
      0
      0
      1
      0
      1
      0
      0
      1

```python
from sklearn.cross_validation import train_test_split
data_train, data_test, label_train, label_test = train_test_split(data, label, test_size = 0.3, random_state = 101)
```

    /Users/talat/anaconda3/lib/python3.6/site-packages/sklearn/cross_validation.py:41: DeprecationWarning: This module was deprecated in version 0.18 in favor of the model_selection module into which all the refactored classes and functions are moved. Also note that the interface of the new CV iterators are different from that of this module. This module will be removed in 0.20.
      "This module will be removed in 0.20.", DeprecationWarning)

```python
from sklearn.linear_model import LogisticRegression

# Run Logistic Regression
log_regr = LogisticRegression()
log_regr.fit(data_train, label_train)
predictions = log_regr.predict(data_test)
```

### Accuracy

\begin{align}
Accuracy = \frac{TP + TN}{TP + TN + FP + FN}
\end{align}

```python
print('Accuracy', log_regr.score(data_test, label_test))
```

    Accuracy 0.8277153558052435

### Precision Recall

\begin{align}
Precision = \frac{TP}{TP + FP}
\end{align}

\begin{align}
Recall = \frac{TP}{TP + FN}
\end{align}

\begin{align}
F1 Score = 2 * \frac{Recall * Precision}{Recall + Precision}
\end{align}

```python
from sklearn.metrics import classification_report
print(classification_report(label_test, predictions))
```

                 precision    recall  f1-score   support

              0       0.82      0.92      0.87       163
              1       0.85      0.68      0.76       104

    avg / total       0.83      0.83      0.82       267

## Cross Validation

```python
from sklearn.model_selection import StratifiedKFold
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
# skf = StratifiedKFold(n_splits=5)

log_regr = LogisticRegression()
log_regr.fit(data_train, label_train)
score = log_regr.score(data_train, label_train)
print('Train accuracy score', score)

score_cv = cross_val_score(log_regr, data_train, label_train, cv=10, scoring='accuracy')
print('Cross Val Accuracy for each run', score_cv)
print('CrossVal Accuracy', score_cv.mean())

```

    Train accuracy score 0.8086816720257235
    Cross Val Accuracy for each run [0.76190476 0.6984127  0.79365079 0.87301587 0.80952381 0.77777778
     0.78688525 0.81967213 0.91803279 0.7704918 ]
    CrossVal Accuracy 0.8009367681498828

## AUC - Receiver Operating Characteristics
- How much a model is capable of distinguishing between classes
- Higher the AUC, better the model is

$
\begin{align}
True Positive Rate = \frac{TP}{TP + FN}
\end{align}
$

$
\begin{align}
\ False Positive Rate = 1 - \frac{TN}{TN + FP} = \frac{FP}{TN + FP}
\end{align}
$

```python
from sklearn import metrics

fpr, tpr, threshold = metrics.roc_curve(label_test, log_regr.predict(data_test))
roc_auc = metrics.auc(fpr, tpr)
print('AUCROC Stage1 vs Healthy: ' , roc_auc)
```

    AUCROC Stage1 vs Healthy:  0.8014688532326569

```python
import matplotlib.pyplot as plt

plt.title('Receiver Operating Characteristic')
plt.plot(fpr, tpr, 'b', label = 'AUC = %0.2f' % roc_auc)
plt.legend(loc = 'lower right')
plt.plot([0, 1], [0, 1],'r--')
plt.xlim([0, 1])
plt.ylim([0, 1])
plt.ylabel('True Positive Rate')
plt.xlabel('False Positive Rate')
plt.show()
```

A really good material for understanding [AUC-ROC](https://towardsdatascience.com/understanding-auc-roc-curve-68b2303cc9c5) visually

```python

```

classification/notebooks/05 - Validation Measures.ipynb


# 1. Set up the notebook

Import modules.

```python
import numpy as np
import sympy as sym
import json
import matplotlib.pyplot as plt
from scipy import linalg
from scipy.interpolate import interp1d
```

Define a function to load data from a hardware flight test and resample it at 100 Hz using linear interpolation. If `only_in_flight=True`, then only data for which the desired position was positive (i.e., "trying to fly" rather than "sitting on the ground") will be returned.

```python
def load_hardware_data(filename, t_min_offset=0, t_max_offset=0, only_in_flight=False):
    # load raw data
    with open(filename, 'r') as f:
        data = json.load(f)

    # convert lists to numpy arrays
    for val in data.values():
        for key in val.keys():
            val[key] = np.array(val[key])

    # create an array of times at which to subsample
    t_min = -np.inf
    t_max = np.inf
    for key, val in data.items():
        t_min = max(t_min, val['time'][0])
        t_max = min(t_max, val['time'][-1])
    t_min += t_min_offset * 1000
    t_max -= t_max_offset * 1000
    nt = int(1 + np.floor((t_max - t_min) / 10.))
    t = np.arange(0, 10 * nt, 10) / 1000.
    resampled_data = {'time': t}

    # resample raw data with linear interpolation
    for k, v in data.items():
        f = interp1d((v['time'] - t_min) / 1000., v['data'])
        resampled_data[k] = f(t)

    # truncate to times when o_z_des is positive
    if only_in_flight:
        i = []
        for k in ['ae483log.o_z_des', 'ctrltarget.z']:
            if k in resampled_data.keys():
                j = np.argwhere(resampled_data[k] > 0).flatten()
                if len(j) > len(i):
                    i = j
        if len(i) < 2:
            raise Exception(
                'Failed to get "only_in_flight" data.\n' + \
                ' - Did you remember to log "ae483log.o_z_des" and was it ever positive?\n' + \
                ' - Did you remember to log "ctrltarget.z" and was it ever positive?\n'
            )
        for key in resampled_data.keys():
            resampled_data[key] = resampled_data[key][i[0]:i[-1]]

    # return the resampled data
    return resampled_data
```

Define a function to compute RMSE.

```python
def rmse(x, y):
    return np.sqrt(np.mean((x - y)**2))
```

# 2. Flight tests to debug observer implementation

## 2.1 Take-off test

**FIXME: REPLACE THIS CELL WITH THE USUAL DESCRIPTION AND VIDEO OF FLIGHT TEST.**

Load and parse data.

```python
# flight test data
data = load_hardware_data('takeoff_1_data.json', only_in_flight=False)

# time
t = data['time']

# state estimates from custom observer
o_x_custom = data['ae483log.o_x']
o_y_custom = data['ae483log.o_y']
o_z_custom = data['ae483log.o_z']
psi_custom = data['ae483log.psi']
theta_custom = data['ae483log.theta']
phi_custom = data['ae483log.phi']
v_x_custom = data['ae483log.v_x']
v_y_custom = data['ae483log.v_y']
v_z_custom = data['ae483log.v_z']

# state estimates from default observer
o_x_default = data['stateEstimate.x']
o_y_default = data['stateEstimate.y']
o_z_default = data['stateEstimate.z']
psi_default = np.deg2rad(data['stateEstimate.yaw'])
theta_default = - np.deg2rad(data['stateEstimate.pitch'])
phi_default = np.deg2rad(data['stateEstimate.roll'])
v_x_default = data['kalman.statePX']
v_y_default = data['kalman.statePY']
v_z_default = data['kalman.statePZ']

# desired position
o_x_des = data['ae483log.o_x_des']
o_y_des = data['ae483log.o_y_des']
o_z_des = data['ae483log.o_z_des']
```

Apply offline implementation of custom observer.

**FIXME: If the observer you are implementing offline (here) is different from the observer you implemented online (in firmware, to produce this flight test), then replace this cell with a description of what you changed (e.g., weights) and why.**

```python
# Define constants
g = 9.81
k_flow = 4.09255568
o_z_eq = 0.0 # <-- FIXME: change equilibrium height
dt = 0.01

# Get inputs
w_x_offline = data['ae483log.w_x']
w_y_offline = data['ae483log.w_y']
w_z_offline = data['ae483log.w_z']
a_z_offline = data['ae483log.a_z']

# Get outputs
n_x_offline = data['ae483log.n_x']
n_y_offline = data['ae483log.n_y']
r_offline = data['ae483log.r']

# Initialize estimates
o_x = 0.
o_y = 0.
o_z = 0.
psi = 0.
theta = 0.
phi = 0.
v_x = 0.
v_y = 0.
v_z = 0.

# Initialize storage (do not change)
o_x_offline = np.zeros(len(t))
o_y_offline = np.zeros(len(t))
o_z_offline = np.zeros(len(t))
psi_offline = np.zeros(len(t))
theta_offline = np.zeros(len(t))
phi_offline = np.zeros(len(t))
v_x_offline = np.zeros(len(t))
v_y_offline = np.zeros(len(t))
v_z_offline = np.zeros(len(t))

# Iterate over all time steps
for i in range(len(t)):
    # Get measurements (do not change)
    w_x = w_x_offline[i]
    w_y = w_y_offline[i]
    w_z = w_z_offline[i]
    a_z = a_z_offline[i]
    n_x = n_x_offline[i]
    n_y = n_y_offline[i]
    r = r_offline[i]

    # Compute each element of:
    #
    #  C x + D u - y
    #
    n_x_err = 0.  # <-- FIXME
    n_y_err = 0.  # <-- FIXME
    r_err = 0.    # <-- FIXME

    # Update estimates
    o_x += dt * (0.)   # <-- FIXME
    o_y += dt * (0.)   # <-- FIXME
    o_z += dt * (0.)   # <-- FIXME
    psi += dt * (0.)   # <-- FIXME
    theta += dt * (0.) # <-- FIXME
    phi += dt * (0.)   # <-- FIXME
    v_x += dt * (0.)   # <-- FIXME
    v_y += dt * (0.)   # <-- FIXME
    v_z += dt * (0.)   # <-- FIXME

    # Store estimates (do not change)
    o_x_offline[i] = o_x
    o_y_offline[i] = o_y
    o_z_offline[i] = o_z
    psi_offline[i] = psi
    theta_offline[i] = theta
    phi_offline[i] = phi
    v_x_offline[i] = v_x
    v_y_offline[i] = v_y
    v_z_offline[i] = v_z
```

Compare state estimates from default and custom observers.

```python
def comparison_plot(t, s_custom, s_default, s_offline, name, ax, s_des=None):
    ax.plot(t, s_custom, label=f'{name} (custom observer)', linewidth=2)
    ax.plot(t, s_offline, '--', label=f'{name} (custom observer - offline)', linewidth=3)
    ax.plot(t, s_default, ':', label=f'{name} (default observer)', linewidth=4)
    if s_des is not None:
        ax.plot(t, s_des, '-.', label=f'{name} (desired)', linewidth=2)
    ax.legend()
    ax.grid()

fig, (ax_o_x,
      ax_o_y,
      ax_o_z,
      ax_psi,
      ax_theta,
      ax_phi,
      ax_v_x,
      ax_v_y,
      ax_v_z) = plt.subplots(9, 1, figsize=(15, 25), sharex=True)

comparison_plot(t, o_x_custom, o_x_default, o_x_offline, 'o_x', ax_o_x, s_des=o_x_des)
comparison_plot(t, o_y_custom, o_y_default, o_y_offline, 'o_y', ax_o_y, s_des=o_y_des)
comparison_plot(t, o_z_custom, o_z_default, o_z_offline, 'o_z', ax_o_z, s_des=o_z_des)
comparison_plot(t, psi_custom, psi_default, psi_offline, 'psi', ax_psi)
comparison_plot(t, theta_custom, theta_default, theta_offline, 'theta', ax_theta)
comparison_plot(t, phi_custom, phi_default, phi_offline, 'phi', ax_phi)
comparison_plot(t, v_x_custom, v_x_default, v_x_offline, 'v_x', ax_v_x)
comparison_plot(t, v_y_custom, v_y_default, v_y_offline, 'v_y', ax_v_y)
comparison_plot(t, v_z_custom, v_z_default, v_z_offline, 'v_z', ax_v_z)
ax_v_z.set_xlabel('time (s)')

ax_o_x.set_ylim([-0.2, 0.2])
ax_o_y.set_ylim([-0.2, 0.2])
ax_o_z.set_ylim([-0.05, 0.25])
ax_psi.set_ylim([-0.1, 0.1])
ax_theta.set_ylim([-0.25, 0.25])
ax_phi.set_ylim([-0.25, 0.25])
ax_v_x.set_ylim([-0.6, 0.6])
ax_v_y.set_ylim([-0.6, 0.6])
ax_v_z.set_ylim([-0.5, 0.5])

plt.show()
```

# 3. Flight tests to compare performance at hover

## 3.1 Hover with default controller and observer

**FIXME: REPLACE THIS CELL WITH THE USUAL DESCRIPTION AND VIDEO OF FLIGHT TEST.**

Load and parse flight test data.

```python
# flight test data
data = load_hardware_data('hover_1_data.json', only_in_flight=True)

# times
t = data['time']

# position and orientation
o_x_default = data['stateEstimate.x']
o_y_default = data['stateEstimate.y']
o_z_default = data['stateEstimate.z']
psi_default = np.deg2rad(data['stateEstimate.yaw'])
theta_default = -np.deg2rad(data['stateEstimate.pitch'])
phi_default = np.deg2rad(data['stateEstimate.roll'])

# desired position
o_x_des = data['ctrltarget.x']
o_y_des = data['ctrltarget.y']
o_z_des = data['ctrltarget.z']
```

Plot position, desired position, and orientation.

```python
fig, (ax_pos, ax_ori) = plt.subplots(2, 1, figsize=(15, 8), sharex=True)
px = ax_pos.plot(t, o_x_default, label=f'o_x')
py = ax_pos.plot(t, o_y_default, label=f'o_y')
pz = ax_pos.plot(t, o_z_default, label=f'o_z')
ax_pos.plot(t, o_x_des, '--', label=f'o_x (desired)', color=px[0].get_color())
ax_pos.plot(t, o_y_des, '--', label=f'o_y (desired)', color=py[0].get_color())
ax_pos.plot(t, o_z_des, '--', label=f'o_z (desired)', color=pz[0].get_color())
ax_pos.set_ylim([-0.1, 0.6])
ax_pos.legend()
ax_pos.grid()
ax_ori.plot(t, psi_default, label='psi')
ax_ori.plot(t, theta_default, label='theta')
ax_ori.plot(t, phi_default, label='phi')
ax_ori.set_ylim([-0.1, 0.1])
ax_ori.legend()
ax_ori.grid()
ax_ori.set_xlabel('time (s)')
ax_ori.set_xlim([0, 14])
plt.show()
```

Compute RMSE between states (from default observer) and their desired values **over the entire flight test**.

```python
print(f'RMSE from t={t[0]} to t={t[-1]} (with targets shown at right):')
print(f' o_x:   {rmse(o_x_default, o_x_des):7.3f}  / {0.075:7.3f}')
print(f' o_y:   {rmse(o_y_default, o_y_des):7.3f}  / {0.075:7.3f}')
print(f' o_z:   {rmse(o_z_default, o_z_des):7.3f}  / {0.075:7.3f}')
print(f' psi:   {rmse(psi_default, 0.):7.3f}  / {0.05:7.3f}')
print(f' theta: {rmse(theta_default, 0.):7.3f}  / {0.015:7.3f}')
print(f' phi:   {rmse(phi_default, 0.):7.3f}  / {0.015:7.3f}')
```

## 3.2 Hover with custom controller and default observer

**FIXME: REPLACE THIS CELL WITH THE USUAL DESCRIPTION AND VIDEO OF FLIGHT TEST.**

Load and parse flight test data.

```python
# flight test data
data = load_hardware_data('hover_2_data.json', only_in_flight=True)

# times
t = data['time']

# position and orientation
o_x_default = data['ae483log.o_x']
o_y_default = data['ae483log.o_y']
o_z_default = data['ae483log.o_z']
psi_default = data['ae483log.psi']
theta_default = data['ae483log.theta']
phi_default = data['ae483log.phi']

# desired position
o_x_des = data['ae483log.o_x_des']
o_y_des = data['ae483log.o_y_des']
o_z_des = data['ae483log.o_z_des']
```

Plot position, desired position, and orientation.

```python
fig, (ax_pos, ax_ori) = plt.subplots(2, 1, figsize=(15, 8), sharex=True)
px = ax_pos.plot(t, o_x_default, label=f'o_x')
py = ax_pos.plot(t, o_y_default, label=f'o_y')
pz = ax_pos.plot(t, o_z_default, label=f'o_z')
ax_pos.plot(t, o_x_des, '--', label=f'o_x (desired)', color=px[0].get_color())
ax_pos.plot(t, o_y_des, '--', label=f'o_y (desired)', color=py[0].get_color())
ax_pos.plot(t, o_z_des, '--', label=f'o_z (desired)', color=pz[0].get_color())
ax_pos.set_ylim([-0.1, 0.6])
ax_pos.legend()
ax_pos.grid()
ax_ori.plot(t, psi_default, label='psi')
ax_ori.plot(t, theta_default, label='theta')
ax_ori.plot(t, phi_default, label='phi')
ax_ori.set_ylim([-0.1, 0.1])
ax_ori.legend()
ax_ori.grid()
ax_ori.set_xlabel('time (s)')
ax_ori.set_xlim([0, 14])
plt.show()
```

Compute RMSE between states (from default observer) and their desired values **over the entire flight test**.

```python
print(f'RMSE from t={t[0]} to t={t[-1]} (with targets shown at right):')
print(f' o_x:   {rmse(o_x_default, o_x_des):7.3f}  / {0.075:7.3f}')
print(f' o_y:   {rmse(o_y_default, o_y_des):7.3f}  / {0.075:7.3f}')
print(f' o_z:   {rmse(o_z_default, o_z_des):7.3f}  / {0.075:7.3f}')
print(f' psi:   {rmse(psi_default, 0.):7.3f}  / {0.05:7.3f}')
print(f' theta: {rmse(theta_default, 0.):7.3f}  / {0.015:7.3f}')
print(f' phi:   {rmse(phi_default, 0.):7.3f}  / {0.015:7.3f}')
```

## 3.3 Hover with custom controller and observer

**FIXME: REPLACE THIS CELL WITH THE USUAL DESCRIPTION AND VIDEO OF FLIGHT TEST.**

Load and parse flight test data.

```python
# flight test data
data = load_hardware_data('hover_3_data.json', only_in_flight=True)

# times
t = data['time']

# position and orientation from default observer
o_x_default = data['stateEstimate.x']
o_y_default = data['stateEstimate.y']
o_z_default = data['stateEstimate.z']
psi_default = np.deg2rad(data['stateEstimate.yaw'])
theta_default = -np.deg2rad(data['stateEstimate.pitch'])
phi_default = np.deg2rad(data['stateEstimate.roll'])

# position and orientation from custom observer
o_x_custom = data['ae483log.o_x']
o_y_custom = data['ae483log.o_y']
o_z_custom = data['ae483log.o_z']
psi_custom = data['ae483log.psi']
theta_custom = data['ae483log.theta']
phi_custom = data['ae483log.phi']

# desired position
o_x_des = data['ae483log.o_x_des']
o_y_des = data['ae483log.o_y_des']
o_z_des = data['ae483log.o_z_des']
```

Plot position, desired position, and orientation (both from default and custom observer) **over the entire flight test**.

```python
fig, (ax_pos, ax_ori) = plt.subplots(2, 1, figsize=(15, 8), sharex=True)
px = ax_pos.plot(t, o_x_custom, label=f'o_x (custom observer)')
py = ax_pos.plot(t, o_y_custom, label=f'o_y (custom observer)')
pz = ax_pos.plot(t, o_z_custom, label=f'o_z (custom observer)')
ax_pos.plot(t, o_x_default, ':', label=f'o_x (default observer)', color=px[0].get_color(), linewidth=4)
ax_pos.plot(t, o_y_default, ':', label=f'o_y (default observer)', color=py[0].get_color(), linewidth=4)
ax_pos.plot(t, o_z_default, ':', label=f'o_z (default observer)', color=pz[0].get_color(), linewidth=4)
ax_pos.plot(t, o_x_des, '--', label=f'o_x (desired)', color=px[0].get_color())
ax_pos.plot(t, o_y_des, '--', label=f'o_y (desired)', color=py[0].get_color())
ax_pos.plot(t, o_z_des, '--', label=f'o_z (desired)', color=pz[0].get_color())
ax_pos.set_ylim([-0.1, 0.6])
ax_pos.legend()
ax_pos.grid()
ppsi = ax_ori.plot(t, psi_custom, label=f'psi (custom observer)')
ptheta = ax_ori.plot(t, theta_custom, label=f'theta (custom observer)')
pphi = ax_ori.plot(t, phi_custom, label=f'phi (custom observer)')
ax_ori.plot(t, psi_default, ':', label=f'psi (default observer)', color=ppsi[0].get_color(), linewidth=4)
ax_ori.plot(t, theta_default, ':', label=f'theta (default observer)', color=ptheta[0].get_color(), linewidth=4)
ax_ori.plot(t, phi_default, ':', label=f'phi (default observer)', color=pphi[0].get_color(), linewidth=4)
ax_ori.set_ylim([-0.1, 0.1])
ax_ori.legend()
ax_ori.grid()
ax_ori.set_xlabel('time (s)')
ax_ori.set_xlim([0, 14])
plt.show()
```

Compute RMSE between states (from custom observer) and their desired values.

```python
print(f'RMSE from t={t[0]} to t={t[-1]} (with targets shown at right):')
print(f' o_x:   {rmse(o_x_custom, o_x_des):7.3f}  / {0.075:7.3f}')
print(f' o_y:   {rmse(o_y_custom, o_y_des):7.3f}  / {0.075:7.3f}')
print(f' o_z:   {rmse(o_z_custom, o_z_des):7.3f}  / {0.075:7.3f}')
print(f' psi:   {rmse(psi_custom, 0.):7.3f}  / {0.05:7.3f}')
print(f' theta: {rmse(theta_custom, 0.):7.3f}  / {0.015:7.3f}')
print(f' phi:   {rmse(phi_custom, 0.):7.3f}  / {0.015:7.3f}')
```

## 3.4 Summary and discussion

The following table reports the RMSE in each component of position and of orientation:

|  | $o_x$ | $o_y$ | $o_z$ | $\psi$ | $\theta$ | $\phi$ |
| --: | :--: | :--: | :--: | :--: | :--: | :--: |
| RMSE in hover test (default controller, default observer) | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| RMSE in hover test (custom controller, default observer) | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| RMSE in move test (custom controller, custom observer) | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |

**Modify the text in this cell** to answer the following questions:

* Are you able to match the performance of the default controller and observer (top row) with your controller and observer (bottom row)? Do the results when using the custom controller with the default observer (middle row) help explain any differences in performance?
* What about your observer design (if anything) did you have to change in order to get your custom controller and observer to work well? Why were these changes necessary?

# 4. Flight test while moving in squares

**FIXME: REPLACE THIS CELL WITH THE USUAL DESCRIPTION AND VIDEO OF FLIGHT TEST.**

Load and parse flight test data.

```python
# flight test data
data = load_hardware_data('move_data.json', only_in_flight=True)

# times
t = data['time']

# position and orientation from default observer
o_x_default = data['stateEstimate.x']
o_y_default = data['stateEstimate.y']
o_z_default = data['stateEstimate.z']
psi_default = np.deg2rad(data['stateEstimate.yaw'])
theta_default = -np.deg2rad(data['stateEstimate.pitch'])
phi_default = np.deg2rad(data['stateEstimate.roll'])

# position and orientation from custom observer
o_x_custom = data['ae483log.o_x']
o_y_custom = data['ae483log.o_y']
o_z_custom = data['ae483log.o_z']
psi_custom = data['ae483log.psi']
theta_custom = data['ae483log.theta']
phi_custom = data['ae483log.phi']

# desired position
o_x_des = data['ae483log.o_x_des']
o_y_des = data['ae483log.o_y_des']
o_z_des = data['ae483log.o_z_des']
```

Plot position, desired position, and orientation (both from default and custom observer) **over the entire flight test**.

```python
fig, (ax_pos, ax_ori) = plt.subplots(2, 1, figsize=(15, 8), sharex=True)
px = ax_pos.plot(t, o_x_custom, label=f'o_x (custom observer)')
py = ax_pos.plot(t, o_y_custom, label=f'o_y (custom observer)')
pz = ax_pos.plot(t, o_z_custom, label=f'o_z (custom observer)')
ax_pos.plot(t, o_x_default, ':', label=f'o_x (default observer)', color=px[0].get_color(), linewidth=4)
ax_pos.plot(t, o_y_default, ':', label=f'o_y (default observer)', color=py[0].get_color(), linewidth=4)
ax_pos.plot(t, o_z_default, ':', label=f'o_z (default observer)', color=pz[0].get_color(), linewidth=4)
ax_pos.plot(t, o_x_des, '--', label=f'o_x (desired)', color=px[0].get_color())
ax_pos.plot(t, o_y_des, '--', label=f'o_y (desired)', color=py[0].get_color())
ax_pos.plot(t, o_z_des, '--', label=f'o_z (desired)', color=pz[0].get_color())
ax_pos.legend()
ax_pos.grid()
ppsi = ax_ori.plot(t, psi_custom, label=f'psi (custom observer)')
ptheta = ax_ori.plot(t, theta_custom, label=f'theta (custom observer)')
pphi = ax_ori.plot(t, phi_custom, label=f'phi (custom observer)')
ax_ori.plot(t, psi_default, ':', label=f'psi (default observer)', color=ppsi[0].get_color(), linewidth=4)
ax_ori.plot(t, theta_default, ':', label=f'theta (default observer)', color=ptheta[0].get_color(), linewidth=4)
ax_ori.plot(t, phi_default, ':', label=f'phi (default observer)', color=pphi[0].get_color(), linewidth=4)
ax_ori.legend()
ax_ori.grid()
ax_ori.set_xlabel('time (s)')
plt.show()
```

Plot top-down position and desired position (both from default and custom observer).

```python
plt.figure(figsize=(10, 10))
plt.plot(o_x_custom, o_y_custom, label='position (custom observer)')
plt.plot(o_x_default, o_y_default, ':', label='position (default observer)', linewidth=4)
plt.plot(o_x_des, o_y_des, '--', label='desired position')
plt.axis('equal')
plt.legend()
plt.grid()
plt.show()
```

**FIXME: REPLACE THIS CELL WITH A DISCUSSION OF RESULTS.**

First, fill in the following table:

| | $o_x$ (meters) | $o_y$ (meters) |
| ---: | :---: | :---: |
| landing position (measured by ruler) | 0.000 | 0.000 |
| landing position (from custom observer) | 0.000 | 0.000 |
| landing position (from default observer) | 0.000 | 0.000 |

Then, answer the following questions:

* Last week, you may have noticed that position estimates from the default observer appeared "good" and position estimates from the custom observer appeared "bad". This week, you may have noticed the opposite. What is going on here? Which state estimates are correct? (Are *any* of them correct?) Like last week, you will want to compare the landing position that you measured with a ruler to the final position that was estimated by each observer, and will also want to think about which states are observable (and about what that means).
* To what extent does your code enable the drone to track a desired position? It may help to look at the top-down plot when answering this question. Are there ways you could improve tracking performance?


# Building Chemical Reaction Networks (CRNs) Directly with BioCRNpyler

**Overview:**  This tutorial shows how to use [BioCRNpyler](https://github.com/BuildACell/BioCRNPyler) to represent simple CRNs

## What is a CRN?
A CRN is a widely established model of chemistry and biochemistry.
* A set of species $S$
* A set of reactions $R$ interconvert species $I_r$ to $O_r$

\begin{align}
\\
I \xrightarrow[]{\rho(s)} O
\\
\end{align}

 * $I$ and $O$ are multisets of species $S$.
 * $\rho(s): S \to \mathbb{R}$ is a function that determines how fast the reaction occurs.

```python
#Import everything from biocrnpyler
from biocrnpyler import *
```

## Combining Species and Reactions into a CRN

The following code defines a species called 'S' made out of material 'material'. Species can also have attributes to help identify them. Note that Species with the same name, but different materials or attributes are considered different species in terms of the reactions they participate in.

    S = Species('name', material_type = 'material', attributes = [])

The collowing code produces a reaction R

    R = Reaction(Inputs, Outputs, k)

here Inputs and Outputs must both be a list of Species. the parameter k is the rate constant of the reaction. By default, propensities in BioCRNpyler are massaction:

### $\rho(S) = k \Pi_{s} s^{I_s}$

Note: for stochastic models mass action propensities are $\rho(S) = k \Pi_{s} s!/(s - I_s)!$.

Massaction reactions can be made reversible with the k_rev keyword:

    R_reversible = Reaction(Inputs, Outputs, k, k_rev = krev)

is the same as two reactions:

    R = Reaction(Inputs, Outputs, k)
    Rrev = Reaction(Outputs, Inputs, krev)

Finally, a CRN can be made by combining species and reactions:

    CRN = ChemicalReactionNetwork(species = species, reactions = reactions, initial_condition_dict = {})

Here, initial_condition_dict is an optional dictionary to store the initial values of different species.

    initial_condition_dict = {Species:value}

Species without an initial condition will default to 0.

```python
#Example: Model the CRN consisting of: A --> 2B, 2B <--> B + C where C has the same name as B but a new material
A = Species("A", material_type = "m1", attributes = ["attribute"])
B = Species("B", material_type = "m1")
C = Species("B", material_type = "m2")
D = Species("D")

print("Species can be printed to show their string representation:", A, B, C, D)

#Reaction Rates
k1 = 3.
k2 = 1.4
k2rev = 0.15

#Reaciton Objects
R1 = Reaction.from_massaction([A], [B, B], k_forward = k1)
R2 = Reaction.from_massaction([B], [C, D], k_forward = k2)

print("\nReactions can be printed as well:\n", R1,"\n", R2)

#create an initial condition so A has a non-zero value
initial_concentration_dict = {A:10}

#Make a CRN
CRN = ChemicalReactionNetwork(species = [A, B, C, D], reactions = [R1, R2], initial_concentration_dict = initial_concentration_dict)

#CRNs can be printed in two different ways
print("\nDirectly printing a CRN shows the string representation of the species used in BioCRNpyler:")
print(CRN)

print("\nCRN.pretty_print(...) is a function that prints a more customizable version of the CRN, but doesn't show the proper string representation of species.")
print(CRN.pretty_print(show_materials = True, show_rates = True, show_attributes = True))
```

    Species can be printed to show their string representation: m1_A_attribute m1_B m2_B D

    Reactions can be printed as well:
     m1[A(attribute)] --> 2m1[B]
     m1[B] --> m2[B]+D

    Directly printing a CRN shows the string representation of the species used in BioCRNpyler:
    Species = m1_A_attribute, m1_B, m2_B, D
    Reactions = [
    	m1[A(attribute)] --> 2m1[B]
    	m1[B] --> m2[B]+D
    ]

    CRN.pretty_print(...) is a function that prints a more customizable version of the CRN, but doesn't show the proper string representation of species.
    Species (4) = {0. m1[A(attribute)] init_conc = 0, 1. m1[B] init_conc = 0, 2. m2[B] init_conc = 0, 3. D init_conc = 0}

    Reactions (2) = [
    0. m1[A(attribute)] --> 2m1[B]
     Kf=k_forward * m1_A_attribute
      k_forward=3.0

    1. m1[B] --> m2[B]+D
     Kf=k_forward * m1_B
      k_forward=1.4

    ]

## CRNs can be saved as SBML and simulated

To save a CRN as SBML:

    CRN.write_sbml_file("file_name.xml")

To simulate a CRN with biosrape:

    Results, Model = CRN_expression.simulate_with_bioscrape(timepoints, initial_condition_dict = x0)

Where x0 is a dictionary: x0 = {species_name:initial_value}

```python
#Saving and simulating a CRN
CRN.write_sbml_file("build_crns_directly.xml")

try:
    import bioscrape
    import numpy as np
    import pylab as plt
    import pandas as pd

    #Initial conditions can be set with a dictionary:
    x0 = {A:120}
    timepoints = np.linspace(0, 1, 100)#Timepoints to simulate over

    #This function can also take a filename keyword to save the file at the same time
    R = CRN.simulate_with_bioscrape_via_sbml(timepoints = timepoints, initial_condition_dict = x0)

    #Check to ensure simulation worked
    #Results are in a Pandas Dictionary and can be accessed via string-names of species
    plt.plot(R['time'], R[str(A)], label = "A")
    plt.plot(R['time'], R[str(B)], label = "B")
    plt.plot(R['time'], R[str(C)], "--", label = "C")
    plt.plot(R['time'], R[str(D)],":", label = "D")
    plt.legend()

except ModuleNotFoundError:
    print("Plotting Modules not installed.")
```

## ComplexSpecies and OrderedComplexSpecies

When Species bind together to form complexes, it is recommended to use the Complex function:

    Complex([list of species], ordered = True/False)

This function returns the classes ComplexSpecies or OrderedComplexSpecies subclasses which contain information about the species inside of them. ComplexSpecies treats its internal species as an unordered multiset. OrderedComplexSpecies treats its internal species as an ordered list. It is recommended to always use the function Complex to create these types of Species for compatability reasons discussed in the OrderedPolymerSpecies example notebook.

_Note: These objects do not automatically generate binding reactions. To do that, use the Component wrappers ChemicalComplex and OrderedChemicalComplex._

```python
A = Species("A", material_type = "m1")
B = Species("B", material_type = "m1")
B2 = Species("B", material_type = "m2")

print("\nFor ComplexSpecies, the order of the elements does not matter:")
C1 = Complex([A, B, B2, A])
C2 = Complex([B, A, B2, A])
print("C1=ComplexSpecies([A, B, B2, A])=", C1)
print("C2=ComplexSpecies([B, A, B2, A])=", C2)
print("C1==C2 ==>", C1==C2)

print("\nFor OrderedComplexSpecies, the Order Does Matter:")
C3 = Complex([A, B, B2, A], ordered = True)
C4 = Complex([B, A, B2, A], ordered = True)
print("C3=OrderedComplexSpecies([A, B, B2, A])=", C3)
print("C4=OrderedComplexSpecies([B, A, B2, A])=", C4)
print("C3==C4 ==>", C3==C4)

print("\nComplexSpecies (and the OrderedComplexSpecies and Multimers) are Species and can be used in reactions:")
R = Reaction.from_massaction([A, B, B2, A], [C1], k_forward=10)
print("Reaction.from_massaction([A, B, B2, A], [C1], k_forward = 10)=")
print(R)
```

    For ComplexSpecies, the order of the elements does not matter:
    C1=ComplexSpecies([A, B, B2, A])= complex_m1_A_2x_m1_B_m2_B_
    C2=ComplexSpecies([B, A, B2, A])= complex_m1_A_2x_m1_B_m2_B_
    C1==C2 ==> True

    For OrderedComplexSpecies, the Order Does Matter:
    C3=OrderedComplexSpecies([A, B, B2, A])= ordered_complex_m1_A_m1_B_m2_B_m1_A_
    C4=OrderedComplexSpecies([B, A, B2, A])= ordered_complex_m1_B_m1_A_m2_B_m1_A_
    C3==C4 ==> False

    ComplexSpecies (and the OrderedComplexSpecies and Multimers) are Species and can be used in reactions:
    Reaction.from_massaction([A, B, B2, A], [C1], k_forward = 10)=
    2m1[A]+m1[B]+m2[B] --> complex[2x_m1[A]:m1[B]:m2[B]]

# Non-massaction propensities in BioCRNpyler
By default, BioCRNpyler assumes that propensities are massaction with only one parameter, the rate constant $k_{forward}$ (and optional reverse rate $k_{reverse}$. However, additional propensity types are also supported. These reactions are created in two steps: first a Propensity of the appropriate type is made, then Reaction is made using that Propensity. These reactions are always created irreversibly. Examples are shown below:

## HillPositive:
$\rho(s) = k \frac{s_1^n}{K^n+s_1^n}$

Requried parameters: rate constant "k", offset "K", hill coefficient "n", hill species "s1".

```python
#create the propensity
R = Species("R")
hill_pos  = HillPositive(k=1, s1=R, K=5, n=2)

#create the reaction
r_hill_pos = Reaction([A], [B], propensity_type = hill_pos)

#print the reaction
print(r_hill_pos.pretty_print())
```

    m1[A] --> m1[B]
     Kf = k R^n / ( K^n + R^n )
      k=1
      K=5
      n=2

## HillNegative:
$\rho(s) = k \frac{1}{K^n+s_1^n}$

Requried parameters: rate constant "k", offset "K", hill coefficient "n", hill species "s1".

```python
#create the propensity
R = Species("R")
hill_neg  = HillPositive(k=1, s1=R, K=5, n=2)

#create the reaction
r_hill_neg = Reaction([A], [B], propensity_type = hill_neg)

#print the reaction
print(r_hill_neg.pretty_print())
```

    m1[A] --> m1[B]
     Kf = k R^n / ( K^n + R^n )
      k=1
      K=5
      n=2

## ProportionalHillPositive:
$\rho(s, d) = k d \frac{s_1^n}{K^n + s_1^n}$

Requried parameters: rate constant "k", offset "K", hill coefficient "n", hill species "s1", proportional species "d"

```python
#create the propensity
R = Species("R")
D = Species("D")
prop_hill_pos  = ProportionalHillPositive(k=1, s1=R, K=5, n=2, d = D)

#create the reaction
r_prop_hill_pos = Reaction([A], [B], propensity_type = prop_hill_pos)

#print the reaction
print(r_prop_hill_pos.pretty_print())
```

    m1[A] --> m1[B]
     Kf = k D R^n / ( K^n + R^n )
      k=1
      K=5
      n=2

## ProportionalHillNegative:
$\rho(s, d) = k d \frac{1}{K^n + s_1^n}$

Requried parameters: rate constant "k", offset "K", hill coefficient "n", hill species "s1", proportional species "d"

```python
#create the propensity
R = Species("R")
D = Species("D")
prop_hill_neg  = ProportionalHillNegative(k=1, s1=R, K=5, n=2, d = D)

#create the reaction
r_prop_hill_neg = Reaction([A], [B], propensity_type = prop_hill_neg)

#print the reaction
print(r_prop_hill_neg.pretty_print())
```

    m1[A] --> m1[B]
     Kf = k D / ( K^n + R^2 )
      k=1
      K=5
      n=2

## General Propensity:
$\rho(s) = $ function of your choice

For general propensities, the function must be written out as a string with all species and parameters declared.

```python
#create species
# create some parameters - note that parameters will be discussed in the next lecture
k1 = ParameterEntry("k1", 1.11)
k2 = ParameterEntry("k2", 2.22)
S = Species("S")

#type the string as a rate then declare teh species and parameters
general = GeneralPropensity(f'k1*2 - k2/{S}^2', propensity_species=[S], propensity_parameters=[k1, k2])

r_general = Reaction([A, B], [C1], propensity_type = general)

print(r_general.pretty_print())
```

    m1[A]+m1[B] --> complex[2x_m1[A]:m1[B]:m2[B]]
    k1*2 - k2/S^2
      k1=1.11
      k2=2.22

```python

```

```python

```

examples/1. Building CRNs Directly.ipynb


```python
# 그래프, 수학 기능 추가
# Add graph and math features
import pylab as py
import numpy as np
import numpy.linalg as nl
# 기호 연산 기능 추가
# Add symbolic operation capability
import sympy as sy

```

# 가우스 소거법 : 선형 연립방정식의 풀이
Gauss Elimiation : Solving a System of Linear Equations

화살표를 그리는 함수
Function to draw an arrow

```python
def draw_2dvec(x, y, x0=0, y0=0, color='k', name=None):
    py.quiver(x0, y0, x, y, color=color, angles='xy', scale_units='xy', scale=1)
    if name is not None:
        if not name.startswith('$'):
            vec_str = '$\\vec{%s}$' % name
        else:
            vec_str = name
        py.text(0.5 * x + x0, 0.5 * y + y0, vec_str)

```

정삼각형을 그리는 함수
Function to draw an equilateral triangle

```python
def triangle_support(x, y, length):
    # https://matplotlib.org/gallery/lines_bars_and_markers/fill.html
    height = py.cos(py.radians(30)) * length
    py.fill((x, x + length*0.5, x + length*-0.5), (y, y - height, y - height))

```

## 5절점 트러스 예제
Example of a Five Node Truss

다음과 같은 트러스를 생각해 보자.

Let's think about a truss as follows.

(ref: "[Application of system of linear equations](https://www.chegg.com/homework-help/questions-and-answers/application-system-linear-equations-sure-work-matlab-problem-figure-1-shows-mechanical-str-q22676917)", Chegg Study)

```python
# 마디점 좌표 nodal point coordinates
xy_list = [(0, 0), (1, 3**0.5), (2, 0), (3, 3**0.5), (4, 0)]

# 각 부재의 양 끝 점 end points of each member
connectivity_list = [(0, 1), (0, 2), (1, 2), (1, 3), (2, 3), (2, 4), (3, 4)]

for k, i_j in enumerate(connectivity_list):
    i, j = i_j

    py.plot(
        (xy_list[i][0], xy_list[j][0]),
        (xy_list[i][1], xy_list[j][1]),
        '.-'
    )

    # 부재 번호 표시 Indicate member id
    py.text(0.5 * (xy_list[i][0] + xy_list[j][0]),
            0.5 * (xy_list[i][1] + xy_list[j][1]), k + 1)

# 마디점 번호 표시 Indicate node ids
for k, xy in enumerate(xy_list):
    py.text(xy[0], xy[1], '(%d)' % (k+1))

draw_2dvec(0, -0.5, xy_list[1][0], xy_list[1][1], name='$F_2$')
draw_2dvec(0, -0.5, xy_list[3][0], xy_list[3][1], name='$F_4$')

triangle_support(xy_list[0][0], xy_list[0][1], 0.25)
triangle_support(xy_list[4][0], xy_list[4][1], 0.25)

py.axis('equal')
py.xlim((-2, 6))
py.ylim((-1, 2))
# https://stackoverflow.com/questions/9295026/matplotlib-plots-removing-axis-legends-and-white-spaces
py.axis('off')

py.savefig('trapezoid_truss.svg')

```

모든 부재의 길이는 2m 이다.

All members have length of 2m.

$F_2$ 과 $F_4$는 각각 $400N$ 과 $800N$ 이다.

$F_2$ and $F_4$ are $400N$ and $800N$ respectively.

```python
F_2_N = 400
F_4_N = 800

```

우선, 전체 반력을 계산한다.
First of all, calculate the reaction force.

$$
\left\{
    \begin{align}
        R_{1x}+\sum F_x = 0 \\
        R_{1y}+R_{5y}+\sum F_y = 0 \\
        x_5 \cdot R_{5y}+\sum x \cdot F_y = 0 \\
    \end{align}
\right.
$$

알고 있는 상수를 오른쪽으로 옮기도록 하자.
Let's move known constants to the right side.

$$
\left\{
    \begin{align}
        R_{1x} &= 0 \\
        R_{1y}+R_{5y} &= - F_{2y} - F_{4y} \\
        x_5 \cdot R_{5y} &= - x_2 \cdot F_{2y} - x_5 \cdot F_{5y} \\
    \end{align}
\right.
$$

### 후진대입 예
An example of back substitution

#### 첫번째 해
First solution

$y$ 방향 반력에 관한 식만 살펴 보자.
Let's take a look at the equations about reaction forces in $y$ direction.

$$
\left\{
    \begin{align}
        1 \cdot R_{1y}+1 \cdot R_{5y} &= - F_{2y} - F_{4y} \\
        0 \cdot R_{1y}+x_5 \cdot R_{5y} &= - x_2 \cdot F_{2y} - x_5 \cdot F_{5y} \\
    \end{align}
\right.
$$

아래 방정식에서는 하나의 미지수는 계수가 0이고 다른 하나는 0이 아니다.

In the lower equation, one unknown has coefficient 0 and the other non-zero.

여기서 계수가 0이 아닌 미지수는 해당 계수로 우변을 나누면 쉽게 알 수 있다.
Here, we can easily find the unknown with non-zero coefficient by dividing the right side with the coefficient.

$$
R_{5y} = \frac{1}{x_5} \left( - x_2 \cdot F_{2y} - x_5 \cdot F_{5y}  \right)
$$

이제 이 반력은 알려진 상수 이다.
Now this reaction force is a known constant.

다른 미지수를 구하기 위해 이를 이용할 수 있다.
We can use this to find other unknown(s).

#### 두번째 해
Second solution

첫번째 식을 살펴 보자. $R_{5y}$는 알고 있는 값이므로 오른쪽으로 옮긴다.
Let's look at the first equation. Because we know $R_{5y}$, let's move to the right side.

$$
1 \cdot R_{1y} = - 1 \cdot R_{5y} - F_{2y} - F_{4y}
$$

$$
R_{1y} =\frac{1}{1} \left(- 1 \cdot R_{5y} - F_{2y} - F_{4y}\right)
$$

이렇게 해서 모든 반력을 구할 수 있다.
This way, we can find all the reaction forces.

이렇게 먼저 알아낸 미지수 값을 이용하여 다른 미지수 값을 찾는 것을 **후진대입법**이라고 한다.
 **Back subsitution** uses a solution found in advance to find other solutions.

Gauss 소거법은 이 후진대입법을 적용하기 좋은 형태로 계수 행렬을 변형시킨다.

Gauss elimination would make the coefficient matrix suitable for the back substitution.

### 각 절점에서의 평형 방정식을 행렬 형태로 정리
Organizing equations of equilibrium at each node into a matrix form

트러스의 부재 사이의 각은 모두 60도이다.
The angles between the members are 60 degrees.

$$
\alpha=sin\left( \frac{\pi}{6} \right)=cos\left( \frac{\pi}{3} \right) \\
\beta=sin\left( \frac{\pi}{3} \right)=cos\left( \frac{\pi}{6} \right)
$$

#### (1) 번 마디
Node (1)

$x$ 방향 평형

$x$ directional equilibrium.

$$
R_{1x} + \alpha f_1 + f_2 =0
$$

여기서 $f_i$는 부재 $i$ 번의 장력이다.
Here $f_i$ indicates the tensile force of $i$th member.

$y$ 방향 평형

$y$ directional equilibrium.

$$
R_{1y} + \beta f_1 =0
$$

행렬 형태로 한번 정리해 보자.
Let's organize in a matrix form.

$$
\begin{bmatrix}
    1 & 0 & \alpha & 1 \\
    0 & 1 &  \beta & 0 \\
\end{bmatrix}
\begin{pmatrix}
    R_{1x} \\ R_{1y} \\ f_1 \\ f_2
\end{pmatrix}
=
\begin{pmatrix}
    0 \\ 0 \\ 0 \\ 0
\end{pmatrix}
$$

#### (2) 번 마디
Node (2)

(2)번 마디 $x$ 방향

$x$ direction at node (2).

$$
-\alpha f_1 + \alpha f_3 + f_{4}=0
$$

(2)번 마디 $y$ 방향

$y$ direction at node (2).

$$
-\beta f_1 - F_{2} -\beta f_3=0 \\
-\beta f_1 -\beta f_3=F_{2}
$$

$$
\begin{bmatrix}
    -\alpha & \alpha & 1 \\
     -\beta & -\beta & 0 \\
\end{bmatrix}
\begin{pmatrix}
    f_1 \\ f_3 \\ f_4
\end{pmatrix}
=
\begin{pmatrix}
    0 \\ F_2
\end{pmatrix}
$$

#### (3) 번 마디
Node (3)

$$
-f_2 -\alpha f_3 + \alpha f_5 + f_6 = 0 \\
\beta f_3 + \beta f_5 = 0
$$

$$
\begin{bmatrix}
   -1 & -\alpha & \alpha & 1 \\
    0 &   \beta &  \beta & 0 \\
\end{bmatrix}
\begin{pmatrix}
    f_2 \\ f_3 \\ f_5 \\ f_6
\end{pmatrix}
=
\begin{pmatrix}
    0 \\ 0
\end{pmatrix}
$$

#### (4) 번 마디
Node (4)

$$
-f_4 - \alpha f_5 + \alpha f_7 = 0 \\
- \beta f_5 - \beta f_7 = F_4 \\
$$

$$
\begin{bmatrix}
   -1 & -\alpha & \alpha \\
    0 &  -\beta & -\beta \\
\end{bmatrix}
\begin{pmatrix}
    f_4 \\ f_5 \\ f_7
\end{pmatrix}
=
\begin{pmatrix}
    0 \\ F_4
\end{pmatrix}
$$

#### (5) 번 마디
Node (5)

$$
-f_6 -\alpha f_7 = 0 \\
\beta f_7 + R_{5y} = 0
$$

$$
\begin{bmatrix}
   -1 & -\alpha & 0 \\
    0 &   \beta & 1 \\
\end{bmatrix}
\begin{pmatrix}
    f_6 \\ f_7 \\ R_{5y}
\end{pmatrix}
=
\begin{pmatrix}
    0 \\ 0
\end{pmatrix}
$$

행렬형태로는:

In matrix form:

$$
\begin{bmatrix}
1 & 0 &  \alpha &  1 &       0 &  0 &       0 &  0 &       0 & 0\\
0 & 1 &   \beta &  0 &       0 &  0 &       0 &  0 &       0 & 0\\
0 & 0 & -\alpha &  0 &  \alpha &  1 &       0 &  0 &       0 & 0\\
0 & 0 &  -\beta &  0 &  -\beta &  0 &       0 &  0 &       0 & 0\\
0 & 0 &       0 & -1 & -\alpha &  0 &  \alpha &  1 &       0 & 0\\
0 & 0 &       0 &  0 &   \beta &  0 &   \beta &  0 &       0 & 0\\
0 & 0 &       0 &  0 &       0 & -1 & -\alpha &  0 &  \alpha & 0\\
0 & 0 &       0 &  0 &       0 &  0 &  -\beta &  0 &  -\beta & 0\\
0 & 0 &       0 &  0 &       0 &  0 &       0 & -1 & -\alpha & 0\\
0 & 0 &       0 &  0 &       0 &  0 &       0 &  0 &   \beta & 1\\
\end{bmatrix}
\begin{pmatrix}
R_{1x} \\ R_{1y} \\ f_1 \\ f_2 \\ f_3 \\ f_4 \\ f_5 \\ f_6 \\ f_7 \\ R_{5y}
\end{pmatrix}
=
\begin{pmatrix}
0 \\ 0 \\ 0 \\ F_2 \\ 0 \\ 0 \\ 0 \\ F_4 \\ 0 \\ 0
\end{pmatrix}
$$

주대각선 주위에 0이 아닌 요소들이 모여 있음을 알 수 있다.

We can see that none-zero elements are close to the main diagonal.

행렬에 변수 값을 넣어 준비해 보자.

Let's prepare a matrix with numerical values.

```python
alpha = py.sin(py.radians(30))
beta = py.sin(py.radians(60))

```

```python
matrix = np.array([
    [1, 0,  alpha,  1,      0,  0,      0,  0,      0, 0,],
    [0, 1,   beta,  0,      0,  0,      0,  0,      0, 0,],
    [0, 0, -alpha,  0,  alpha,  1,      0,  0,      0, 0,],
    [0, 0,  -beta,  0,  -beta,  0,      0,  0,      0, 0,],
    [0, 0,      0, -1, -alpha,  0,  alpha,  1,      0, 0,],
    [0, 0,      0,  0,   beta,  0,   beta,  0,      0, 0,],
    [0, 0,      0,  0,      0, -1, -alpha,  0,  alpha, 0,],
    [0, 0,      0,  0,      0,  0,  -beta,  0,  -beta, 0,],
    [0, 0,      0,  0,      0,  0,      0, -1, -alpha, 0,],
    [0, 0,      0,  0,      0,  0,      0,  0,   beta, 1,],
])

```

행렬의 계수를 계산해 보자.
Let's check the rank of the matrix.

```python
nl.matrix_rank(matrix)

```

미지수의 갯수와 정방행렬의 계수가 같다는 것은 이 선형 연립 방정식의 해를 구할 수 있다는 뜻이다.

The number of unknowns and the rank of the matrix are the same; we can find a root of this system of linear equations.

우변을 준비해 보자.

Let's prepare for the right side.

```python
vector = np.array([[0, 0, 0, F_2_N, 0, 0, 0, F_4_N, 0, 0,]]).T

```

파이썬의 확장 기능 가운데 하나인 NumPy 의 선형 대수 기능 `solve()` 를 사용하여 해를 구해 보자.

Using `solve()` of linear algebra subpackage of `NumPy`, a Python package, let's find a solution.

```python
x = nl.solve(matrix, vector)
x

```

이렇게 찾은 `x`가 원래의 연립방정식을 만족하는지 확인해보자.

Let's check whether `x` found this way satisfies the original system of equations.

```python
import numpy.testing as nt

nt.assert_array_almost_equal(matrix @ x, vector)

```

## Final Bell
마지막 종

```python
# stackoverfow.com/a/24634221
import os
os.system("printf '\a'");

```

```python

```


# Curso Macroeconomia
# Prof. Cássio Besarria

```python
# Importando o numpy e nomeando-o

import numpy as np
    #importando o numpy (junto com todos seus módulos) e o nomenando como np
    #os módulos em default são: random, fft, lib, linalg, testing e core

# Importanto todos os objetos públicos do numpy dentro do namespace corrente

from numpy import *

# Importanto funções do numpy de módulos especificios

from numpy.random import rand, randn
    #importando as funções rand e randn do módulo random do numpy
    # rand = retorna números aleatórios dentro dentro de um formato
    # randn = retorna uma matriz com dados com base em uma distribuição normal padronizada
from numpy.fft import fft, ifft
    #importando as funções fft e ifft do módulo fft do numpy
    # fft = calcula a unidimensional transformação discreta de Fourier
    # ifft = calcula a unidimensional transformação discreta inversa de Fourier

# Demais importações

from scipy.stats import beta
    #Importanto a função beta do módulo stats do scipy
    # beta = Uma variável aleatória contínua beta

import matplotlib.pyplot as plt
    #importando o módulo(sub-pacote) pyplot do matplotlib e o nomenando como plt

```

```python
from sympy import Symbol, solve
    #Importa Symbol representa a possibilidade de operações com símbolos
    #Importa solve permite a solução de equações
```

```python
from pylab import plot, arange, legend
    #Importa a função de elaboração de gráficos da biblioteca pylab
    #Função plot para gerar gráficos
    #Função arange para definir as faixas de valores, o numpy possui essa função
    #Inclui legenda
```

```python
x = np.arange(0,4,0.5)
    #Usando a função arange do numpy
    #x fica no intervalo entre 0 e 4 (domínio) e vai variar de 0.5 entre os pontos
```

```python
#Plot 1: usando pylab e matplotlib

y = 1/x
plot(x,y, 'xg:', label='Demanda')
plt.axhline(ls='--', c='k')
    #Inclui a linha do eixo 'x'
legend(loc='best')
plt.savefig('demanda_lab')
    #Escolhe a melhor localização pra legenda
```

```python
#Plot 3: usando matplotlib

plt.plot(x,y,'rx--')
#plt.title('Soluções da Eq_1')
plt.ylabel('Preço')
# Taxa de juros (i)
plt.xlabel('Demanda')
# Moeda (M)
#plt.legend(['Eq_1'])
plt.annotate('Inclinação', xy=(2, 4.6), xytext=(1, 1.5),
            arrowprops=dict(facecolor='black', shrink=0.05),
            )
plt.axhline(ls='--', c='k')
plt.savefig('moeda_lab')
plt.show()
```

# Demanda por moeda

```python
x = np.arange(1,15,0.1)
v = np.arange(4,15,0.1)
    #Usando a função arange do numpy
    #x fica no intervalo entre 0 e 4 (domínio) e vai variar de 0.5 entre os pontos
```

```python
#Plot 1: usando pylab e matplotlib
y = 4/x
z = 16/v
plt.plot(v,z, color='blue', linewidth=2.5, linestyle="--", label='$M^d_1$')
plot(x,y, 'r--', label='$M^d_0$')
#plt.ylabel('Demanda (Z), Produção (Y)')
plt.ylabel('Taxa de juros (i)')
plt.xlabel('Demanda por moeda (M)')

plt.annotate(r'$Y_1>Y_0$',
            xy=(6.4, 2.5), xycoords='data',
            xytext=(20, 70), textcoords='offset points', fontsize=10,
            arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2"))

plt.annotate("",
            xy=(1.6, 2.5), xycoords='data',
            xytext=(6.4, 2.5), textcoords='data',
            arrowprops=dict(arrowstyle="<-"))

plt.axhline(ls='--', c='k')
# Inclui a linha do eixo 'x'
legend(loc='best')
plt.savefig('demanda_moeda')
```

# Equilíbrio no mercado monetário

```python
x = np.arange(0,4,0.5)
v = np.arange(0,4,0.5)
w = np.arange(0,4,0.5)
    #Usando a função arange do numpy
    #x fica no intervalo entre 0 e 4 (domínio) e vai variar de 0.5 entre os pontos
```

```python
#Plot 1: usando pylab e matplotlib

y = 1/x
plot(x,y, 'xg:', label='Eq_1')
plt.axhline(ls='--', c='k')
    #Inclui a linha do eixo 'x'
legend(loc='best')
    #Escolhe a melhor localização pra legenda
```

```python
#Plot 3: usando matplotlib

z = 2 + 0*x

plt.plot(x,y,'rx--', label='$M^d$')
plot(z,v, 'blue', label='$M^s$')
#plt.title('Soluções da Eq_1')
plt.ylabel('Taxa de juros (i)')
plt.xlabel('Moeda (M)')
#plt.legend(['Eq_1'])
plt.axhline(ls='--', c='k')
legend(loc='best')
plt.savefig('equilibrio_moeda')
plt.show()
```

# Deslocamento da demanda

```python
#Plot 3: usando matplotlib

z = 2 + 0*x
d = 2.5/w

plt.plot(x,y,'rx--', label='$M^d$')   # Demanda 01
plt.plot(w,d,'green', label='$M^d$')  # Demanda 02
plot(z,v, 'blue', label='$M^s$')      # Oferta
#plt.title('Soluções da Eq_1')

plt.annotate(r'$Y_1>Y_0$',
            xy=(2, 1.25), xycoords='data',
            xytext=(20, 70), textcoords='offset points', fontsize=10,
            arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2"))

plt.annotate("",
            xy=(0.8, 1.25), xycoords='data',
            xytext=(2, 1.25), textcoords='data',
            arrowprops=dict(arrowstyle="<-"))

plt.ylabel('Taxa de juros (i)')
plt.xlabel('Moeda (M)')
#plt.legend(['Eq_1'])
plt.axhline(ls='--', c='k')
legend(loc='best')
plt.savefig('equilibrio_moeda_renda')
plt.show()
```

```python

```


> **Copyright (c) 2021 CertifAI Sdn. Bhd.**

This program is part of OSRFramework. You can redistribute it and/or modify

it under the terms of the GNU Affero General Public License as published by

the Free Software Foundation, either version 3 of the License, or

(at your option) any later version.

This program is distributed in the hope that it will be useful

but WITHOUT ANY WARRANTY; without even the implied warranty of

MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the

GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License

along with this program.  If not, see <http://www.gnu.org/licenses/>.

# Introduction

In this notebook, we are going to train a sequence-to-sequence model that uses the [nn.Transformer](https://pytorch.org/docs/stable/generated/torch.nn.Transformer.html) module on a language modeling task.

# What will we accomplish?

Steps to implement topic modelling using PyTorch Transformer:

> Step 1: Understand and implement Transformer Architecture

> Step 2: Load and batch data

> Step 3: Train the transformer model

> Step 4: Evaluate the trained model

# Notebook Content

* [Language Modelling with NN.TRANSFORMER and TORCHTEXT](#Language-Modelling-with-NN.TRANSFORMER-and-TORCHTEXT)

* [Define the Model](#Define-the-Model)

* [Load and Batch Data](#Load-and-Batch-Data)

* [Generate Input and Target Sequence](#Generate-Input-and-Target-Sequence)

* [Initiate an Instance](#Initiate-an-Instance)

* [Run the Model](#Run-the-Model)

* [Evaluate the Best Model on the Test Dataset](#Evaluate-the-Best-Model-on-the-Test-Dataset)

# Language Modelling with NN.TRANSFORMER and TORCHTEXT

The PyTorch 1.2 release includes a standard transformer module based on the paper [Attention is All You Need](https://arxiv.org/pdf/1706.03762.pdf).

Compared to **Recurrent Neural Networks (RNNs)**, the transformer model has proven to be superior in quality for many sequence-to-sequence tasks while being more parallelizable. The `nn.Transformer` module relies entirely on an attention mechanism (implemented as [nn.MultiheadAttention](https://pytorch.org/docs/stable/generated/torch.nn.MultiheadAttention.html) to draw global dependencies between input and output. The `nn.Transformer` module is highly modularized such that a single component (e.g., [nn.TransformerEncoder](https://pytorch.org/docs/stable/generated/torch.nn.TransformerEncoder.html)) can be easily adapted/composed.

## Define the Model

In this notebook, we train a `nn.TransformerEncoder` model on a **language modeling task**. The language modeling task is to assign a **probability** for the likelihood of a given word (or a sequence of words) to follow a sequence of words. A sequence of tokens are passed to the **embedding layer** first, followed by a **positional encoding layer** to account for the order of the word. The `nn.TransformerEncoder` consists of multiple layers of [nn.TransformerEncoderLayer](https://pytorch.org/docs/stable/generated/torch.nn.TransformerEncoderLayer.html).

Along with the input sequence, a **square attention mask** is required because the **self-attention layers** in `nn.TransformerEncoder` are only allowed to attend the earlier positions in the sequence. For the language modeling task, any tokens on the future positions should be masked. To produce a probability distribution over output words, the output of the `nn.TransformerEncoder` model is passed through a **linear layer** followed by a **log-softmax function**.

```python
import math
from typing import Tuple

import torch
from torch import nn, Tensor
import torch.nn.functional as F
from torch.nn import TransformerEncoder, TransformerEncoderLayer
from torch.utils.data import dataset

class TransformerModel(nn.Module):

    def __init__(self, ntoken: int, d_model: int, nhead: int, d_hid: int,
                 nlayers: int, dropout: float = 0.5):
        super().__init__()
        self.model_type = 'Transformer'
        self.pos_encoder = PositionalEncoding(d_model, dropout)
        encoder_layers = TransformerEncoderLayer(d_model, nhead, d_hid, dropout)
        self.transformer_encoder = TransformerEncoder(encoder_layers, nlayers)
        self.encoder = nn.Embedding(ntoken, d_model)
        self.d_model = d_model
        self.decoder = nn.Linear(d_model, ntoken)

        self.init_weights()

    def init_weights(self) -> None:
        initrange = 0.1
        self.encoder.weight.data.uniform_(-initrange, initrange)
        self.decoder.bias.data.zero_()
        self.decoder.weight.data.uniform_(-initrange, initrange)

    def forward(self, src: Tensor, src_mask: Tensor) -> Tensor:
        """
        Args:
            src: Tensor, shape [seq_len, batch_size]
            src_mask: Tensor, shape [seq_len, seq_len]

        Returns:
            output Tensor of shape [seq_len, batch_size, ntoken]
        """
        src = self.encoder(src) * math.sqrt(self.d_model)
        src = self.pos_encoder(src)
        output = self.transformer_encoder(src, src_mask)
        output = self.decoder(output)
        return output

def generate_square_subsequent_mask(sz: int) -> Tensor:
    """Generates an upper-triangular matrix of -inf, with zeros on diag."""
    return torch.triu(torch.ones(sz, sz) * float('-inf'), diagonal=1)
```

``PositionalEncoding`` module injects some information about the **relative or absolute position** of the tokens in the sequence. The positional encodings have the same dimension as the embeddings so that the two can be summed. Here, we use ``sine`` and ``cosine`` functions of different frequencies.

```python
class PositionalEncoding(nn.Module):

    def __init__(self, d_model: int, dropout: float = 0.1, max_len: int = 5000):
        super().__init__()
        self.dropout = nn.Dropout(p=dropout)

        position = torch.arange(max_len).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2) * (-math.log(10000.0) / d_model))
        pe = torch.zeros(max_len, 1, d_model)
        pe[:, 0, 0::2] = torch.sin(position * div_term)
        pe[:, 0, 1::2] = torch.cos(position * div_term)
        self.register_buffer('pe', pe)

    def forward(self, x: Tensor) -> Tensor:
        """
        Args:
            x: Tensor, shape [seq_len, batch_size, embedding_dim]
        """
        x = x + self.pe[:x.size(0)]
        return self.dropout(x)
```

## Load and Batch Data

This tutorial uses ``torchtext`` to generate Wikitext-2 dataset. The vocab object is built based on the train dataset and is used to numericalize tokens into tensors. Wikitext-2 represents rare tokens as `<unk>`.

Given a 1-D vector of sequential data, ``batchify()`` arranges the data into ``batch_size`` columns. If the data does not divide evenly into ``batch_size`` columns, then the data is trimmed to fit. For instance, with the alphabet as the data (total length of 26) and ``batch_size=4``, we would divide the alphabet into 4 sequences of length 6:

\begin{align}\begin{bmatrix}
  \text{A} & \text{B} & \text{C} & \ldots & \text{X} & \text{Y} & \text{Z}
  \end{bmatrix}
  \Rightarrow
  \begin{bmatrix}
  \begin{bmatrix}\text{A} \\ \text{B} \\ \text{C} \\ \text{D} \\ \text{E} \\ \text{F}\end{bmatrix} &
  \begin{bmatrix}\text{G} \\ \text{H} \\ \text{I} \\ \text{J} \\ \text{K} \\ \text{L}\end{bmatrix} &
  \begin{bmatrix}\text{M} \\ \text{N} \\ \text{O} \\ \text{P} \\ \text{Q} \\ \text{R}\end{bmatrix} &
  \begin{bmatrix}\text{S} \\ \text{T} \\ \text{U} \\ \text{V} \\ \text{W} \\ \text{X}\end{bmatrix}
  \end{bmatrix}\end{align}

Batching enables more parallelizable processing. However, batching means that the model treats each column independently; for example, the dependence of ``G`` and ``F`` can not be learned in the example above.

```python
from torchtext.datasets import WikiText2
from torchtext.data.utils import get_tokenizer
from torchtext.vocab import build_vocab_from_iterator

train_iter = WikiText2(root="../../../resources/.data", split='train')
tokenizer = get_tokenizer('basic_english')
vocab = build_vocab_from_iterator(map(tokenizer, train_iter), specials=['<unk>'])
vocab.set_default_index(vocab['<unk>'])

def data_process(raw_text_iter: dataset.IterableDataset) -> Tensor:
    """Converts raw text into a flat Tensor."""
    data = [torch.tensor(vocab(tokenizer(item)), dtype=torch.long) for item in raw_text_iter]
    return torch.cat(tuple(filter(lambda t: t.numel() > 0, data)))

# train_iter was "consumed" by the process of building the vocab,
# so we have to create it again
train_iter, val_iter, test_iter = WikiText2(root="../../../resources/.data")
train_data = data_process(train_iter)
val_data = data_process(val_iter)
test_data = data_process(test_iter)

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

def batchify(data: Tensor, bsz: int) -> Tensor:
    """Divides the data into bsz separate sequences, removing extra elements
    that wouldn't cleanly fit.

    Args:
        data: Tensor, shape [N]
        bsz: int, batch size

    Returns:
        Tensor of shape [N // bsz, bsz]
    """
    seq_len = data.size(0) // bsz
    data = data[:seq_len * bsz]
    data = data.view(bsz, seq_len).t().contiguous()
    return data.to(device)

batch_size = 20
eval_batch_size = 10
train_data = batchify(train_data, batch_size)  # shape [seq_len, batch_size]
val_data = batchify(val_data, eval_batch_size)
test_data = batchify(test_data, eval_batch_size)
```

    C:\Users\tanch\Desktop\CheeLam\Skymind\NLP\Certifai\nlp-labs\resources\.data\WikiText2\wikitext-2-v1.zip: 100%|█| 4.48M

## Generate Input and Target Sequence

``get_batch()`` generates a pair of input-target sequences for the transformer model. It subdivides the source data into chunks of length ``bptt``. For the language modeling task, the model needs the following words as ``Target``. For example, with a ``bptt`` value of 2, we’d get following two Variables for ``i`` = 0:

It should be noted that the chunks are along dimension 0, consistent with the ``S`` dimension in the Transformer model. The batch dimension ``N`` is along dimension 1.

```python
bptt = 35
def get_batch(source: Tensor, i: int) -> Tuple[Tensor, Tensor]:
    """
    Args:
        source: Tensor, shape [full_seq_len, batch_size]
        i: int

    Returns:
        tuple (data, target), where data has shape [seq_len, batch_size] and
        target has shape [seq_len * batch_size]
    """
    seq_len = min(bptt, len(source) - 1 - i)
    data = source[i:i+seq_len]
    target = source[i+1:i+1+seq_len].reshape(-1)
    return data, target
```

## Initiate an Instance

The model hyperparameters are defined below. The vocab size is equal to the length of the vocab object.

```python
ntokens = len(vocab)  # size of vocabulary
emsize = 200  # embedding dimension
d_hid = 200  # dimension of the feedforward network model in nn.TransformerEncoder
nlayers = 2  # number of nn.TransformerEncoderLayer in nn.TransformerEncoder
nhead = 2  # number of heads in nn.MultiheadAttention
dropout = 0.2  # dropout probability
model = TransformerModel(ntokens, emsize, nhead, d_hid, nlayers, dropout).to(device)
```

## Run the Model

We use [CrossEntropyLoss](https://pytorch.org/docs/stable/generated/torch.nn.CrossEntropyLoss.html) with the [SGD](https://pytorch.org/docs/stable/generated/torch.optim.SGD.html) (stochastic gradient descent) optimizer. The learning rate is initially set to 5.0 and follows a [StepLR](https://pytorch.org/docs/stable/generated/torch.optim.lr_scheduler.StepLR.html) schedule. During training, we use [nn.utils.clip_grad_norm_](https://pytorch.org/docs/stable/generated/torch.nn.utils.clip_grad_norm_.html) to prevent gradients from exploding.

```python
import copy
import time

criterion = nn.CrossEntropyLoss()
lr = 5.0  # learning rate
optimizer = torch.optim.SGD(model.parameters(), lr=lr)
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, 1.0, gamma=0.95)

def train(model: nn.Module) -> None:
    model.train()  # turn on train mode
    total_loss = 0.
    log_interval = 200
    start_time = time.time()
    src_mask = generate_square_subsequent_mask(bptt).to(device)

    num_batches = len(train_data) // bptt
    for batch, i in enumerate(range(0, train_data.size(0) - 1, bptt)):
        data, targets = get_batch(train_data, i)
        batch_size = data.size(0)
        if batch_size != bptt:  # only on last batch
            src_mask = src_mask[:batch_size, :batch_size]
        output = model(data, src_mask)
        loss = criterion(output.view(-1, ntokens), targets)

        optimizer.zero_grad()
        loss.backward()
        torch.nn.utils.clip_grad_norm_(model.parameters(), 0.5)
        optimizer.step()

        total_loss += loss.item()
        if batch % log_interval == 0 and batch > 0:
            lr = scheduler.get_last_lr()[0]
            ms_per_batch = (time.time() - start_time) * 1000 / log_interval
            cur_loss = total_loss / log_interval
            ppl = math.exp(cur_loss)
            print(f'| epoch {epoch:3d} | {batch:5d}/{num_batches:5d} batches | '
                  f'lr {lr:02.2f} | ms/batch {ms_per_batch:5.2f} | '
                  f'loss {cur_loss:5.2f} | ppl {ppl:8.2f}')
            total_loss = 0
            start_time = time.time()

def evaluate(model: nn.Module, eval_data: Tensor) -> float:
    model.eval()  # turn on evaluation mode
    total_loss = 0.
    src_mask = generate_square_subsequent_mask(bptt).to(device)
    with torch.no_grad():
        for i in range(0, eval_data.size(0) - 1, bptt):
            data, targets = get_batch(eval_data, i)
            batch_size = data.size(0)
            if batch_size != bptt:
                src_mask = src_mask[:batch_size, :batch_size]
            output = model(data, src_mask)
            output_flat = output.view(-1, ntokens)
            total_loss += batch_size * criterion(output_flat, targets).item()
    return total_loss / (len(eval_data) - 1)
```

Loop over epochs. Save the model if the validation loss is the best
we've seen so far. Adjust the learning rate after each epoch.

```python
best_val_loss = float('inf')
epochs = 3
best_model = None

for epoch in range(1, epochs + 1):
    epoch_start_time = time.time()
    train(model)
    val_loss = evaluate(model, val_data)
    val_ppl = math.exp(val_loss)
    elapsed = time.time() - epoch_start_time
    print('-' * 89)
    print(f'| end of epoch {epoch:3d} | time: {elapsed:5.2f}s | '
          f'valid loss {val_loss:5.2f} | valid ppl {val_ppl:8.2f}')
    print('-' * 89)

    if val_loss < best_val_loss:
        best_val_loss = val_loss
        best_model = copy.deepcopy(model)

    scheduler.step()
```

    | epoch   1 |   200/ 2928 batches | lr 5.00 | ms/batch 18.23 | loss  8.09 | ppl  3273.48
    | epoch   1 |   400/ 2928 batches | lr 5.00 | ms/batch 16.40 | loss  6.87 | ppl   963.58
    | epoch   1 |   600/ 2928 batches | lr 5.00 | ms/batch 16.56 | loss  6.43 | ppl   622.67
    | epoch   1 |   800/ 2928 batches | lr 5.00 | ms/batch 16.51 | loss  6.30 | ppl   543.93
    | epoch   1 |  1000/ 2928 batches | lr 5.00 | ms/batch 16.45 | loss  6.19 | ppl   485.98
    | epoch   1 |  1200/ 2928 batches | lr 5.00 | ms/batch 16.64 | loss  6.16 | ppl   471.36
    | epoch   1 |  1400/ 2928 batches | lr 5.00 | ms/batch 16.67 | loss  6.11 | ppl   449.83
    | epoch   1 |  1600/ 2928 batches | lr 5.00 | ms/batch 16.91 | loss  6.11 | ppl   448.16
    | epoch   1 |  1800/ 2928 batches | lr 5.00 | ms/batch 16.77 | loss  6.02 | ppl   412.51
    | epoch   1 |  2000/ 2928 batches | lr 5.00 | ms/batch 16.88 | loss  6.01 | ppl   406.05
    | epoch   1 |  2200/ 2928 batches | lr 5.00 | ms/batch 16.87 | loss  5.89 | ppl   362.57
    | epoch   1 |  2400/ 2928 batches | lr 5.00 | ms/batch 16.91 | loss  5.97 | ppl   390.99
    | epoch   1 |  2600/ 2928 batches | lr 5.00 | ms/batch 16.74 | loss  5.95 | ppl   383.00
    | epoch   1 |  2800/ 2928 batches | lr 5.00 | ms/batch 16.75 | loss  5.88 | ppl   357.24
    -----------------------------------------------------------------------------------------
    | end of epoch   1 | time: 51.33s | valid loss  5.76 | valid ppl   316.45
    -----------------------------------------------------------------------------------------
    | epoch   2 |   200/ 2928 batches | lr 4.75 | ms/batch 16.86 | loss  5.86 | ppl   349.11
    | epoch   2 |   400/ 2928 batches | lr 4.75 | ms/batch 16.87 | loss  5.84 | ppl   343.69
    | epoch   2 |   600/ 2928 batches | lr 4.75 | ms/batch 16.83 | loss  5.66 | ppl   288.45
    | epoch   2 |   800/ 2928 batches | lr 4.75 | ms/batch 16.90 | loss  5.71 | ppl   301.12
    | epoch   2 |  1000/ 2928 batches | lr 4.75 | ms/batch 17.30 | loss  5.65 | ppl   283.71
    | epoch   2 |  1200/ 2928 batches | lr 4.75 | ms/batch 16.95 | loss  5.68 | ppl   291.73
    | epoch   2 |  1400/ 2928 batches | lr 4.75 | ms/batch 16.97 | loss  5.69 | ppl   296.32
    | epoch   2 |  1600/ 2928 batches | lr 4.75 | ms/batch 17.31 | loss  5.71 | ppl   302.96
    | epoch   2 |  1800/ 2928 batches | lr 4.75 | ms/batch 17.00 | loss  5.65 | ppl   284.68
    | epoch   2 |  2000/ 2928 batches | lr 4.75 | ms/batch 17.10 | loss  5.68 | ppl   291.87
    | epoch   2 |  2200/ 2928 batches | lr 4.75 | ms/batch 17.07 | loss  5.55 | ppl   256.13
    | epoch   2 |  2400/ 2928 batches | lr 4.75 | ms/batch 17.13 | loss  5.64 | ppl   281.77
    | epoch   2 |  2600/ 2928 batches | lr 4.75 | ms/batch 17.17 | loss  5.65 | ppl   283.58
    | epoch   2 |  2800/ 2928 batches | lr 4.75 | ms/batch 17.07 | loss  5.58 | ppl   265.88
    -----------------------------------------------------------------------------------------
    | end of epoch   2 | time: 52.05s | valid loss  5.67 | valid ppl   290.15
    -----------------------------------------------------------------------------------------
    | epoch   3 |   200/ 2928 batches | lr 4.51 | ms/batch 17.21 | loss  5.60 | ppl   271.19
    | epoch   3 |   400/ 2928 batches | lr 4.51 | ms/batch 17.30 | loss  5.62 | ppl   275.02
    | epoch   3 |   600/ 2928 batches | lr 4.51 | ms/batch 17.21 | loss  5.42 | ppl   225.84
    | epoch   3 |   800/ 2928 batches | lr 4.51 | ms/batch 17.35 | loss  5.48 | ppl   240.20
    | epoch   3 |  1000/ 2928 batches | lr 4.51 | ms/batch 17.31 | loss  5.43 | ppl   229.05
    | epoch   3 |  1200/ 2928 batches | lr 4.51 | ms/batch 17.30 | loss  5.47 | ppl   238.18
    | epoch   3 |  1400/ 2928 batches | lr 4.51 | ms/batch 17.26 | loss  5.49 | ppl   241.46
    | epoch   3 |  1600/ 2928 batches | lr 4.51 | ms/batch 17.29 | loss  5.52 | ppl   249.93
    | epoch   3 |  1800/ 2928 batches | lr 4.51 | ms/batch 17.54 | loss  5.46 | ppl   234.99
    | epoch   3 |  2000/ 2928 batches | lr 4.51 | ms/batch 17.44 | loss  5.48 | ppl   239.75
    | epoch   3 |  2200/ 2928 batches | lr 4.51 | ms/batch 17.38 | loss  5.35 | ppl   210.77
    | epoch   3 |  2400/ 2928 batches | lr 4.51 | ms/batch 17.42 | loss  5.45 | ppl   233.57
    | epoch   3 |  2600/ 2928 batches | lr 4.51 | ms/batch 17.39 | loss  5.47 | ppl   237.50
    | epoch   3 |  2800/ 2928 batches | lr 4.51 | ms/batch 17.34 | loss  5.40 | ppl   222.27
    -----------------------------------------------------------------------------------------
    | end of epoch   3 | time: 53.30s | valid loss  5.63 | valid ppl   278.64
    -----------------------------------------------------------------------------------------

## Evaluate the Best Model on the Test Dataset

```python
test_loss = evaluate(best_model, test_data)
test_ppl = math.exp(test_loss)
print('=' * 89)
print(f'| End of training | test loss {test_loss:5.2f} | '
      f'test ppl {test_ppl:8.2f}')
print('=' * 89)
```

    =========================================================================================
    | End of training | test loss  5.54 | test ppl   255.51
    =========================================================================================

# Contributors

**Author**

Chee Lam

# References

1. [PyTorch Transformer Tutorial](https://pytorch.org/tutorials/beginner/transformer_tutorial.html)

nlp-labs/Day_11/Topic Modelling (Transformer)/Topic Modelling with Transformer.ipynb


# Some definitions

Import libraries:

* `mpnum`: Matrix product states with Python, http://mpnum.readthedocs.io/
* `numpy`: Numerical computations with Python, http://www.numpy.org/
* `scipy`: Advanced numerical facilities for Python, https://www.scipy.org/
* `matplotlib`: Create figures, http://matplotlib.org/
* `seaborn`: More tools for creating figures, http://seaborn.pydata.org/

This notebook uses Python and it is part of https://github.com/milan-hl/mpnum-examples/.

```python
%matplotlib inline

import mpnum as mp
import numpy as np
import scipy as sp
import scipy.sparse as spa

import matplotlib.pyplot as plt
import seaborn as sns
```

Limit the amount of memory we can use to avoid crashing the machine (does not work on Windows).

```python
max_gb = 2

try:
    import resource
except ImportError:
    resource = None

if resource is None:
    print('Warning: No memory limit set!')
else:
    resource.setrlimit(resource.RLIMIT_AS, (max_gb * 2**30, resource.RLIM_INFINITY))
```

We work with qubits, which have "local dimension" two.

```python
ldim = 2
```

Create some frequently-used vectors and matrices.

```python
# Spin up and down (pure states)
up = np.array([1, 0], dtype=complex)
down = np.array([0, 1], dtype=complex)

# Spin up and down (density matrices)
puu = np.outer(up, up.conj())
pdd = np.outer(down, down.conj())

# Pauli matrices
px = np.array([[0, 1], [1, 0]], dtype=complex)
py = np.array([[0, -1j], [1j, 0]], dtype=complex)
pz = np.array([[1, 0], [0, -1]], dtype=complex)
```

Convert the vectors and matrices to MPS and MPO objects with a single site.

This is used to compute tensor products as matrix products states/operators.

```python
mup, mdown = (mp.MPArray.from_array(x, ndims=1) for x in (up, down))
mpx, mpy, mpz, muu, mdd = (mp.MPArray.from_array(x, ndims=2) for x in (px, py, pz, puu, pdd))
mid = mp.factory.eye(sites=1, ldim=ldim)
```

Here is an overview of the MPSs and MPOs defined above:
\begin{align}
  \texttt{mup} &= \vert0\rangle \\
  \texttt{mdown} &= \vert1\rangle \\
  \texttt{muu} &= \vert0\rangle\langle0\vert \\
  \texttt{mdd} &= \vert1\rangle\langle1\vert \\
  \texttt{mpx} &= \sigma_X \\
  \texttt{mpy} &= \sigma_Y \\
  \texttt{mpz} &= \sigma_Z \\
  \texttt{mid} &= \mathbb 1 \quad \text{(single qubit)} \\
\end{align}

```python

```