# Ce notebook est *en cours de rédaction*
- Je vais implémenter une fonction, en [Python 3](https://docs.python.org/3/), qui permettra de résoudre rapidement un problème mathématique.

---
## Exposé du problème :
- Soit $n \geq 1$ un nombre de faces pour des dés bien équilibrés. On prendre $n = 6$ pour commencer, mais $n = 8, 10, 12, 20$ ou $n = 100$ est aussi possible.
- On veut trouver deux ensembles d'entiers, $A$ et $B$, de tailles $n$, tels que les entiers dans $A$ soient tous [premiers](https://fr.wikipedia.org/wiki/Nombre_premier), et les entiers dans $B$ soient tous [pairs](https://fr.wikipedia.org/wiki/Parit%C3%A9_(arithm%C3%A9tique)), et on souhaite que chaque somme $a + b$ pour $a \in A$ et $b \in B$ soit encore un nombre premier.

Par exemples :
- Avec $n = 1$, prendre $A = \{3\}$ et $B = \{2\}$ fonctionne, comme $3 + 2 = 5$ est premier.
- Avec $n = 2$, prendre $A = \{3, 5\}$ et $B = \{2, 8\}$ fonctionne, comme $3 + 2 = 5$, $3 + 8 = 11$, $5 + 2 = 7$, et $5 + 8 = 13$ sont premiers.
- Avec $n = 6$, c'est tout de suite moins facile à trouver de tête... Allez, je triche et je donne un exemple trouvé plus bas : $A = \{5, 7, 17, 31, 61, 101\}$ et $B = \{6, 12, 36, 66, 96, 132\}$.

### Buts
Notre but est d'abord de trouver vérifier qu'une paire $(A, B)$ donnée soit valide, puis de trouver une paire valide de taille $n$ donnée.
On ne cherchera pas à avoir un algorithme très malin, une énumération exhaustive nous suffira.

Ensuite on cherchera à répondre à une question plus combinatoire : peut-on trouver, à $n$ fixer, la paire valide $(A, B)$ de somme minimale (ou minimisant un certain critère) ?
La réponse sera oui, et pas trop dure à obtenir.

---
## Présentation de ma solution numérique
- Je vais utiliser le module python [`sympy`](http://sympy.org/) et en particulier ses fonctions du module [`sympy.ntheory`](http://docs.sympy.org/latest/modules/ntheory.html) pour avoir facilement accès à une liste de nombres premiers. En particulier, [`primerange`](http://docs.sympy.org/latest/modules/ntheory.html#sympy.ntheory.generate.primerange) sera bien pratique.
- Je vais procéder par énumération totale avec un "doubling trick" : on cherchera toutes les paires $(A, B)$ bornées dans, disons, $[1, 100]$, puis $[1, 110]$ et ainsi de suite (avec un petit incrément), jusqu'à en trouver une qui marche.
- Cette approche "ascendante" garantit de terminer, pour peu qu'on puisse prouver théoriquement l'existence de la solution qu'on cherche.

### Solution théorique ?
> Je ne vais pas rentrer dans les détails, mais avec [le théorème de la progression arithmétique de Dirichlet](https://fr.wikipedia.org/wiki/Théorème_de_la_progression_arithmétique) (cf. aussi [ce document](http://perso.eleves.ens-rennes.fr/~ariffaut/Agregation/Dirichlet.pdf)), on peut montrer que pour tout nombre de faces $n \geq 1$, on peut trouver un nombre infini d'ensembles $(A, B)$ qui conviennent.
> *Ouais, c'est balèze*.

----
### Solution numérique
On commence avec les dépendances suivantes.

> *Note :* Si vous n'avez pas installé [`sympy`](http://sympy.org/), un simple coup de `pip install sympy` suffit.

```python
from sympy import isprime
from sympy import primerange
```

---
## Implémentation des fonctions utilitaires requises
D'abord, une fonction `verifie_paire` qui vérifie si une paire $(A, B)$, donnée sous forme de deux *itérateurs* (liste ou ensemble, peu importe), est valide.

```python
def verifie_paire(A, B):
    """Version efficace, qui s'arrête le plus tôt possible."""
    if A is None or B is None:
        return False
    for a in A:
        # if not isprime(a):  # Inutile car A aura été bien choisi
        #    return False
        for b in B:
            if not isprime(a + b):
                return False
    return True
```

Pour visualiser un peu, on en fait une version qui parle :

```python
def verifie_paire_debug(A, B):
    """Version plus lente qui fait tous les tests, et affiche vrai ou faux pour chaque somme a + b."""
    if A is None or B is None:
        return False
    reponse = True
    for a in A:
        if not isprime(a):
            print("  - a = {:<6} est pas premier, ECHEC ...".format(a))
            reponse = False
        for b in B:
            if not isprime(a + b):
                print("  - a = {:<6} + b = {:<6} donnent {:<6} qui n'est pas premier, ECHEC ...".format(a, b, a + b))
                reponse = False
            else:
                print("  - a = {:<6} + b = {:<6} donnent {:<6} qui est bien premier, OK ...".format(a, b, a + b))
    return reponse
```

Premier exemple :

```python
A = [3]
B = [2]
verifie_paire_debug(A, B)
```

      - a = 3      + b = 2      donnent 5      qui est bien premier, OK ...

    True

Second exemple :

```python
A = (3, 5)
B = (2, 8)
verifie_paire_debug(A, B)
```

      - a = 3      + b = 2      donnent 5      qui est bien premier, OK ...
      - a = 3      + b = 8      donnent 11     qui est bien premier, OK ...
      - a = 5      + b = 2      donnent 7      qui est bien premier, OK ...
      - a = 5      + b = 8      donnent 13     qui est bien premier, OK ...

    True

```python
verifie_paire(A, B)
```

    True

Avec $n = 6$ :

```python
A = [5, 7, 17, 31, 61, 101]
B = [6, 12, 36, 66, 96, 132]
verifie_paire(A, B)
```

    True

Les dés vendus dans le commerce sont marqués avec ces faces :

```python
A = [2, 6, 20, 252, 266, 380]
B = [17, 41, 107, 191, 311, 347]
verifie_paire(A, B)
```

    True

---
On a besoin de cette fonction de combinaison :

```python
from itertools import combinations
```

Ensuite, on écrit une fonction qui va énumérer *tous* les ensembles $(A, B)$ possibles et valides, avec $\max (A \cup B ) \leq M$ pour $M$ fixé.
J'ajoute une borne inférieure $m$, valant par défaut $m = 1$, pour rester le plus générique possible, mais on ne s'en sert pas vraiment.

- On récupère les candidats possibles pour $a \in A$ via `primerange` (notés $C_A$), et pour $B$ via un `range` à pas $2$ pour considérer seulement des nombres pairs (notés $B_A$),
- Ensuite on boucle sur tous les ensembles $A$ de taille $n$ dans $C_A$, et $B$ de taille $n$ dans $C_B$, via [`itertools.combinations`](https://docs.python.org/3/library/itertools.html#itertools.combinations),
- On garde uniquement ceux qui sont valides, et on les stocke tous.

> **Attention** ça devient vite très couteux !

```python
def candidats_CA_CB(M=10, m=1):
    # Premiers entre m et M (compris), ie. P inter [|m, ..., M|]
    C_A = list(primerange(m, M + 1))
    # Nombres pairs
    if m % 2 == 0:
        C_B = list(range(m, M - 1, 2))
    else:
        C_B = list(range(m + 1, M - 1, 2))
    return C_A, C_B
```

Cette première approche est très naïve, et énormément de calculs sont dédoublés (on teste les paires $(A, B)$ en faisant plein de tests pour savoir si $a + b$ est premier, c'est couteux).

```python
def enumere_toutes_les_paires(n=2, M=10, m=1, debug=False):
    C_A, C_B = candidats_CA_CB(M=M, m=m)
    if debug:
        print("C_A =", C_A)
        print("C_B =", C_B)
        print("Combinaison de n =", n, "éléments dans A =", list(combinations(C_A, n)))
        print("Combinaison de n =", n, "éléments dans B =", list(combinations(C_B, n)))
    # C_A, C_B est déjà trié, c'est cool
    all_A_B = []
    for A in combinations(C_A, n):
        if debug: print(" - A =", A)
        for B in combinations(C_B, n):
            if debug: print("     - B =", B)
            if verifie_paire(A, B):
                if debug: print("==> Une paire (A, B) de plus !")
                all_A_B.append((A, B))
    # all_A_B est aussi trié par ordre lexicographique
    return all_A_B
```

On peut vérifier que les exemples donnés ci-dessus sont valides, et sont en fait les plus petits :

```python
n = 1
M = 10
enumere_toutes_les_paires(n, M, debug=True)
```

    C_A = [2, 3, 5, 7]
    C_B = [2, 4, 6, 8]
    Combinaison de n = 1 éléments dans A = [(2,), (3,), (5,), (7,)]
    Combinaison de n = 1 éléments dans B = [(2,), (4,), (6,), (8,)]
     - A = (2,)
         - B = (2,)
         - B = (4,)
         - B = (6,)
         - B = (8,)
     - A = (3,)
         - B = (2,)
    ==> Une paire (A, B) de plus !
         - B = (4,)
    ==> Une paire (A, B) de plus !
         - B = (6,)
         - B = (8,)
    ==> Une paire (A, B) de plus !
     - A = (5,)
         - B = (2,)
    ==> Une paire (A, B) de plus !
         - B = (4,)
         - B = (6,)
    ==> Une paire (A, B) de plus !
         - B = (8,)
    ==> Une paire (A, B) de plus !
     - A = (7,)
         - B = (2,)
         - B = (4,)
    ==> Une paire (A, B) de plus !
         - B = (6,)
    ==> Une paire (A, B) de plus !
         - B = (8,)

    [((3,), (2,)),
     ((3,), (4,)),
     ((3,), (8,)),
     ((5,), (2,)),
     ((5,), (6,)),
     ((5,), (8,)),
     ((7,), (4,)),
     ((7,), (6,))]

```python
n = 2
M = 10
enumere_toutes_les_paires(n, M, debug=True)
```

    C_A = [2, 3, 5, 7]
    C_B = [2, 4, 6, 8]
    Combinaison de n = 2 éléments dans A = [(2, 3), (2, 5), (2, 7), (3, 5), (3, 7), (5, 7)]
    Combinaison de n = 2 éléments dans B = [(2, 4), (2, 6), (2, 8), (4, 6), (4, 8), (6, 8)]
     - A = (2, 3)
         - B = (2, 4)
         - B = (2, 6)
         - B = (2, 8)
         - B = (4, 6)
         - B = (4, 8)
         - B = (6, 8)
     - A = (2, 5)
         - B = (2, 4)
         - B = (2, 6)
         - B = (2, 8)
         - B = (4, 6)
         - B = (4, 8)
         - B = (6, 8)
     - A = (2, 7)
         - B = (2, 4)
         - B = (2, 6)
         - B = (2, 8)
         - B = (4, 6)
         - B = (4, 8)
         - B = (6, 8)
     - A = (3, 5)
         - B = (2, 4)
         - B = (2, 6)
         - B = (2, 8)
    ==> Une paire (A, B) de plus !
         - B = (4, 6)
         - B = (4, 8)
         - B = (6, 8)
     - A = (3, 7)
         - B = (2, 4)
         - B = (2, 6)
         - B = (2, 8)
         - B = (4, 6)
         - B = (4, 8)
         - B = (6, 8)
     - A = (5, 7)
         - B = (2, 4)
         - B = (2, 6)
         - B = (2, 8)
         - B = (4, 6)
         - B = (4, 8)
         - B = (6, 8)

    [((3, 5), (2, 8))]

On peut continuer, avec $n = 3$ et un petit majorant $M$ :

```python
n = 3
M = 20
enumere_toutes_les_paires(n, M, debug=False)
```

    [((3, 7, 13), (4, 10, 16)), ((5, 11, 17), (2, 6, 12))]

On peut continuer, avec $n = 4$ et un petit majorant $M$ :

```python
n = 4
M = 40
%time enumere_toutes_les_paires(n, M, debug=False)
```

    CPU times: user 7.41 s, sys: 0 ns, total: 7.41 s
    Wall time: 7.41 s

    [((3, 7, 13, 37), (4, 10, 16, 34)),
     ((7, 11, 17, 31), (6, 12, 30, 36)),
     ((7, 13, 19, 37), (4, 10, 24, 34)),
     ((7, 13, 31, 37), (6, 10, 16, 30)),
     ((7, 17, 23, 37), (6, 24, 30, 36))]

On voit que ça commence à prendre du temps.

Deux améliorations seront explorées :
- On a pas besoin de toutes les calculer, en fait. Si on cherche juste une paire valide, on peut s'arrêter à la première trouvée.
- On peut être malin dans la création des candidats $C_B$, plutôt que de juste prendre de nombres pairs, on prend directement des nombres $b$ tels que $a + b$ soit premier.

---
## Première optimisation
- On n'a pas besoin d'énunumérer toutes les paires, il suffit de donner la premier trouvée.
- Pourquoi la première ? Et bien si $C_A$ et $C_B$ sont triés, les candidats pour $(A, B)$ seront aussi triés par ordre lexicographique, et donc le premier trouvé sera le plus petit.

```python
def premiere_paire(n=2, M=10, m=1):
    C_A, C_B = candidats_CA_CB(M=M, m=m)
    # C_A, C_B est déjà trié, c'est cool
    for A in combinations(C_A, n):
        for B in combinations(C_B, n):
            if verifie_paire(A, B):
                return (A, B)
    return (None, None)
```

```python
n = 4
M = 40
%time A, B = premiere_paire(n, M)
print("A =", A)
print("B =", B)
```

    CPU times: user 2 s, sys: 8 ms, total: 2.01 s
    Wall time: 2.01 s
    A = (3, 7, 13, 37)
    B = (4, 10, 16, 34)

Avec $n = 5$, on commence à avoir besoin d'aller plus loin que ce majorant `M = 40` :

```python
n = 5
M = 40
%time A, B = premiere_paire(n, M)  # (None, None) indique qu'on a pas trouvé
print("A =", A)
print("B =", B)
```

    CPU times: user 33.7 s, sys: 0 ns, total: 33.7 s
    Wall time: 33.7 s
    A = None
    B = None

Mais parfois on ne sait pas trop quelle valeur donner à ce majorant `M`...

Une approche simple est donc d'augmenter sa valeur jusqu'à trouver une paire valide.

```python
def premiere_paire_explore_M(n=2, Ms=[10,20,30,40,50], m=1):
    for M in Ms:
        resultat = premiere_paire(n=n, M=M, m=m)
        if resultat[0] is not None:
            return resultat
    return (None, None)
```

On peut faire encore mieux, en augmentant `M` automatiquement d'un certain offset $\delta_M > 0$ :

```python
def premiere_paire_augmente_M(n=2, Mmin=10, deltaM=10, m=1):
    assert isinstance(deltaM, int)
    assert deltaM >= 1
    M = Mmin
    while True:
        print("Appel à premiere_paire(n={}, M={}, m={}) ...".format(n, M, m))
        # Ce n'est pas dangereux, puisqu'on est garanti de trouver une paire qui marche
        resultat = premiere_paire(n=n, M=M, m=m)
        if resultat[0] is not None:
            print("Terminé, avec M =", M)
            return resultat
        M += deltaM
```

On peut retrouver le résultat trouvé plus haut, pour $n = 2$ :

```python
n = 2
Ms = [10,20,30,40,50]
%time A, B = premiere_paire_explore_M(n, Ms)
print("A =", A)
print("B =", B)
```

    CPU times: user 0 ns, sys: 0 ns, total: 0 ns
    Wall time: 331 µs
    A = (3, 5)
    B = (2, 8)

```python
n = 4
M = 40
deltaM = 10
%time A, B = premiere_paire_augmente_M(n=n, Mmin=M, deltaM=deltaM)
print("A =", A)
print("B =", B)
```

    Appel à premiere_paire(n=4, M=40, m=1) ...
    Terminé, avec M = 40
    CPU times: user 2.01 s, sys: 0 ns, total: 2.01 s
    Wall time: 2.01 s
    A = (3, 7, 13, 37)
    B = (4, 10, 16, 34)

Et on peut résoudre pour $n = 5$ et $n = 6$. Mais cette approche prend beaucoup trop de temps.

```python
n = 5
M = 100
deltaM = 10
#%time A, B = premiere_paire_augmente_M(n=n, Mmin=M, deltaM=deltaM)
#print("A =", A)
#print("B =", B)
```

> *Note :* Plutôt que d'écrire une fonction (qui `return` le premier résultat), on peut aussi écrire un **générateur**, qui `yield` les résultats un à un.
> Il sera alors possible d'écrire des boucles directement comme ça :

```python
for (A, B) in generateur_paires(n=2, M=10, m=1):
    ...
```

```python
def generateur_paires(n=2, M=10, m=1):
    C_A, C_B = candidats_CA_CB(M=M, m=m)
    # C_A, C_B est déjà trié, c'est cool
    for A in combinations(C_A, n):
        for B in combinations(C_B, n):
            if verifie_paire(A, B):
                yield (A, B)
```

---
## Exemples

---
### Exemple avec des dés à 6 faces

```python
n = 6
M = 100
#A, B = premiere_paire(n, M)
#print("A =", A)
#print("B =", B)
```

---
## Autre approche, deuxième optimisation
On a vu que l'approche *naïve* exposée ci-dessus n'est vraiment pas très efficace.

On va être plus malin :
- d'abord générer toutes paires $(a, b)$ possibles, avec $a$ et $a + b$ premiers, pour $a, b \geq M$.
- ensuite on pourra créer la plus grande paire $(A, B)$, probablement en regardant simplement des intersections d'ensembles.

### Préliminaire : petite optimisation pour `isprime`
En une seule ligne, on peut ajouter un cache ([`functools.lru_cache`](https://docs.python.org/3/library/functools.html#functools.lru_cache)) pour la rendre plus rapide :

```python
from functools import lru_cache

@lru_cache(maxsize=1<<10, typed=False)
def estpremier(n):
    return isprime(n)

@lru_cache(maxsize=1<<20, typed=False)
def candidats_CA_CB_cache(M=10, m=1):
    return candidats_CA_CB(M=M, m=m)
```

```python
import numpy.random as nr
%timeit [isprime(n) for n in nr.randint(1, 1<<10, 100000)]
```

    1 loop, best of 3: 208 ms per loop

```python
%timeit [estpremier(n) for n in nr.randint(1, 1<<10, 100000)]
```

    10 loops, best of 3: 18.7 ms per loop

### Générer les paires $(a, b)$ valides
C'est assez rapide.
- La première fonction renvoie le résultat sous forme de liste de paires $(a, b)$ avec $b$ tel que $a + b$ soit premier,
- La seconde fonction renvoie plutôt un dictionnaire qui, à $a \in C_A$, associe tous les $b \in C_B$ tels que $a + b$.

```python
def genere_paires(M=10, m=1):
    C_A, C_B = candidats_CA_CB_cache(M=M, m=m)
    return [(a, b) for a in C_A for b in C_B if estpremier(a + b)]

def genere_dict(M=10, m=1):
    C_A, C_B = candidats_CA_CB_cache(M=M, m=m)
    return {a: [b for b in C_B if estpremier(a + b)] for a in C_A}
```

```python
genere_paires(10)
```

    [(3, 2), (3, 4), (3, 8), (5, 2), (5, 6), (5, 8), (7, 4), (7, 6)]

```python
genere_dict(10)
```

    {2: [], 3: [2, 4, 8], 5: [2, 6, 8], 7: [4, 6]}

On voit que le nombres de paires $(a, b)$ telle que $a + b$ soit premier et $a, b \leq M$ augmente assez vite avec $M$ :

```python
len(genere_paires(400))
```

    5107

```python
import matplotlib.pyplot as plt
X = list(range(10, 2000, 10))
Y = [ len(genere_paires(M)) for M in X ]
plt.plot(X, Y)
plt.show()
```

## Suite : générer une paire $(A, B)$
- On génère les candidats $C_A, C_B$,
- On associe à chaque $a \in C_A$ les $b \in C_B$ qui fonctionne, notés $d(a)$,
- On garde ceux qui ont au moins $n$ valeurs associées,
- On regarde tous les ensembles $A \subset C_A$ de taille exactement $n$,
- Pour un $A$ donné, on construit $B$ comme $\bigcap_{a \in A} d(a)$, et on garde les $B$ de taille $\geq n$,
- On regarde enfin tous les ensembles $B' \subset B$ de taille exactement $n$.
- Ici, on garde juste le premier trouvé, plus bas on les garde tous.

```python
@lru_cache(maxsize=1<<10, typed=False)
def genere_A_B(n=2, M=10, m=1):
    C_A, C_B = candidats_CA_CB_cache(M=M, m=m)
    dictAB = {a: {b for b in C_B if estpremier(a + b)} for a in C_A}
    for a in C_A:
        if len(dictAB[a]) < n:
            del dictAB[a]
    # print(dictAB)
    C_A = sorted(list(dictAB.keys()))
    # C_A est déjà trié, c'est cool
    # TODO : This for loop could possibly get parallelized, using joblib.Parallel
    # It could speed up the computation by a factor of cpu_count()...
    for A in combinations(C_A, n):
        # DONE : improve this step to stop as soon as the intersection has length < n
        # B = set.intersection(*(dictAB[a] for a in A))
        B = set(dictAB[A[0]])
        for a in A[1:]:
            if len(B) < n:
                break
            B &= dictAB[a]
        if len(B) >= n:
            print(" - A =", A, "\t et B =", B, "...")
            for BB in combinations(B, n):  # On prend la 1ère
                return (list(A), list(BB))
    return (None, None)
```

```python
def test_genere_AB(n=2, M=10, m=1):
    A, B = genere_A_B(n=n, M=M)
    if not verifie_paire(A, B):
        print("Mauvaise paire (A, B) =", (A, B), ": elle n'est pas valable.")
        return (None, None)
    print("A =", A)
    print("B =", B)
    return (A, B)
```

```python
n = 2
M = 10
test_genere_AB(n=n, M=M)
```

     - A = (3, 5) 	 et B = {8, 2} ...
    A = [3, 5]
    B = [8, 2]

    ([3, 5], [8, 2])

```python
n = 3
M = 40
%time test_genere_AB(n=n, M=M)
```

     - A = (3, 5, 11) 	 et B = {8, 2, 26} ...
    A = [3, 5, 11]
    B = [8, 2, 26]
    CPU times: user 0 ns, sys: 0 ns, total: 0 ns
    Wall time: 1.8 ms

    ([3, 5, 11], [8, 2, 26])

```python
n = 4
M = 50
%time test_genere_AB(n=n, M=M)
```

     - A = (3, 7, 13, 19) 	 et B = {40, 34, 10, 4} ...
    A = [3, 7, 13, 19]
    B = [40, 34, 10, 4]
    CPU times: user 0 ns, sys: 0 ns, total: 0 ns
    Wall time: 1.89 ms

    ([3, 7, 13, 19], [40, 34, 10, 4])

On peut comparer cette nouvelle fonction avec `premiere_paire` écrite plus haut :

```python
n = 4
M = 50
%timeit -n1 premiere_paire(n=n, M=M)
```

    1 loop, best of 3: 10.3 s per loop

```python
n = 4
M = 50
%timeit -n1 genere_A_B(n=n, M=M)
```

    1 loop, best of 3: 1.12 µs per loop

→ On est effectivement bien plus rapide. Yay.

```python
n = 5
M = 100
%time test_genere_AB(n=n, M=M)
```

     - A = (3, 7, 13, 19, 97) 	 et B = {40, 34, 10, 4, 94} ...
    A = [3, 7, 13, 19, 97]
    B = [40, 34, 10, 4, 94]
    CPU times: user 4 ms, sys: 0 ns, total: 4 ms
    Wall time: 4.67 ms

    ([3, 7, 13, 19, 97], [40, 34, 10, 4, 94])

```python
n = 6
M = 100
%time test_genere_AB(n=n, M=M)
```

     - A = (7, 13, 31, 37, 73, 97) 	 et B = {66, 6, 10, 76, 16, 30} ...
    A = [7, 13, 31, 37, 73, 97]
    B = [66, 6, 10, 76, 16, 30]
    CPU times: user 112 ms, sys: 0 ns, total: 112 ms
    Wall time: 110 ms

    ([7, 13, 31, 37, 73, 97], [66, 6, 10, 76, 16, 30])

On peut vérifier que la valeur du majorant $M$ sur les valeurs des faces ne change pas le premier $A$ trouvé, mais comme les $B$ ne sont **pas** parcourus dans un ordre lexicographique (les `set` sont non triés), on ne trouve pas le même $B$.
Et plus $M$ est grand, plus c'est long à calculer.

```python
n = 6
M = 200
%time test_genere_AB(n=n, M=M)
```

     - A = (3, 7, 13, 31, 73, 97) 	 et B = {160, 100, 40, 10, 76, 16} ...
    A = [3, 7, 13, 31, 73, 97]
    B = [160, 100, 40, 10, 76, 16]
    CPU times: user 248 ms, sys: 0 ns, total: 248 ms
    Wall time: 246 ms

    ([3, 7, 13, 31, 73, 97], [160, 100, 40, 10, 76, 16])

Ça marche plutôt bien, on trouve effectivement la paire qui possède le plus petit $\max A$ mais pas celle qui minimise $\sum A \cup B$ ou $\max A \cup B$...

## Trouver la paire de somme minimale ou de max minimum.
- On va calculer *toutes* les paires, avec notre fonction optimisée, dans `genere_toutes_A_B`,
- Puis on les triera (en ordre croissant) selon le critère voulu,
- Et on renvoie la première, qui minimisera donc le critère cherché.

> *Note :* en étant malin, on pourrait carrément faire mieux qu'une énumération complète. Mais la flemme.

### D'abord, générer toutes les paires $(A, B)$
C'est la même fonction que `genere_A_B`, mais on les renvoie toutes au lieu de s'arrêter à la première.

```python
@lru_cache(maxsize=1<<10, typed=False)
def genere_toutes_A_B(n=2, M=10, m=1):
    C_A, C_B = candidats_CA_CB_cache(M=M, m=m)
    dictAB = {a: {b for b in C_B if estpremier(a + b)} for a in C_A}
    for a in C_A:
        if len(dictAB[a]) < n:
            del dictAB[a]
    # print(dictAB)
    C_A = sorted(list(dictAB.keys()))
    # C_A est déjà trié, c'est cool
    paires = []
    # TODO : This for loop could very easily get parallelized, using joblib.Parallel
    # It could speed up the computation by a factor of cpu_count()...
    for A in combinations(C_A, n):
        # DONE : improve this step to stop as soon as the intersection has length < n
        # B = set.intersection(*(dictAB[a] for a in A))
        B = set(dictAB[A[0]])
        for a in A[1:]:
            if len(B) < n:
                break
            B &= dictAB[a]
        if len(B) >= n:
            for BB in combinations(B, n):
                print(" - A =", A, "\t et B =", B, "...")
                # print('.', end='')  # Affiche .... au fur et à mesure de l'exécution.
                paires.append((list(A), list(BB)))
    print('')
    return paires
```

On vérifie que ça fonctionne comme prévu :

```python
n = 4
M = 40
genere_toutes_A_B(n=n, M=M)
```

     - A = (3, 7, 13, 37) 	 et B = {16, 34, 10, 4} ...
     - A = (7, 11, 17, 31) 	 et B = {36, 12, 6, 30} ...
     - A = (7, 13, 19, 37) 	 et B = {24, 34, 10, 4} ...
     - A = (7, 13, 31, 37) 	 et B = {16, 10, 6, 30} ...
     - A = (7, 17, 23, 37) 	 et B = {24, 36, 6, 30} ...

    [([3, 7, 13, 37], [16, 34, 10, 4]),
     ([7, 11, 17, 31], [36, 12, 6, 30]),
     ([7, 13, 19, 37], [24, 34, 10, 4]),
     ([7, 13, 31, 37], [16, 10, 6, 30]),
     ([7, 17, 23, 37], [24, 36, 6, 30])]

Il suffit donc de trier selon le critère choisi.
$$
    \mathrm{sumsum} : (A, B) \mapsto \sum_{a \in A} a + \sum_{b \in B} b.
$$
$$
    \mathrm{maxmax} : (A, B) \mapsto \max A \cup B.
$$

```python
def sumsum(paire):
    return sum(paire[0]) + sum(paire[1])

def maxmax(paire):
    return max(max(paire[0]), max(paire[1]))
```

On peut aussi prendre un critère qui combine les deux :

```python
def combine(paire):
    return (sumsum(paire) + maxmax(paire)) / 2
```

Il suffit d'utiliser l'argument `key=` de la fonction [`sorted`](https://docs.python.org/3/library/functions.html#sorted) :

```python
def trie_et_prend_min(paires, key=sumsum):
    paires = sorted(paires, key=key)
    A, B = paires[0]
    assert verifie_paire(A, B)
    return (sorted(A), sorted(B))
```

Pour tester :

```python
def test_trie_et_prend_min(n=2, M=10):
    paires = genere_toutes_A_B(n=n, M=M)
    print("Pour n =", n, "et M =", M, "il y a", len(paires), "paires (A, B) trouvées valides ...")

    print("- On recherche celle de somme sum(A u B) minimale ...")
    A, B = trie_et_prend_min(paires, key=sumsum)
    print("A =", A)
    print("B =", B)
    print("  Elle donne sumsum =", sumsum((A, B)), "et maxmax =", maxmax((A, B)))

    print("- On recherche celle de maximum max(A u B) minimale ...")
    A, B = trie_et_prend_min(paires, key=maxmax)
    print("A =", A)
    print("B =", B)
    print("  Elle donne sumsum =", sumsum((A, B)), "et maxmax =", maxmax((A, B)))

    print("- On recherche celle de moyenne sumsum(A,B), maxmax(A,B) minimale ...")
    A, B = trie_et_prend_min(paires, key=combine)
    print("A =", A)
    print("B =", B)
    print("  Elle donne sumsum =", sumsum((A, B)), "et maxmax =", maxmax((A, B)))

    # for key in [sumsum, maxmax, combine]:
    #    alls = [ (key(paire), (sorted(paire[0]), sorted(paire[1]))) for paire in sorted(paires, key=key) ]
    #    print(alls[:5])
    return (A, B)
```

On peut refaire les mêmes exemples pour $n = 3, 4, 5$, et enfin $n = 6$.

```python
n = 3
M = 20
test_trie_et_prend_min(n, M)
```

     - A = (3, 7, 13) 	 et B = {16, 10, 4} ...
     - A = (5, 11, 17) 	 et B = {2, 12, 6} ...

    Pour n = 3 et M = 20 il y a 2 paires (A, B) trouvées valides ...
    - On recherche celle de somme sum(A u B) minimale ...
    A = [3, 7, 13]
    B = [4, 10, 16]
      Elle donne sumsum = 53 et maxmax = 16
    - On recherche celle de maximum max(A u B) minimale ...
    A = [3, 7, 13]
    B = [4, 10, 16]
      Elle donne sumsum = 53 et maxmax = 16
    - On recherche celle de moyenne sumsum(A,B), maxmax(A,B) minimale ...
    A = [3, 7, 13]
    B = [4, 10, 16]
      Elle donne sumsum = 53 et maxmax = 16

    ([3, 7, 13], [4, 10, 16])

```python
n = 4
M = 37
test_trie_et_prend_min(n, M)
```

     - A = (3, 7, 13, 37) 	 et B = {16, 34, 10, 4} ...
     - A = (7, 13, 19, 37) 	 et B = {24, 34, 10, 4} ...
     - A = (7, 13, 31, 37) 	 et B = {16, 10, 6, 30} ...

    Pour n = 4 et M = 37 il y a 3 paires (A, B) trouvées valides ...
    - On recherche celle de somme sum(A u B) minimale ...
    A = [3, 7, 13, 37]
    B = [4, 10, 16, 34]
      Elle donne sumsum = 124 et maxmax = 37
    - On recherche celle de maximum max(A u B) minimale ...
    A = [3, 7, 13, 37]
    B = [4, 10, 16, 34]
      Elle donne sumsum = 124 et maxmax = 37
    - On recherche celle de moyenne sumsum(A,B), maxmax(A,B) minimale ...
    A = [3, 7, 13, 37]
    B = [4, 10, 16, 34]
      Elle donne sumsum = 124 et maxmax = 37

    ([3, 7, 13, 37], [4, 10, 16, 34])

```python
n = 5
M = 70
test_trie_et_prend_min(n, M)
```

     - A = (5, 11, 17, 41, 47) 	 et B = {6, 42, 12, 56, 26, 62} ...
     - A = (5, 11, 17, 41, 47) 	 et B = {6, 42, 12, 56, 26, 62} ...
     - A = (5, 11, 17, 41, 47) 	 et B = {6, 42, 12, 56, 26, 62} ...
     - A = (5, 11, 17, 41, 47) 	 et B = {6, 42, 12, 56, 26, 62} ...
     - A = (5, 11, 17, 41, 47) 	 et B = {6, 42, 12, 56, 26, 62} ...
     - A = (5, 11, 17, 41, 47) 	 et B = {6, 42, 12, 56, 26, 62} ...
     - A = (5, 17, 29, 47, 59) 	 et B = {24, 42, 12, 54, 14} ...
     - A = (5, 17, 31, 47, 61) 	 et B = {66, 12, 42, 36, 6} ...
     - A = (7, 13, 37, 43, 67) 	 et B = {16, 4, 30, 46, 60} ...
     - A = (11, 13, 23, 41, 53) 	 et B = {48, 18, 60, 6, 30} ...
     - A = (11, 17, 23, 47, 53) 	 et B = {56, 50, 36, 20, 6} ...

    Pour n = 5 et M = 70 il y a 11 paires (A, B) trouvées valides ...
    - On recherche celle de somme sum(A u B) minimale ...
    A = [5, 11, 17, 41, 47]
    B = [6, 12, 26, 42, 56]
      Elle donne sumsum = 263 et maxmax = 56
    - On recherche celle de maximum max(A u B) minimale ...
    A = [5, 11, 17, 41, 47]
    B = [6, 12, 26, 42, 56]
      Elle donne sumsum = 263 et maxmax = 56
    - On recherche celle de moyenne sumsum(A,B), maxmax(A,B) minimale ...
    A = [5, 11, 17, 41, 47]
    B = [6, 12, 26, 42, 56]
      Elle donne sumsum = 263 et maxmax = 56

    ([5, 11, 17, 41, 47], [6, 12, 26, 42, 56])

On observe que celle qui minimise `sumsum` est la même que celle qui minimise `maxmax`. Curieux ?

C'est aussi le cas pour $n = 6$ :

```python
n = 6
M = 150
%time test_trie_et_prend_min(n, M)
```

     - A = (3, 7, 13, 37, 73, 97) 	 et B = {34, 100, 10, 76, 16, 94} ...
     - A = (5, 7, 17, 31, 61, 101) 	 et B = {96, 66, 132, 36, 6, 12} ...
     - A = (5, 11, 17, 41, 71, 137) 	 et B = {96, 2, 42, 12, 56, 26} ...
     - A = (5, 11, 17, 41, 101, 137) 	 et B = {96, 2, 12, 56, 26, 62} ...
     - A = (5, 11, 17, 47, 71, 137) 	 et B = {36, 42, 12, 56, 26, 92} ...
     - A = (5, 11, 17, 47, 101, 137) 	 et B = {36, 12, 56, 26, 92, 62} ...
     - A = (5, 11, 17, 47, 131, 137) 	 et B = {36, 42, 146, 26, 92, 62} ...
     - A = (5, 11, 17, 71, 101, 137) 	 et B = {96, 2, 36, 12, 56, 26, 92} ...
     - A = (5, 11, 17, 71, 101, 137) 	 et B = {96, 2, 36, 12, 56, 26, 92} ...
     - A = (5, 11, 17, 71, 101, 137) 	 et B = {96, 2, 36, 12, 56, 26, 92} ...
     - A = (5, 11, 17, 71, 101, 137) 	 et B = {96, 2, 36, 12, 56, 26, 92} ...
     - A = (5, 11, 17, 71, 101, 137) 	 et B = {96, 2, 36, 12, 56, 26, 92} ...
     - A = (5, 11, 17, 71, 101, 137) 	 et B = {96, 2, 36, 12, 56, 26, 92} ...
     - A = (5, 11, 17, 71, 101, 137) 	 et B = {96, 2, 36, 12, 56, 26, 92} ...
     - A = (5, 11, 31, 53, 101, 103) 	 et B = {96, 36, 6, 78, 48, 126} ...
     - A = (5, 11, 41, 53, 83, 101) 	 et B = {96, 98, 6, 48, 56, 26} ...
     - A = (5, 11, 41, 53, 83, 131) 	 et B = {96, 98, 6, 48, 18, 26} ...
     - A = (5, 11, 41, 53, 101, 131) 	 et B = {96, 98, 6, 48, 26, 126} ...
     - A = (5, 11, 41, 71, 101, 137) 	 et B = {96, 2, 12, 56, 26, 126} ...
     - A = (5, 11, 47, 71, 101, 137) 	 et B = {36, 12, 56, 26, 92, 126} ...
     - A = (5, 11, 47, 71, 131, 137) 	 et B = {36, 102, 42, 26, 92, 126} ...
     - A = (5, 17, 31, 41, 61, 97) 	 et B = {96, 66, 132, 6, 42, 12} ...
     - A = (5, 19, 23, 29, 89, 149) 	 et B = {108, 78, 144, 18, 84, 24} ...
     - A = (5, 19, 23, 79, 89, 149) 	 et B = {78, 144, 48, 18, 84, 24} ...
     - A = (5, 19, 23, 83, 89, 149) 	 et B = {108, 144, 48, 18, 84, 24} ...
     - A = (5, 23, 29, 53, 89, 149) 	 et B = {8, 74, 78, 14, 144, 18, 84} ...
     - A = (5, 23, 29, 53, 89, 149) 	 et B = {8, 74, 78, 14, 144, 18, 84} ...
     - A = (5, 23, 29, 53, 89, 149) 	 et B = {8, 74, 78, 14, 144, 18, 84} ...
     - A = (5, 23, 29, 53, 89, 149) 	 et B = {8, 74, 78, 14, 144, 18, 84} ...
     - A = (5, 23, 29, 53, 89, 149) 	 et B = {8, 74, 78, 14, 144, 18, 84} ...
     - A = (5, 23, 29, 53, 89, 149) 	 et B = {8, 74, 78, 14, 144, 18, 84} ...
     - A = (5, 23, 29, 53, 89, 149) 	 et B = {8, 74, 78, 14, 144, 18, 84} ...
     - A = (5, 23, 29, 59, 89, 149) 	 et B = {134, 8, 108, 78, 14, 24} ...
     - A = (5, 23, 29, 83, 89, 149) 	 et B = {74, 108, 14, 144, 18, 84, 24} ...
     - A = (5, 23, 29, 83, 89, 149) 	 et B = {74, 108, 14, 144, 18, 84, 24} ...
     - A = (5, 23, 29, 83, 89, 149) 	 et B = {74, 108, 14, 144, 18, 84, 24} ...
     - A = (5, 23, 29, 83, 89, 149) 	 et B = {74, 108, 14, 144, 18, 84, 24} ...
     - A = (5, 23, 29, 83, 89, 149) 	 et B = {74, 108, 14, 144, 18, 84, 24} ...
     - A = (5, 23, 29, 83, 89, 149) 	 et B = {74, 108, 14, 144, 18, 84, 24} ...
     - A = (5, 23, 29, 83, 89, 149) 	 et B = {74, 108, 14, 144, 18, 84, 24} ...
     - A = (5, 23, 29, 89, 113, 149) 	 et B = {78, 14, 144, 18, 84, 24} ...
     - A = (5, 23, 53, 83, 89, 149) 	 et B = {74, 14, 144, 48, 18, 84} ...
     - A = (5, 29, 41, 59, 71, 89) 	 et B = {68, 38, 42, 108, 12, 122} ...
     - A = (7, 11, 17, 31, 67, 101) 	 et B = {96, 36, 6, 72, 12, 30} ...
     - A = (7, 11, 17, 41, 67, 101) 	 et B = {96, 6, 72, 12, 90, 30} ...
     - A = (7, 13, 19, 37, 79, 97) 	 et B = {34, 4, 10, 144, 60, 94} ...
     - A = (7, 13, 19, 37, 79, 103) 	 et B = {34, 4, 10, 24, 60, 94} ...
     - A = (7, 13, 19, 37, 79, 139) 	 et B = {34, 10, 144, 24, 60, 94} ...
     - A = (7, 13, 19, 37, 103, 139) 	 et B = {34, 10, 24, 90, 60, 94} ...
     - A = (7, 13, 19, 73, 103, 139) 	 et B = {34, 10, 54, 24, 90, 94} ...
     - A = (7, 13, 23, 37, 47, 107) 	 et B = {66, 6, 144, 24, 90, 60} ...
     - A = (7, 13, 23, 37, 83, 107) 	 et B = {66, 6, 144, 24, 90, 30} ...
     - A = (7, 13, 31, 37, 73, 97) 	 et B = {66, 100, 6, 10, 76, 16, 30} ...
     - A = (7, 13, 31, 37, 73, 97) 	 et B = {66, 100, 6, 10, 76, 16, 30} ...
     - A = (7, 13, 31, 37, 73, 97) 	 et B = {66, 100, 6, 10, 76, 16, 30} ...
     - A = (7, 13, 31, 37, 73, 97) 	 et B = {66, 100, 6, 10, 76, 16, 30} ...
     - A = (7, 13, 31, 37, 73, 97) 	 et B = {66, 100, 6, 10, 76, 16, 30} ...
     - A = (7, 13, 31, 37, 73, 97) 	 et B = {66, 100, 6, 10, 76, 16, 30} ...
     - A = (7, 13, 31, 37, 73, 97) 	 et B = {66, 100, 6, 10, 76, 16, 30} ...
     - A = (7, 13, 37, 67, 79, 97) 	 et B = {34, 100, 4, 144, 60, 30} ...
     - A = (7, 13, 37, 79, 97, 139) 	 et B = {34, 100, 10, 144, 60, 94} ...
     - A = (7, 17, 31, 41, 67, 101) 	 et B = {96, 132, 6, 72, 12, 30} ...
     - A = (7, 17, 31, 41, 97, 101) 	 et B = {96, 66, 132, 6, 12, 30} ...
     - A = (7, 19, 37, 43, 79, 103) 	 et B = {4, 10, 60, 24, 120, 94} ...
     - A = (7, 19, 37, 79, 97, 139) 	 et B = {34, 10, 144, 52, 60, 94} ...
     - A = (11, 13, 41, 53, 103, 131) 	 et B = {96, 6, 138, 48, 60, 126} ...
     - A = (11, 17, 41, 53, 71, 137) 	 et B = {96, 140, 86, 56, 26, 30} ...
     - A = (11, 17, 41, 53, 83, 101) 	 et B = {96, 6, 140, 56, 26, 30} ...
     - A = (11, 17, 41, 53, 83, 137) 	 et B = {96, 140, 20, 56, 26, 30} ...
     - A = (11, 17, 41, 71, 101, 137) 	 et B = {96, 2, 140, 12, 56, 26, 30} ...
     - A = (11, 17, 41, 71, 101, 137) 	 et B = {96, 2, 140, 12, 56, 26, 30} ...
     - A = (11, 17, 41, 71, 101, 137) 	 et B = {96, 2, 140, 12, 56, 26, 30} ...
     - A = (11, 17, 41, 71, 101, 137) 	 et B = {96, 2, 140, 12, 56, 26, 30} ...
     - A = (11, 17, 41, 71, 101, 137) 	 et B = {96, 2, 140, 12, 56, 26, 30} ...
     - A = (11, 17, 41, 71, 101, 137) 	 et B = {96, 2, 140, 12, 56, 26, 30} ...
     - A = (11, 17, 41, 71, 101, 137) 	 et B = {96, 2, 140, 12, 56, 26, 30} ...
     - A = (11, 17, 41, 83, 101, 137) 	 et B = {96, 90, 140, 56, 26, 30} ...
     - A = (11, 17, 53, 71, 101, 137) 	 et B = {96, 36, 140, 56, 26, 30} ...
     - A = (11, 17, 59, 107, 137, 149) 	 et B = {2, 42, 120, 90, 92, 30} ...
     - A = (11, 23, 41, 53, 71, 137) 	 et B = {140, 86, 30, 56, 60, 126} ...
     - A = (11, 23, 41, 53, 107, 137) 	 et B = {20, 86, 30, 56, 60, 126} ...
     - A = (11, 37, 47, 71, 131, 137) 	 et B = {36, 102, 42, 120, 60, 126} ...
     - A = (11, 41, 53, 71, 101, 137) 	 et B = {96, 140, 30, 56, 26, 126} ...
     - A = (11, 41, 53, 83, 101, 131) 	 et B = {96, 98, 6, 140, 48, 26} ...
     - A = (11, 41, 67, 71, 97, 137) 	 et B = {96, 42, 12, 126, 60, 30} ...
     - A = (13, 17, 23, 73, 83, 107) 	 et B = {66, 6, 84, 24, 90, 30} ...
     - A = (13, 17, 43, 83, 97, 127) 	 et B = {96, 66, 114, 84, 54, 30} ...
     - A = (13, 19, 23, 83, 89, 149) 	 et B = {144, 48, 18, 84, 24, 90} ...
     - A = (13, 19, 29, 43, 113, 139) 	 et B = {138, 18, 84, 54, 24, 60} ...
     - A = (13, 19, 29, 53, 113, 139) 	 et B = {138, 144, 18, 84, 54, 60} ...
     - A = (13, 19, 29, 89, 113, 139) 	 et B = {138, 144, 18, 84, 24, 60} ...
     - A = (13, 19, 37, 43, 79, 103) 	 et B = {4, 70, 10, 24, 60, 94} ...
     - A = (13, 19, 37, 79, 97, 103) 	 et B = {34, 4, 70, 10, 60, 94} ...
     - A = (13, 19, 43, 73, 103, 139) 	 et B = {10, 138, 54, 24, 28, 94} ...
     - A = (13, 19, 43, 79, 103, 139) 	 et B = {10, 60, 24, 88, 28, 94} ...
     - A = (13, 23, 37, 53, 67, 97) 	 et B = {6, 144, 114, 30, 60, 126} ...
     - A = (13, 23, 53, 67, 97, 107) 	 et B = {6, 144, 84, 30, 60, 126} ...
     - A = (13, 23, 53, 79, 83, 149) 	 et B = {144, 48, 18, 114, 84, 30} ...
     - A = (13, 31, 43, 61, 97, 127) 	 et B = {96, 66, 70, 40, 136, 10} ...
     - A = (13, 37, 67, 79, 97, 127) 	 et B = {100, 4, 70, 144, 114, 30} ...
     - A = (13, 43, 67, 97, 109, 127) 	 et B = {4, 70, 40, 114, 84, 30} ...
     - A = (17, 23, 53, 59, 83, 137) 	 et B = {140, 44, 14, 114, 20, 30} ...
     - A = (17, 29, 59, 107, 137, 149) 	 et B = {2, 134, 42, 44, 120, 30} ...
     - A = (17, 41, 53, 71, 83, 101) 	 et B = {96, 140, 110, 56, 26, 30} ...
     - A = (17, 47, 59, 89, 107, 149) 	 et B = {134, 42, 50, 24, 90, 92} ...
     - A = (17, 47, 59, 107, 137, 149) 	 et B = {132, 134, 42, 120, 90, 92} ...
     - A = (19, 23, 29, 79, 89, 113) 	 et B = {78, 144, 18, 84, 24, 60} ...
     - A = (23, 29, 53, 83, 113, 149) 	 et B = {128, 44, 14, 144, 18, 84} ...
     - A = (23, 29, 53, 89, 113, 149) 	 et B = {78, 14, 144, 18, 50, 84} ...

    Pour n = 6 et M = 150 il y a 109 paires (A, B) trouvées valides ...
    - On recherche celle de somme sum(A u B) minimale ...
    A = [7, 13, 31, 37, 73, 97]
    B = [6, 10, 16, 30, 66, 76]
      Elle donne sumsum = 462 et maxmax = 97
    - On recherche celle de maximum max(A u B) minimale ...
    A = [7, 13, 31, 37, 73, 97]
    B = [6, 10, 16, 30, 66, 76]
      Elle donne sumsum = 462 et maxmax = 97
    - On recherche celle de moyenne sumsum(A,B), maxmax(A,B) minimale ...
    A = [7, 13, 31, 37, 73, 97]
    B = [6, 10, 16, 30, 66, 76]
      Elle donne sumsum = 462 et maxmax = 97
    CPU times: user 2.43 s, sys: 12 ms, total: 2.44 s
    Wall time: 2.41 s

    ([7, 13, 31, 37, 73, 97], [6, 10, 16, 30, 66, 76])

```python
A, B = test_trie_et_prend_min(n, M)
verifie_paire_debug(A, B)
```

    Pour n = 6 et M = 150 il y a 109 paires (A, B) trouvées valides ...
    - On recherche celle de somme sum(A u B) minimale ...
    A = [7, 13, 31, 37, 73, 97]
    B = [6, 10, 16, 30, 66, 76]
      Elle donne sumsum = 462 et maxmax = 97
    - On recherche celle de maximum max(A u B) minimale ...
    A = [7, 13, 31, 37, 73, 97]
    B = [6, 10, 16, 30, 66, 76]
      Elle donne sumsum = 462 et maxmax = 97
    - On recherche celle de moyenne sumsum(A,B), maxmax(A,B) minimale ...
    A = [7, 13, 31, 37, 73, 97]
    B = [6, 10, 16, 30, 66, 76]
      Elle donne sumsum = 462 et maxmax = 97
      - a = 7      + b = 6      donnent 13     qui est bien premier, OK ...
      - a = 7      + b = 10     donnent 17     qui est bien premier, OK ...
      - a = 7      + b = 16     donnent 23     qui est bien premier, OK ...
      - a = 7      + b = 30     donnent 37     qui est bien premier, OK ...
      - a = 7      + b = 66     donnent 73     qui est bien premier, OK ...
      - a = 7      + b = 76     donnent 83     qui est bien premier, OK ...
      - a = 13     + b = 6      donnent 19     qui est bien premier, OK ...
      - a = 13     + b = 10     donnent 23     qui est bien premier, OK ...
      - a = 13     + b = 16     donnent 29     qui est bien premier, OK ...
      - a = 13     + b = 30     donnent 43     qui est bien premier, OK ...
      - a = 13     + b = 66     donnent 79     qui est bien premier, OK ...
      - a = 13     + b = 76     donnent 89     qui est bien premier, OK ...
      - a = 31     + b = 6      donnent 37     qui est bien premier, OK ...
      - a = 31     + b = 10     donnent 41     qui est bien premier, OK ...
      - a = 31     + b = 16     donnent 47     qui est bien premier, OK ...
      - a = 31     + b = 30     donnent 61     qui est bien premier, OK ...
      - a = 31     + b = 66     donnent 97     qui est bien premier, OK ...
      - a = 31     + b = 76     donnent 107    qui est bien premier, OK ...
      - a = 37     + b = 6      donnent 43     qui est bien premier, OK ...
      - a = 37     + b = 10     donnent 47     qui est bien premier, OK ...
      - a = 37     + b = 16     donnent 53     qui est bien premier, OK ...
      - a = 37     + b = 30     donnent 67     qui est bien premier, OK ...
      - a = 37     + b = 66     donnent 103    qui est bien premier, OK ...
      - a = 37     + b = 76     donnent 113    qui est bien premier, OK ...
      - a = 73     + b = 6      donnent 79     qui est bien premier, OK ...
      - a = 73     + b = 10     donnent 83     qui est bien premier, OK ...
      - a = 73     + b = 16     donnent 89     qui est bien premier, OK ...
      - a = 73     + b = 30     donnent 103    qui est bien premier, OK ...
      - a = 73     + b = 66     donnent 139    qui est bien premier, OK ...
      - a = 73     + b = 76     donnent 149    qui est bien premier, OK ...
      - a = 97     + b = 6      donnent 103    qui est bien premier, OK ...
      - a = 97     + b = 10     donnent 107    qui est bien premier, OK ...
      - a = 97     + b = 16     donnent 113    qui est bien premier, OK ...
      - a = 97     + b = 30     donnent 127    qui est bien premier, OK ...
      - a = 97     + b = 66     donnent 163    qui est bien premier, OK ...
      - a = 97     + b = 76     donnent 173    qui est bien premier, OK ...

    True

## Conclusion pour des dés à $6$ faces
Avec $n = 6$ dés, la plus petite paire de valeurs $(A, B)$ trouvée est
- A = $\{7, 13, 31, 37, 73, 97\}$
- B = $\{6, 10, 16, 30, 66, 76\}$

Elle donne $\sum A \cup B = 462$ et $\max A \cup B = 97$.

Assez surprenament, c'est la paire qui minimise les deux critères à la fois (et l'unique paire qui satisfait cette propriété).

---
## Avec plus de faces ?

### Exemples avec des dés à 8, 10, 12 et 20 faces
> Ça va commencer à être *très couteux*. Je recommande de ne pas faire tourner ce code sur votre machine, mais sur un serveur de calcul, par exemple !

```python
n = 7
M = 170
%time test_genere_AB(n=n, M=M)
%time test_trie_et_prend_min(n, M)
```

     - A = (3, 7, 13, 37, 73, 97, 157) 	 et B = {160, 34, 100, 10, 76, 16, 94} ...
    A = [3, 7, 13, 37, 73, 97, 157]
    B = [160, 34, 100, 10, 76, 16, 94]
    CPU times: user 704 ms, sys: 12 ms, total: 716 ms
    Wall time: 713 ms
     - A = (3, 7, 13, 37, 73, 97, 157) 	 et B = {160, 34, 100, 10, 76, 16, 94} ...
     - A = (3, 13, 19, 37, 79, 97, 103) 	 et B = {160, 34, 4, 70, 10, 154, 94} ...
     - A = (3, 13, 37, 73, 97, 157, 163) 	 et B = {34, 100, 10, 76, 16, 154, 94} ...
     - A = (7, 13, 31, 37, 73, 97, 157) 	 et B = {160, 66, 100, 6, 10, 76, 16} ...
     - A = (7, 13, 31, 67, 73, 97, 151) 	 et B = {160, 100, 166, 6, 40, 16, 30} ...
     - A = (7, 13, 37, 73, 97, 157, 163) 	 et B = {34, 66, 100, 10, 76, 16, 94} ...
     - A = (13, 17, 23, 73, 83, 107, 167) 	 et B = {66, 6, 84, 150, 24, 90, 30} ...
     - A = (13, 19, 37, 79, 97, 103, 163) 	 et B = {34, 4, 70, 10, 154, 60, 94} ...

    Pour n = 7 et M = 170 il y a 8 paires (A, B) trouvées valides ...
    - On recherche celle de somme sum(A u B) minimale ...
    A = [7, 13, 31, 37, 73, 97, 157]
    B = [6, 10, 16, 66, 76, 100, 160]
      Elle donne sumsum = 849 et maxmax = 160
    - On recherche celle de maximum max(A u B) minimale ...
    A = [3, 7, 13, 37, 73, 97, 157]
    B = [10, 16, 34, 76, 94, 100, 160]
      Elle donne sumsum = 877 et maxmax = 160
    - On recherche celle de moyenne sumsum(A,B), maxmax(A,B) minimale ...
    A = [7, 13, 31, 37, 73, 97, 157]
    B = [6, 10, 16, 66, 76, 100, 160]
      Elle donne sumsum = 849 et maxmax = 160
    CPU times: user 21.9 s, sys: 8 ms, total: 21.9 s
    Wall time: 21.9 s

    ([7, 13, 31, 37, 73, 97, 157], [6, 10, 16, 66, 76, 100, 160])

#### Avec 8 faces

```python
n = 8
M = 440
%time test_genere_AB(n=n, M=M)
# %time test_trie_et_prend_min(n, M)
```

#### Avec 10 faces

```python
n = 10
M = 550
%time test_genere_AB(n=n, M=M)
# %time test_trie_et_prend_min(n, M)
```

#### Avec 12 faces

```python
n = 12
M = 800
%time test_genere_AB(n=n, M=M)
# %time test_trie_et_prend_min(n, M)
```

#### Avec 20 faces

```python
n = 20
M = 1000
%time test_genere_AB(n=n, M=M)
# %time test_trie_et_prend_min(n, M)
```

### Exemples avec des dés de tailles différentes
> Il ne serait pas très compliqué d'adapter le code précédent pour considérer $A$ de taille $n$ et $B$ de taille $p$ avec possiblement $n \neq p$.
> Je le ferai, plus tard.

### Exemples avec $100$ faces ?
> Il faudrait une machine costaux, et du temps...

----
# Généralisations possibles
- Dés avec un nombre de faces différentes, i.e., $\# A \neq \# B$,
- Plus de deux dés, i.e., considérer $A_1, \dots, A_p$ et chercher la même propriété :
  $$ \forall a_1 \in A_1, \dots, a_p \in A_p, a_1 + \dots + a_p \in \mathcal{P}. $$
  En imposant toujours que $A_1 \subset \mathcal{P}$, et éventuellement plus.
- Résoudre autrement le problème initial pour ne pas avoir à donner une borné $M$ sur $\max A, B$.


## Equação de Bateman-Burgers

* A equação de Burgers é o modelo mais simples para analisar o efeito combinado de convecção e difusão não linear

\begin{equation}
\frac{\partial u}{\partial t} + u \frac{\partial u}{\partial x} = \nu \frac{\partial^2 u}{\partial x^2}.
\end{equation}

* No limite onde a viscosidade tende a zero, o termo de difusão se torna cada vez menor e as soluções suaves convergem de forma não uniforme para a onda de choque descontínua apropriada.

* A equação de convecção propaga uma forma de onda ou sinal na velocidade $c$

\begin{equation}
\frac{\partial u}{\partial t} + c \frac{\partial u}{\partial x} = 0.
\end{equation}

* Por outro lado, a equação não linear propaga sinais de tal forma que ocorre distorção no perfil da forma de onda

\begin{equation}
\frac{\partial u}{\partial t} + u \frac{\partial u}{\partial x} = 0.
\end{equation}

* É sabido também que a equação do calor contém um termo de difusão responsável por suavizar o perfil inicial

\begin{equation}
\frac{\partial u}{\partial t} - \alpha \frac{\partial^2 u}{\partial x^2} = 0,
\end{equation}

onde $ \alpha > 0$.

# Equação de Navier Stokes

A equação de Burgers é considerada uma simplificação de outra equação mais complexa, a equação de Navier Stokes, uma EDP usada para descrever o movimento de substâncias fluidas como líquidos e gases.

Considere as equações a seguir:

$\left\{\begin{array}{l}\nabla \cdot \mathbf{v}=0 \\ (\rho \mathbf{v})_{t}+\nabla \cdot(\rho \mathbf{v v})+\nabla p-\mu \nabla^{2} \mathbf{v}=0\end{array}\right.$

Onde $\rho$ é a densidade, $p$ é a pressão, $v$ é a velocidade e $\mu$ é a viscosidade de um fluido.
Se simplificarmos a equação usando somente o componente $x$ do vetor velocidade, que chamaremos de $v^{x}$, ficaremos com:

$\rho \cfrac{\partial v^{x}}{\partial t}+\rho v^{x} \cfrac{\partial v^{x}}{\partial x}+\rho v^{y} \cfrac{\partial v^{x}}{\partial y}+\rho v^{z} \cfrac{\partial v^{x}}{\partial z}+\cfrac{\partial p}{\partial x}-\mu\left(\cfrac{\partial^{2} v^{x}}{\partial x^{2}}+\cfrac{\partial^{2} v^{x}}{\partial y^{2}}+\cfrac{\partial^{2} v^{x}}{\partial z^{2}}\right)=0$

Agora, se considerarmos um problema com somente uma dimensão:

$\rho \cfrac{\partial v^{x}}{\partial t}+\rho v^{x} \cfrac{\partial v^{x}}{\partial x}-\mu \cfrac{\partial^{2} v^{x}}{\partial x^{2}}=0$

Finalmente, se substituirmos $v^{x}$ por $u$, já que o problema é unidimensional, e usar a relação $\epsilon=\cfrac{\mu}{\rho}$ da viscosidade cinemática, ficaremos com a equação discutida.

## Transformação Hopf-Cole

A transformação Hopf-Cole é usada para tornar a equação de Burgers, que é não linear, em uma versão da equação do calor, que é linear.

$\cfrac{\partial}{\partial t} \psi(\mathbf{x}, t)=v \Delta \psi(\mathbf{x}, t)$

$\psi(\mathbf{x}, t)=e^{h(\mathbf{x}, t) / 2 v}$

$\Delta \psi=\cfrac{1}{2 v}\left[\Delta h+\cfrac{1}{2 v}(\nabla h)^{2}\right] e^{h / 2 v}$

$\cfrac{\partial}{\partial t} h-\cfrac{1}{2}(\nabla h)^{2}=v \Delta h$

Nesta última equação, percebe-se que ela se iguala à equação de Kardar-Parisi-Zhang, que é um modelo para uma superfície sólida crescendo por deposição de vapor, ou, o caso oposto, erosão de material de uma superfície sólida:

$\cfrac{\partial}{\partial t} h(\mathbf{x}, t)-\cfrac{1}{2}(\nabla h(\mathbf{x}, t))^{2}=v \cfrac{\partial^{2}}{\partial x^{2}} h(x, t)+F(x, t)$

onde

$\mathbf{u}(\mathbf{x}, t)=-\nabla h(\mathbf{x}, t)$

Combinando esses conhecimentos, se obtêm:

$\mathbf{u}(\mathbf{x}, t)=-\cfrac{1}{2 v} \nabla \ln \psi(\mathbf{x}, t)$

### **Solução geral em 1D**

Condições iniciais:

$u(x, 0), \quad \psi(x, 0)=e^{-\cfrac{1}{2 v} \displaystyle\int^{x} d x^{\prime} u\left(x^{\prime}, 0\right)}$

Equação do calor:

$\psi(x, t)=\displaystyle\int d x^{\prime} G\left(x-x^{\prime}, t\right) \psi\left(x^{\prime}, 0\right)$

$G\left(x-x^{\prime}, t\right)=\cfrac{1}{\sqrt{4 \pi t}} e^{-\cfrac{\left(x-x^{\prime}\right)^{2}}{4 v t}}$

Solução em termos da condição inicial:

$\psi(x, t)=\cfrac{1}{\sqrt{4 \pi t}} \displaystyle\int d x^{\prime} e^{-\cfrac{\left(x-x^{\prime}\right)^{2}}{4 v t}-\cfrac{1}{2 v} \displaystyle\int^{x^{\prime}} d x^{\prime \prime} u\left(x^{\prime \prime}, 0\right)}$

## Método Numérico

### Discretização

$\cfrac{\partial{u}}{\partial{t}}+u\cfrac{\partial{u}}{\partial{x}}=\nu\cfrac{\partial^{2}{u}}{\partial{x^{2}}}$

#### FTCS (_**F**oward **T**ime **C**entral **S**pace_):

Sabe-se que, (utilizando a derivada à direita)

$\cfrac{\partial}{\partial{t}}u(x,t) = \cfrac{u(x,t+\Delta t) - u(x,t)}{\Delta t} +\mathcal{O}(\Delta t^{2})$

$\cfrac{\partial^{2}}{\partial{x^{2}}}u(x,t) = \cfrac{u(x+\Delta x, t) - 2u(x,t) + u(x-\Delta x,t)}{(\Delta t)^{2}} +\mathcal{O}(\Delta x^{3})$

Se,

$u(x, t) := u_{i}^{n}$

$u(x, t+\Delta t) := u_{i}^{n+1}$

$u(x+\Delta x, t) := u_{i+1}^{n}$

$\implies \cfrac{\partial{u}}{\partial{t}}+u\cfrac{\partial{u}}{\partial{x}}=\nu\cfrac{\partial^{2}{u}}{\partial{x^{2}}} \longrightarrow \cfrac{u_{i}^{n+1}-u_{i}^{n}}{\Delta t} + u_{i}^{n}\cfrac{u_{i}^{n}-u_{i-1}^{n}}{\Delta x} = \nu\cfrac{u_{i+1}^{n}-2u_{i}^{n}+u_{i-1}^{n}}{(\Delta x)^{2}}$

$\longrightarrow u_{i}^{n+1} = u_{i}^{n} - u_{i}^{n}\cfrac{\Delta t}{\Delta x}(u_{i}^{n}-u_{i-1}^{n})+\nu\cfrac{\Delta t}{(\Delta x)^{2}}(u_{i+1}^{n}-2u_{i}^{n}+u_{i-1}^{n})$

#### Supondo que

* **Condições Iniciais (CI)**:

$
\left\{
    \begin{array}\\
        u_{i}^{n=0} = \cfrac{-2\nu}{\phi(x,\nu)}\left(\cfrac{\partial\phi(x,\nu)}{\partial x}\right)+4 \\
        \phi = e^{\left(\cfrac{-x^{2}}{4\nu}\right)} + e^{\left[\cfrac{-(x-2\pi)^{2}}{4\nu}\right]}
    \end{array}
\right.
$

Derivando $\phi$ e substituindo na equação:

$\implies u_{i}^{n=0} = -\cfrac{x}{2\nu}e^{\left(\cfrac{-x^{2}}{4\nu}\right)}-\cfrac{(x-2\pi)}{2\nu}e^{\left[\cfrac{-(x-2\pi)^{2}}{4\nu}\right]}$

* **Condições de Contorno (CC)**:

Periódica:

$u_{i=0}^{n}=u_{i_{max}}^{n}$

#### Solução analítica:

$
\left\{
    \begin{array}\\
        u_{i}^{n} = \cfrac{-2\nu}{\phi(x,\nu)}\left(\cfrac{\partial\phi(x,\nu)}{\partial x}\right)+4 \\
        \phi=e^{\left(\cfrac{-(x-4 t)^{2}}{4 \nu(t+1)}\right)}+e^{\left[\cfrac{-(x-4 t-2 \pi)^{2}}{4 \nu(t+1)}\right]}
    \end{array}
\right.
$

Derivando $\phi$ e substituindo na equação:

$\implies u_{i}^{n} = -\cfrac{(x-4t)}{2\nu(t+1)}e^{-\left(\cfrac{(x-4t)^{2}}{4\nu(t+1)}\right)}-\cfrac{(x-4t-2\pi)}{2\nu(t+1)}e^{\left[\cfrac{-(x-4t-2\pi)^{2}}{4\nu(t+1)}\right]}$

## Código - Fluido víscido ($\nu\neq0$)
##### *(com viscosidade)*

### Burgers Numérico

```python
from math import pi, exp
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm

Array2D = np.ndarray # typing...

def burgers(nt:int, nx:int, tmax:float, xmax:float, v:float) -> tuple:
    # incrementos
    dt = tmax/(nt-1) # no tempo
    dx = xmax/(nx-1) # no espaco

    # arrays
    u = np.zeros((nx, nt)) # velocidade
    x = np.zeros(nx)
    ipos = np.zeros(nx)
    ineg = np.zeros(nx)

    # condicoes de contorno periodicas
    for i in range(0, nx): # i:     0,  1, ... nx-2, nx-1, nx
        x[i] = i*dx
        ipos[i] = i+1 # 1, 2, ... nx-2, nx+1
        ineg[i] = i-1 # -1, 0, ... nx-2, nx-1

    ipos[-1] = 0 # ipos[nx-1] = 0, u_{n+1}
    ineg[0] = nx-1 # u_{n-1}

    # condicoes iniciais
    for i in range(0, nx):
        phi = exp( -(x[i]**2)/(4*v) ) + exp( -(x[i]-2*pi)**2 / (4*v) )
        dphi = (-(0.5*x[i]/v)*exp( -(x[i]**2) / (4*v) ) - (0.5*(x[i]-2*pi) / v )
                *exp(-(x[i]-2*pi)**2 / (4*v) ))
        u[i,0] = -2*v*(dphi/phi) + 4

    # solucao numerica
    for n in range(0, nt-1):
        for i in range(0,nx):
            u[i,n+1] = (u[i,n]-u[i,n]*(dt/dx)*(u[i,n]-u[int(ineg[i]),n])+
                        v*(dt/dx**2)*(u[int(ipos[i]),n]-2*u[i,n]+u[int(ineg[i]),n]))
    return((u, x))
```

### Burgers Analítico

```python
def burgers_analytical(nt:int, nx:int, tmax:float, xmax:float, v:float) -> tuple:
    # incrementos
    dt = tmax/(nt-1)
    dx = xmax/(nx-1)

    # arrays
    u_a = np.zeros((nx, nt))
    x = np.zeros(nx)
    t = np.zeros(nt)

    # len(x)
    for i in range(0, nx):
        x[i] = i*dx

    # solucao analitica
    for n in range(0, nt):
        t = n*dt

        for i in range(0, nx):
            phi = (exp( -(x[i]-4*t)**2/(4*v*(t+1)) ) +
                   exp( -(x[i]-4*t-2*pi)**2/(4*v*(t+1)) ))

            dphi = ( -0.5*(x[i]-4*t)/(v*(t+1))*exp( -(x[i]-4*t)**2/(4*v*(t+1)) )
                -0.5*(x[i]-4*t-2*pi)/(v*(t+1))*exp( -(x[i]-4*t-2*pi)**2/(4*v*(t+1)) ) )

            u_a[i,n] = -2*v*(dphi/phi) + 4

    return((u_a, x))
```

#### Gráfico de $u$ e $u_{a}$ *vs.* $x$

```python
def plot(u_a:Array2D, u:Array2D, x:list, nt:int, title:str):
    plt.figure(figsize=(8, 6), dpi=80)
    colour=iter(cm.rainbow(np.linspace(0, 20, nt)))
    for n in range(0, nt, 20):
        c=next(colour)
        plt.plot(x, u[:, n], 'ko', markerfacecolor=c, alpha=0.5)
        plt.plot(x, u_a[:, n], linestyle='-', c=c, label=f'i={n}')
    plt.legend()
    plt.xlabel(r'$x$')
    plt.ylabel(r'$u$')
    plt.ylim([0, 8.0])
    plt.xlim([0, max(x)])
    plt.title(title)
    plt.show()
```

#### Gráfico de $u_{a}$ *vs.* $x$

```python
def plot_analytical(u_a:Array2D, x:list, nt:int, title:str) -> None:
    plt.figure(figsize=(8, 6), dpi=80)
    colour=iter(cm.rainbow(np.linspace(0, 20, nt)))
    for n in range(0, nt, 20):
        c=next(colour)
        plt.plot(x, u_a[:, n], linestyle='-', c=c, label=f'i={n}')
    plt.legend()
    plt.xlabel(r'$x$')
    plt.ylabel(r'$u$')
    plt.ylim([0, 8.0])
    plt.xlim([0, max(x)])
    plt.title(title)
    plt.show()
```

#### Erro numérico $\left|u-u_{a}\right|$ *vs.* $x$
##### *(log)*

```python
def plot_error(u_a:Array2D, u:Array2D, x:list, nt:int) -> None:
    plt.figure(figsize=(8, 6), dpi=80)
    colour=iter(cm.rainbow(np.linspace(0,20,nt)))
    for n in range(0,nt,20):
        c = next(colour)
        err = abs(u[:, n]-u_a[:, n])
        plt.plot(x, err, 'ko', color=c, markersize=3,
                 label=f'i={n}, max={max(err):.4f}, M={np.sum(err)/len(err):.4f}')
    plt.legend()
    plt.xlabel(r'$x$')
    plt.ylabel(r'$\|u-u_{a}\|$')
    plt.xlim([0, max(x)])
    plt.yscale('log')
    plt.ylim([10e-6, 10e2])
    plt.title('Erro Numérico')
    plt.show()
```

* $\nu=0.1, \,\,\, n_{t}=151, \,\,\, n_{x}=151, \,\,\, t_{max}=0.5, \,\,\, x_{max}=2\pi$

```python
nt, nx, tmax, xmax, v = 151, 151, 0.5, 2*pi, 0.1
```

* $\nu=0.01, \,\,\, n_{t}=151, \,\,\, n_{x}=151, \,\,\, t_{max}=0.5, \,\,\, x_{max}=2\pi$

```python
nt, nx, tmax, xmax, v = 151, 151, 0.5, 2*pi, 0.01
```

* $\nu=0.15, \,\,\, n_{t}=151, \,\,\, n_{x}=151, \,\,\, t_{max}=0.5, \,\,\, x_{max}=2\pi$

```python
nt, nx, tmax, xmax, v = 151, 151, 0.5, 2*pi, 0.15
```

* $\nu=0.1, \,\,\, n_{t}=201, \,\,\, n_{x}=201, \,\,\, t_{max}=0.5, \,\,\, x_{max}=3\pi$

```python
nt, nx, tmax, xmax, v = 201, 201, 0.5, 3*pi, 0.9
```

---

```python
u, x = burgers(nt, nx, tmax, xmax, v)
u_a, x = burgers_analytical(nt, nx, tmax, xmax, v)
title = str(r'$\nu=$'+f'{v}, '+r'$n_t$'+f'={nt}, '+
     r'$n_x$'+f'={nx}, '+r'$t_{max}$'+f'={tmax}')
plot(u_a, u, x, nt, title)
plot_analytical(u_a, x, nt, title)
plot_error(u_a,u,x,nt)
```

---

## Fluido invíscido ($\nu=0$)
##### *(sem viscosidade)*

$\cfrac{\partial{u}}{\partial{t}}+u\cfrac{\partial{u}}{\partial{x}} = 0$

$\implies \cfrac{\partial{u}}{\partial{t}}+u\cfrac{\partial{u}}{\partial{x}} = 0 \longrightarrow \cfrac{u_{i}^{n+1}-u_{i}^{n}}{\Delta t} + u_{i}^{n}\cfrac{u_{i}^{n}-u_{i-1}^{n}}{\Delta x} = 0$

$\longrightarrow u_{i}^{n+1} = u_{i}^{n} - u_{i}^{n}\cfrac{\Delta t}{\Delta x}(u_{i}^{n}-u_{i-1}^{n})$

* Utilizando as mesmas condições:

### Código

```python
def burgers_inv(nt:int, nx:int, tmax:float, xmax:float, v) -> tuple:
    # incrementos
    dt = tmax/(nt-1)
    dx = xmax/(nx-1)

    # arrays
    u = np.zeros((nx,nt))
    x = np.zeros(nx)
    ipos = np.zeros(nx)
    ineg = np.zeros(nx)

    # condicoes de contorno periodicas
    for i in range(0,nx):
        x[i] = i*dx
        ipos[i] = i+1
        ineg[i] = i-1

    ipos[nx-1] = 0
    ineg[0] = nx-1

    # condicoes iniciais
    for i in range(0,nx):
        phi = exp( -(x[i]**2)/(4*v) ) + exp( -(x[i]-2*pi)**2 / (4*v) )
        dphi = -(0.5*x[i]/v)*exp( -(x[i]**2) / (4*v) ) - (0.5*(x[i]-2*pi) / v )*exp(-(x[i]-2*pi)**2 / (4*v) )
        u[i,0] = -2*v*(dphi/phi) + 4

    # solucao numerica
    for n in range(0,nt-1):
        for i in range(0,nx):
            u[i,n+1] = (u[i,n]-u[i,n]*(dt/dx)*(u[i,n]-u[int(ineg[i]),n]))
    return((u, x))
```

#### Gráfico de $u_{inv}$ e $u$ *vs.* $x$

```python
def plot_iv(u_inv:Array2D, u:Array2D, x:list, nt:int) -> None:
    plt.figure(figsize=(8, 6), dpi=80)
    plt.plot(x, u[:,0], color='red', label=r'$u$')
    plt.plot(x, u_inv[:,0], color='blue', label=r'$u_{inv}$')
    for n in range(1,nt,20):
        plt.plot(x, u[:,n], color='red')
        plt.plot(x, u_inv[:,n], color='blue')
    plt.legend()
    plt.xlabel(r'$x$')
    plt.ylabel(r'$u$')
    plt.xlim([0, max(x)])
    plt.title(r'$(u_{inv})$ '+'e '+r'$u$')
    plt.show()
```

---

```python
u_inv, x_inv = burgers_inv(nt, nx, tmax, xmax, v)
plot_analytical(u, x, nt, str(r'$\nu=$'+f'{v}, '+r'$n_t$'+f'={nt}, '+
                 r'$n_x$'+f'={nx}, '+r'$t_{max}$'+f'={tmax}'))
plot_analytical(u_inv, x_inv, nt, str(r'$\nu=$'+'0, '+r'$n_t$'+f'={nt}, '+
                 r'$n_x$'+f'={nx}, '+r'$t_{max}$'+f'={tmax}'))
plot_iv(u_inv, u, x, nt)
```

bar


# One and two electron Hamiltonian

This model is valid for a double-dot system tuned to the transition from (1,0) to (0,1) or with two electrons for (1,1) to (2,0).

Author: Pieter Eendebak (pieter.eendebak@gmail.com), Bruno Buijtendorp (brunobuijtendorp@gmail.com)

```python
import numpy as np
import matplotlib.pyplot as plt
import sympy as sp
%matplotlib inline
sp.init_printing(use_latex='latex')
```

## One electron Hamiltonian

Define 1-electron double dot Hamiltonian
e is detuning, $t$ is tunnel coupling. The basis we work in is (1,0) and (0,1).

```python
e, t = sp.symbols('e t')
H = sp.Matrix([[e/2, t],[t, -e/2]])
sp.pprint(H)

#%% Get normalized eigenvectors and eigenvalues
eigvec_min = H.eigenvects()[0][2][0].normalized()
eigval_min = H.eigenvects()[0][0]

eigvec_plus = H.eigenvects()[1][2][0].normalized()
eigval_plus = H.eigenvects()[1][0]

#%% Lambdify eigenvalues to make them numerical functions of e and t (nicer plotting)
eigval_min_func = sp.lambdify((e,t), eigval_min , 'numpy')
eigval_plus_func = sp.lambdify((e,t), eigval_plus, 'numpy')

#%% Plot energy levels
t_value = 1
plot_x_limit = 5
Npoints_x = 1000
```

    ⎡e     ⎤
    ⎢─   t ⎥
    ⎢2     ⎥
    ⎢      ⎥
    ⎢   -e ⎥
    ⎢t  ───⎥
    ⎣    2 ⎦

```python
erange = np.linspace(-plot_x_limit, plot_x_limit, Npoints_x)
levelfig, levelax = plt.subplots()
levelax.plot(erange, eigval_min_func(erange , t_value), label='$S-$')
levelax.plot(erange, eigval_plus_func(erange, t_value), label ='$S+$')
levelax.set_title('Energy levels for double-dot in one-electron regime, t = %.1f' % t_value)
plt.plot(erange, erange/2, ':c', label='avoided crossing')
plt.plot(erange, -erange/2, ':c')
plt.legend()
levelax.set_xlabel('detuning $(uev)$')
levelax.set_ylabel('energy $(ueV)$')
_=plt.axis('tight')
```

```python
#%% Plot energy level differences
SminS = eigval_plus_func(erange , t_value) - eigval_min_func(erange, t_value)

plt.figure()
plt.plot(erange, SminS, label='$E_{S_+} - E_{S_-}$')
plt.title('Energy transitions for double-dot in one-electron regime, t = %.1f $\mu eV$' % (t_value))
plt.legend()
plt.ylabel('$\Delta E$ $ (\mu eV)$')
plt.xlabel('$\epsilon$ $ (\mu eV)$')

#%% Get S(1,0) component of eigenvectors
eigcomp_min = eigvec_min[0]
eigcomp_plus = eigvec_plus[0]

#%% Plot S(1,0) components squared (probabilities) of eigenvectors as function of detuning
t_value = 1
erange = np.linspace(-20,20,500)
plot_x_limit = 20

# Lambdify eigenvector components to make them functions of e and t
eigcompmin_func = sp.lambdify((e,t), eigcomp_min , 'numpy')
eigcompplus_func = sp.lambdify((e,t), eigcomp_plus, 'numpy')

fig2, ax2 = plt.subplots()
ax2.plot(erange,eigcompmin_func(erange, t_value)**2, label='$S_-$')
ax2.plot(erange,eigcompplus_func(erange, t_value)**2, label='$S_+$')
ax2.set_xlabel('detuning,  ($\mu$eV)')
ax2.set_ylabel('(1,0) coefficient squared')
_=plt.legend()
```

## Two-electron Hamiltonian

Define 2-electron double dot Hamiltonian
e is detuning, t is tunnel coupling. The basis we work in is: {S(2,0), S(1,1), T(1,1)}

```python
e, t = sp.symbols('e t')
# Basis: {S(2,0), S(1,1), T(1,1)}
H = sp.Matrix([[e, sp.sqrt(2)*t, 0],[sp.sqrt(2)*t, 0, 0],[0, 0, 0]])

#%% Get normalized eigenvectors and eigenvalues
eigvec_min = H.eigenvects()[1][2][0].normalized()
eigval_min = H.eigenvects()[1][0]

eigvec_plus = H.eigenvects()[2][2][0].normalized()
eigval_plus = H.eigenvects()[2][0]

eigvec_T = H.eigenvects()[0][2][0].normalized()
eigval_T = H.eigenvects()[0][0]

#%% Lambdify eigenvalues to make them numerical functions of e and t (nicer plotting)
eigval_min_func = sp.lambdify((e,t), eigval_min , 'numpy')
eigval_plus_func = sp.lambdify((e,t), eigval_plus, 'numpy')

#%% Plot energy levels
t_value = 1
plot_x_limit = 5
Npoints_x = 1000

erange = np.linspace(-plot_x_limit, plot_x_limit, Npoints_x)
levelfig, levelax = plt.subplots()
levelax.plot(erange, [eigval_T]*len(erange), label='T(1,1)')
levelax.plot(erange, eigval_min_func(erange , t_value), label='$S_-$')
levelax.plot(erange, eigval_plus_func(erange, t_value), label ='$S_+$')
levelax.set_title('Energy levels for double-dot in two-electron regime, t = %.1f' % t_value)
plt.legend()
levelax.set_xlabel('detuning $(uev)$')
levelax.set_ylabel('energy $(ueV)$')
plt.axis('tight')

#%% Plot energy level differences
SminS = eigval_plus_func(erange , t_value) - eigval_min_func(erange, t_value)
S20minT = eigval_plus_func(erange, t_value)
TminS11 = -eigval_min_func(erange, t_value)

plt.figure()
plt.plot(erange, SminS, label='$E_{S_+} - E_{S_-}$')
plt.plot(erange, S20minT, label = '$E_{S_+} - E_T$')
plt.plot(erange, TminS11, label = '$E_T - E_{S_-}$')
plt.title('Energy transitions for double-dot in two-electron regime, t = %.1f $\mu eV$' % (t_value))
plt.legend()
plt.ylabel('$\Delta E$ $ (\mu eV)$')
plt.xlabel('$\epsilon$ $ (\mu eV)$')

#%% Get S(2,0) component of eigenvectors
eigcomp_min = eigvec_min[0]
eigcomp_plus = eigvec_plus[0]
eigcomp_T = eigvec_T[0]

#%% Plot S(2,0) components squared (probabilities) of eigenvectors as function of detuning
t_value = 1
erange = np.linspace(-20,20,500)
plot_x_limit = 20

# Lambdify eigenvector components to make them functions of e and t
eigcompmin_func = sp.lambdify((e,t), eigcomp_min , 'numpy')
eigcompplus_func = sp.lambdify((e,t), eigcomp_plus, 'numpy')

fig2, ax2 = plt.subplots()
ax2.plot(erange,eigcompmin_func(erange, t_value)**2, label='$S_-$')
ax2.plot(erange,eigcompplus_func(erange, t_value)**2, label='$S_+$')
ax2.plot(erange,[eigcomp_T]*len(erange), label='$T$')
ax2.set_xlabel('Detuning ($\mu$eV)')
ax2.set_ylabel('S(2,0) coefficient squared')
_=plt.legend()
```

```python

```


##  Taylor problem 5.2

last revised: 14-Jan-2019 by Dick Furnstahl [furnstahl.1@osu.edu]

The potential energy for two atoms in a molecule can sometimes be approximated by the Morse function

$\begin{align}
  U(r) = A\left[ \left(e^{(R-r)/S} -1\right)^2 -1 \right]
\end{align}$

for $0 < r < \infty$ with $A$, $R$, and $S$ all positive constants and $S \ll R$.

The goals are to find the equilibrium position $r_0$ and then the frequency of small oscillations.

Take the derivative and solve for the equilibrium $r_0$:

$\begin{align}
 \frac{dU}{dr} = 2A\left(e^{(R-r)/S} -1\right)\times e^{(R-r)/S} \times \frac{-1}{S} = 0
 \quad\Longrightarrow\quad
 r_0 = R
  \quad\Longrightarrow\quad
 U(r_0) = -A
\end{align}$

Expand around $r = r_0$ up to $(r - r_0)^2$ to find the small angle approximation equation and frequency:

$\begin{align}
U_{sa}(r) = A + \frac{A}{S^2} (r - r_0)^2
 \quad\Longrightarrow\quad
 k = \frac{2 A}{S^2}
 \quad\Longrightarrow\quad
 \omega = \sqrt{k/m} = \sqrt{\frac{2 A}{ m S^2}}
\end{align}$

## Make a plot to check $r_0$

```python
%matplotlib notebook
# Use notebook rather than inline so plot remains active between
#  cells and so we can zoom in.
```

```python
import numpy as np

import matplotlib.pyplot as plt
```

```python
def U_pot(r, lambda_0, big_R=1, U_0=1):
    """Potential from Taylor problem 5.13."""
    return U_0 * ((r/big_R) + (lambda_0**2) * (big_R/r))

def U_pot_min(lambda_0, big_R=1, U_0=1):
    """Returns the value of r at the minimum of U(r) and the value of
       U(r) at that point."""
    return lambda_0*big_R, U_0
```

```python
def U_pot_sa(r, lambda_0, big_R=1, U_0=1):
    """Plot the potential in the small angle approximation"""
    r0 = lambda_0*big_R
    return  2*U_0*lambda_0 + (1/2)*U_0*2*(r - r0)**2 / (lambda_0*(big_R)**2)

```

```python
r_pts = np.arange(0.01, 6., .001)
U_pot_1 = U_pot(r_pts, lambda_0=1)
U_pot_2 = U_pot(r_pts, lambda_0=2)
U_pot_3 = U_pot(r_pts, lambda_0=3)

fig = plt.figure(figsize=(4,4))
ax = fig.add_subplot(1,1,1)
ax.plot(r_pts, U_pot_1, label=r'$\lambda=1$', color='blue')
ax.plot(r_pts, U_pot_2, label=r'$\lambda=2$', color='red')
ax.plot(r_pts, U_pot_3, label=r'$\lambda=3$', color='green')

ax.set_xlim(0.,5.)
ax.set_ylim(-2.,10)

x0, y0 = U_pot_min(lambda_0=1)
ax.scatter(x0, y0, color='blue')
x0, y0 = U_pot_min(lambda_0=2)
ax.scatter(x0, y0, color='red')
x0, y0 = U_pot_min(lambda_0=3)
ax.scatter(x0, y0, color='green')

ax.set_xlabel('r')
ax.set_ylabel('U(r)')

ax.legend()
fig.tight_layout()
```

    <IPython.core.display.Javascript object>

Now put in small angle potentials (note that this changes the figure above, which is still the active figure (because we are using `%matplotlib notebook`).  Use the controls to zoom in to verify that the small-angle approximated $U(r)$ really does agree for $r$ close enough to the minimum.

```python
# "sa" here stands for "small-angle".
U_pot_sa_1 = U_pot_sa(r_pts, lambda_0=1)
U_pot_sa_2 = U_pot_sa(r_pts, lambda_0=2)
U_pot_sa_3 = U_pot_sa(r_pts, lambda_0=3)
ax.plot(r_pts, U_pot_sa_1, color='blue', ls='--')
ax.plot(r_pts, U_pot_sa_2, color='red', ls='--')
ax.plot(r_pts, U_pot_sa_3, color='green', ls='--')

```

    [<matplotlib.lines.Line2D at 0x120120908>]

Can we solve for exact and small angle oscillations numerically?

```python

```


# Sympy and PhysicalQuantities

```python
%load_ext PhysicalQuantities.ipython
```

```python
from sympy import *
init_printing()
```

```python
a = 1 m * Matrix([ [1,2,3], [4,5,6]])
b = 1 cm * Matrix([ [1,2,3], [4,5,6]])
```

```python
a
```

$\left[\begin{matrix}1 & 2 & 3\\4 & 5 & 6\end{matrix}\right]$ $\text{m}$

```python
a+b
```

$\left[\begin{matrix}1.01 & 2.02 & 3.03\\4.04 & 5.05 & 6.06\end{matrix}\right]$ $\text{m}$

```python
x = Symbol('x')
```

```python
1 m * x
```

$x$ $\text{m}$

```python
a = 2 m * x
a
```

$2 x$ $\text{m}$

```python
a/3
```

$\frac{2 x}{3}$ $\text{m}$

```python
Integral(sqrt(1/x), x)
```

```python

```


```python
from sympy import Symbol, FiniteSet, sympify
from jupyter_math.main import TallBrace, Event, EventSet, SumOverFiniteSet, Probability, Intermediate
from jupyter_math import Def, Pr, EXPAND_OUTER_SUM, SUBSTITUTE_ALL, GET_RESULT
Pr = Probability

d = Def(Symbol('C_A'), FiniteSet('1','2'))
d.display()

TallBrace([d, Symbol('x')]).display()

a = Event('I am happy')
b = Event('not happy')
es = EventSet([a,b])
es.display()

Def(Symbol('X'), es).display()

def lookup_func():
    return None
#
# s = SumOverFiniteSet(sympify('f(x)'), E_space, 'x')
# s.display()

#
# gini.display_evaluation(lookup_func, [])
# gini = Def('G', SumOverFiniteSet(Pr(x) , E_space, 'x'))
def lookup(zz):
    lc = {Pr('raining'): 0.6, Pr('sunny'): 0.4, Pr('B', 'sunny'):0.15, Pr('B', 'raining'):0.8, Pr('A', 'raining'): 0.2, Pr('A', 'sunny'): 0.85}
    # if isinstance(zz, Pr):
    #     print(zz._event, print(zz._condition))
    if zz in lc:
        # print('accepted: ', zz)
        return lc[zz]

E_space = Def('E', EventSet(['raining', 'sunny']))
E_space.display()
x = Def('x')
gini = Def('G', SumOverFiniteSet(Pr(x) * (Pr('A', x)**2 + Pr('B', x)**2), E_space, 'x'))
gini.display_evaluation(lookup, [EXPAND_OUTER_SUM, SUBSTITUTE_ALL, GET_RESULT])

pr = Probability('rain', 'humid')
(pr**2).display()

prc = Def('G', SumOverFiniteSet(Pr(x) + Pr(x) , E_space, 'x'))
# prc.display()
prc.display_evaluation(lambda x:1, [Intermediate.EXPAND_OUTER_SUM, Intermediate.SUBSTITUTE_ALL])

pr1 = Probability('rain')
pr1.display()

((pr + pr1)*pr**2).display()

# new_s = SumOverFiniteSet(, E_space, 'x')
```

\[C_{A} = \left\{1, 2\right\}\]

\[\left\{
                \begin{array}{ll}C_{A}\\x
                \end{array}
              \right.\]

\[\{\text{I am happy},\;\text{not happy}\}\]

\[X = \{\text{I am happy},\;\text{not happy}\}\]

\[E = \{\text{raining},\;\text{sunny}\}\]

$\displaystyle \begin{align}

G &= \sum_{x \in E} \Pr(\,x\,) \times ({\Pr(\,\text{A} \mid x\,)}^2 + {\Pr(\,\text{B} \mid x\,)}^2)\\
 &= (\Pr(\,\text{raining}\,) \times ({\Pr(\,\text{A} \mid \text{raining}\,)}^2 + {\Pr(\,\text{B} \mid \text{raining}\,)}^2)) + (\Pr(\,\text{sunny}\,) \times ({\Pr(\,\text{A} \mid \text{sunny}\,)}^2 + {\Pr(\,\text{B} \mid \text{sunny}\,)}^2))\\
 &= (0.6 \times (0.04 + 0.64)) + (0.4 \times (0.722 + 0.022))\\
 &= 0.706

\end{align}
        $

\[{\Pr(\,\text{rain} \mid \text{humid}\,)}^2\]

$\displaystyle \begin{align}

G &= \sum_{x \in E} \Pr(\,x\,) + \Pr(\,x\,)\\
 &= (\Pr(\,\text{raining}\,) + \Pr(\,\text{raining}\,)) + (\Pr(\,\text{sunny}\,) + \Pr(\,\text{sunny}\,))\\
 &= 1

\end{align}
        $

\[\Pr(\,\text{rain}\,)\]

\[(\Pr(\,\text{rain} \mid \text{humid}\,) + \Pr(\,\text{rain}\,)) \times {\Pr(\,\text{rain} \mid \text{humid}\,)}^2\]


# Problema:
Encontrar as cinmática das pontos que representam o movimento de um braço `Scara` de dois graus de liberdade como mostrado na figura abaixo.

## Referênciais
A localização dos referência fixo, `B0`, foi definida na altura do braço (fazendo com que seja um problema no plano) no centro do da base. Os referenciais móveis, `B1` e `B2`, foram fixados nos pinos de rotação acompanhando o movimento dos elos 1 e 2, respectivamente. Também definiu-se os comprimentos de cada elo como sendo `l_1` e `l_2`.

```python
# Funções das Bibliotecas Utilizadas
from sympy import symbols
from sympy.physics.mechanics import dynamicsymbols
from sympy.physics.vector import ReferenceFrame

# Variáveis Simbólicas
theta_1, theta_2 = dynamicsymbols('theta_1 theta_2')
l_1, l_2 = symbols('l_1 l_2', positive = True)

# Referenciais
B0 = ReferenceFrame('B0')                 # Referencial Parado
B1 = ReferenceFrame('B1')
B1.orient(B0, 'Axis', [theta_1, B0.z])    # Referencial móvel: theta_1 em relação a B0.z
B2 = ReferenceFrame('B2')
B2.orient(B1, 'Axis', [theta_2, B1.z])    # Referencial móvel: theta_2 em relação a B1.z
```

## Matrizes de Rotação
Com a definição dos referenciais as matrizes de rotação entre os referenciais podem ser facilmente encontradas.

```python
# Matrizes de Rotação
T_B0B1 = B0.dcm(B1)    # Matriz de rotação de B1 para B0
T_B1B2 = B1.dcm(B2)    # Matriz de rotação de B2 para B1

T_B0B2 = (B0.dcm(B2)).simplify()     # Matriz de rotação de B0 para B3

# Resultados em LaTeX
from IPython.display import Latex
from sympy import latex

Latex("\\begin{eqnarray}" +
     "T_{\\theta_1}&=" + "&" +str(latex(T_B0B1)) + " T_{\\theta_2}=" + str(latex(T_B1B2)) +
      "\\\\T_{\\theta_1, \\theta_2}&=" + "&" + str(latex(T_B0B2))    + "\\end{eqnarray}")
```

\begin{eqnarray}T_{\theta_1}&=&\left[\begin{matrix}\cos{\left (\theta_{1}{\left (t \right )} \right )} & - \sin{\left (\theta_{1}{\left (t \right )} \right )} & 0\\\sin{\left (\theta_{1}{\left (t \right )} \right )} & \cos{\left (\theta_{1}{\left (t \right )} \right )} & 0\\0 & 0 & 1\end{matrix}\right] T_{\theta_2}=\left[\begin{matrix}\cos{\left (\theta_{2}{\left (t \right )} \right )} & - \sin{\left (\theta_{2}{\left (t \right )} \right )} & 0\\\sin{\left (\theta_{2}{\left (t \right )} \right )} & \cos{\left (\theta_{2}{\left (t \right )} \right )} & 0\\0 & 0 & 1\end{matrix}\right]\\T_{\theta_1, \theta_2}&=&\left[\begin{matrix}\cos{\left (\theta_{1}{\left (t \right )} + \theta_{2}{\left (t \right )} \right )} & - \sin{\left (\theta_{1}{\left (t \right )} + \theta_{2}{\left (t \right )} \right )} & 0\\\sin{\left (\theta_{1}{\left (t \right )} + \theta_{2}{\left (t \right )} \right )} & \cos{\left (\theta_{1}{\left (t \right )} + \theta_{2}{\left (t \right )} \right )} & 0\\0 & 0 & 1\end{matrix}\right]\end{eqnarray}

## Posições Relativas dos pontos A e B
Por termos definido um referencial móvel para cada grau de liberdade do mecanismo, podemos facilmente expressar a posição de cada um dos pontos $A$, $B$ e $C$ em relação a esses referênciais. O ponto $A$ coincide com a origem do referencial fixo, ou seja, $\vec{r_{OA}}=\vec{0}$. O ponto $B$ é facilmente expresso em relação ao referencial `B1`, nesse caso temos que $\vec{r_{AB}}={l_1 \vec{{i}_{B1}}}$. Finalmente, o ponto $C$ é representado por $\vec{r_{BC}}={l_2 \vec{{i}_{B2}}}$. Sendo $\vec{{i}_{B2}}$ e $\vec{{i}_{B3}}$ os versores na direção $x$ dos referenciais ```B1``` e  ```B2```, respectivamente. A seguir, nota-se que a definição desses vetores posição é simples de ser implementado em ```sympy```. A principal vantagem é que não precisamos nos preocupar em relação a qual referencial estamos definindo os vetores, pois as relações entre os referenciais já foram criadas nas suas definições. Ou seja, podemos reescrever um vetor em relação a diferentes referenciais de maneira imediata, como mostrado abaixo.

```python
from sympy.physics.vector import Vector

# Vetores Posição entre os Pontos
r_OA = Vector(0)      # Vetor Nulo
r_AB = l_1 * B1.x     # Vetor que liga os pontos A e B expresso no referencial móvel B1
r_BC = l_2 * B2.x     # Vetor que liga os pontos B e C expresso no referencial móvel B2

R_AB = r_AB.express(B0) # Vetor que liga os pontos A e B expresso no referencial fixo B0
R_BC = r_BC.express(B0) # Vetor que liga os pontos B e C expresso no referencial fixo B0

# Resultado em LaTeX na Forma Vetorial ou Vetor Coluna
Latex("\\begin{eqnarray}" +
     "R_{AB}&=" + "&" +str(latex(R_AB)) + "\\\\" +
     "R_{AB}&=" + "&" +str(latex(R_AB.to_matrix(B0))) +
     "\\end{eqnarray}")
```

\begin{eqnarray}R_{AB}&=&l_{1} \operatorname{cos}\left(\theta_{1}\right)\mathbf{\hat{b0}_x} + l_{1} \operatorname{sin}\left(\theta_{1}\right)\mathbf{\hat{b0}_y}\\R_{AB}&=&\left[\begin{matrix}l_{1} \cos{\left (\theta_{1}{\left (t \right )} \right )}\\l_{1} \sin{\left (\theta_{1}{\left (t \right )} \right )}\\0\end{matrix}\right]\end{eqnarray}

## Resultados
Assim, já podemos encontrar os resultados da cinemática do ponto $C$ em relação a ```B0```. Para tal, só precisamos calcular a os vetores posição dos pontos $A$, $B$ e $C$, e suas primeiras e segunda derivadas no tempo, já que as variáveis $\theta_1$, $\theta_2$ e $\theta_3$ foram definidas como funções do tempo. Para melhor eficiência, poderíamos calcular os vetores aproveitando as fórmulas de Cinemática. Adicionalmente, a função ```trigsimp``` foi utilizada em cada elementos dos vetores colunas resultantes para simplificá-los. Então, segue o resultado.

```python
from sympy.physics.vector import time_derivative, vlatex
from sympy import trigsimp

# Cinemática do ponto A em relação ao referencial B0
r_A  = r_OA.express(B0)
v_A = time_derivative(r_A, B0)
a_A = time_derivative(v_A, B0)

# Cinemática do ponto B em relação ao referencial B0
r_B  = r_A.express(B0) + r_AB.express(B0)
v_B = time_derivative(r_B, B0)
a_B = time_derivative(v_B, B0)

# Cinemática do ponto C em relação ao referencial B0
r_C  = r_B.express(B0) + r_BC.express(B0)
v_C = (time_derivative(r_C, B0))
a_C = (time_derivative(v_C, B0))

# Simplifcação dos Resultados
r_A = (r_A.to_matrix(B0)).applyfunc(trigsimp)
v_A = (v_A.to_matrix(B0)).applyfunc(trigsimp)
a_A = (a_A.to_matrix(B0)).applyfunc(trigsimp)
r_B = (r_B.to_matrix(B0)).applyfunc(trigsimp)
v_B = (v_B.to_matrix(B0)).applyfunc(trigsimp)
a_B = (a_B.to_matrix(B0)).applyfunc(trigsimp)
r_C = (r_C.to_matrix(B0)).applyfunc(trigsimp)
v_C = (v_C.to_matrix(B0)).applyfunc(trigsimp)
a_C = (a_C.to_matrix(B0)).applyfunc(trigsimp)

# Resultados em LaTeX na forma de Vetores Coluna dos pontos A e B
Latex("\\begin{align}"
     "r_{A}&=" + str(vlatex(r_A)) + "&v_{A}&=" + str(vlatex(v_A)) + "&a_{A}&= " + str(vlatex(a_A)) + "\\" + "\\"
     "r_{B}&=" + str(vlatex(r_B)) + "&v_{B}&=" + str(vlatex(v_B)) + "&a_{B}&=" + str(vlatex(a_B)) + "\\" + "\\"
      "\\end{align}")
```

\begin{align}r_{A}&=\left[\begin{matrix}0\\0\\0\end{matrix}\right]&v_{A}&=\left[\begin{matrix}0\\0\\0\end{matrix}\right]&a_{A}&= \left[\begin{matrix}0\\0\\0\end{matrix}\right]\\r_{B}&=\left[\begin{matrix}l_{1} \operatorname{cos}\left(\theta_{1}\right)\\l_{1} \operatorname{sin}\left(\theta_{1}\right)\\0\end{matrix}\right]&v_{B}&=\left[\begin{matrix}- l_{1} \operatorname{sin}\left(\theta_{1}\right) \dot{\theta}_{1}\\l_{1} \operatorname{cos}\left(\theta_{1}\right) \dot{\theta}_{1}\\0\end{matrix}\right]&a_{B}&=\left[\begin{matrix}- l_{1} \left(\operatorname{sin}\left(\theta_{1}\right) \ddot{\theta}_{1} + \operatorname{cos}\left(\theta_{1}\right) \dot{\theta}_{1}^{2}\right)\\l_{1} \left(- \operatorname{sin}\left(\theta_{1}\right) \dot{\theta}_{1}^{2} + \operatorname{cos}\left(\theta_{1}\right) \ddot{\theta}_{1}\right)\\0\end{matrix}\right]\\\end{align}

```python
# Resultados em LaTeX na forma de Vetores Coluna do ponto C
Latex("\\begin{align}"
     "\r_{C}=&" + str(vlatex(r_C)) + "\\\\" + "\\\\"
     "v_{C}=&" + str(vlatex(v_C)) + "\\\\" + "\\\\"
     "a_{C}=& " + str(vlatex(a_C)) + "\\end{align}")
```

\begin{align}
_{C}=&\left[\begin{matrix}l_{1} \operatorname{cos}\left(\theta_{1}\right) + l_{2} \operatorname{cos}\left(\theta_{1} + \theta_{2}\right)\\l_{1} \operatorname{sin}\left(\theta_{1}\right) + l_{2} \operatorname{sin}\left(\theta_{1} + \theta_{2}\right)\\0\end{matrix}\right]\\\\v_{C}=&\left[\begin{matrix}- l_{1} \operatorname{sin}\left(\theta_{1}\right) \dot{\theta}_{1} - l_{2} \left(\dot{\theta}_{1} + \dot{\theta}_{2}\right) \operatorname{sin}\left(\theta_{1} + \theta_{2}\right)\\l_{1} \operatorname{cos}\left(\theta_{1}\right) \dot{\theta}_{1} + l_{2} \left(\dot{\theta}_{1} + \dot{\theta}_{2}\right) \operatorname{cos}\left(\theta_{1} + \theta_{2}\right)\\0\end{matrix}\right]\\\\a_{C}=& \left[\begin{matrix}- l_{1} \operatorname{sin}\left(\theta_{1}\right) \ddot{\theta}_{1} - l_{1} \operatorname{cos}\left(\theta_{1}\right) \dot{\theta}_{1}^{2} - l_{2} \operatorname{sin}\left(\theta_{1} + \theta_{2}\right) \ddot{\theta}_{1} - l_{2} \operatorname{sin}\left(\theta_{1} + \theta_{2}\right) \ddot{\theta}_{2} - l_{2} \operatorname{cos}\left(\theta_{1} + \theta_{2}\right) \dot{\theta}_{1}^{2} - 2 l_{2} \operatorname{cos}\left(\theta_{1} + \theta_{2}\right) \dot{\theta}_{1} \dot{\theta}_{2} - l_{2} \operatorname{cos}\left(\theta_{1} + \theta_{2}\right) \dot{\theta}_{2}^{2}\\- l_{1} \operatorname{sin}\left(\theta_{1}\right) \dot{\theta}_{1}^{2} + l_{1} \operatorname{cos}\left(\theta_{1}\right) \ddot{\theta}_{1} + l_{2} \left(- \operatorname{sin}\left(\theta_{1} + \theta_{2}\right) \dot{\theta}_{1}^{2} - 2 \operatorname{sin}\left(\theta_{1} + \theta_{2}\right) \dot{\theta}_{1} \dot{\theta}_{2} - \operatorname{sin}\left(\theta_{1} + \theta_{2}\right) \dot{\theta}_{2}^{2} + \operatorname{cos}\left(\theta_{1} + \theta_{2}\right) \ddot{\theta}_{1} + \operatorname{cos}\left(\theta_{1} + \theta_{2}\right) \ddot{\theta}_{2}\right)\\0\end{matrix}\right]\end{align}

```python

```


# Path Integral Ground State QMC for the Harmonic Oscillator

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

%config InlineBackend.figure_format = 'retina'
%matplotlib inline
try: plt.style.use('./notebook.mplstyle')
except: pass

π = np.pi
red,blue,green = '#e85c47','#4173b2','#7dcca4'
```

## Hamiltonian

\begin{equation}
\hat{H} = -\lambda \frac{d^2}{dx^2} + \frac{1}{4\lambda} \left(\frac{\hbar \omega}{k_{\rm B}}\right)^2 x^2
\end{equation}

where $\lambda = \hbar^2/2m$.

```python
class Paths:
    '''The set of worldlines, action and estimators.'''
    def __init__(self,beads,M,Δτ,λ):
        self.Δτ = Δτ
        self.λ = λ
        self.beads = np.copy(beads)
        self.M = M
        self.num_time_slices = beads.shape[0]
        self.N = beads.shape[1]
        self.norm = np.ones(self.num_time_slices)

        self.norm[0] = self.norm[-1] = 0.5

    def V(self,x):
        '''harmonic oscillator potential'''
        ω = 1.0
        return ω*ω*x*x /(4.0*self.λ)

    def Ψ(self,x):
        '''trial wave function'''
        return 1.0/np.cosh(np.sqrt(np.pi/2)*x)

    def kinetic_action(self,α,i):
        '''kinetic action of a bead.'''

        if α == 0.0:
            cK = (self.beads[α+1,i]-self.beads[α,i])**2
        elif α == 2*self.M:
            cK = (self.beads[α,i]-self.beads[α-1,i])**2
        else:
            cK = (self.beads[α,i]-self.beads[α-1,i])**2\
                 + (self.beads[α+1,i]-self.beads[α,i])**2
        return cK / (4*self.λ*self.Δτ)

    def potential_action(self,α):
        '''potential action of a slice.'''
        cV = 0.0
        ΨT = 1.0

        for i in range(self.N):
            x = self.beads[α,i]
            cV += self.norm[α]*self.V(x)

            # add the wavefunction at the ends of the path
            if α == 0 or α == 2*self.M:
                ΨT *= self.Ψ(x)

        return self.Δτ*cV - np.log(ΨT)

    def Energy(self):
        '''The total energy.'''

        # the kinetic part
        norm = 1.0/(4.0*self.λ*self.Δτ**2)
        KE = 0.0
        for α in range(self.M-1,self.M+1):
            for i in range(self.N):
                ΔR = self.beads[α+1,i] - self.beads[α,i]
                KE -= norm*np.dot(ΔR,ΔR)
        KE /= 2
        KE += 0.5*self.N/self.Δτ

        # the potential part
        PE = 0.0
        Vslice = np.zeros(self.num_time_slices)
        for α in range(2*self.M+1):
            for i in range(self.N):
                cV = self.V(self.beads[α,i])
                Vslice[α] += cV
                if α == self.M:
                    PE += self.norm[α]*cV

        return KE + PE, Vslice
```

### Displace Single-Slice Update

<pre>
                                i
  ┌─○────○────○──┐       ┌─○────○────○──┐
p │ ○    ○    ○  │       │ ○    ○    ○  │
r │ ○    ○    ○  │       │ ○    ○    ○  │
o │  ○  ○     ○  │       │  ○  ○     ○  │
j │   ○ ○    ○   │       │   ○   ●  ○   │α
e │   ○  ○   ○   │       │   ○  ○   ○   │
c │   ○   ○   ○  │ ━━▶   │   ○   ○   ○  │
t │  ○    ○   ○  │       │  ○    ○   ○  │
i │ ○     ○   ○  │       │ ○     ○   ○  │
o │ ○     ○    ○ │       │ ○     ○    ○ │
n │ ○     ○    ○ │       │ ○     ○    ○ │
  └─○─────○───○──┘       └─○─────○───○──┘
</pre>

```python
def displace(Path,α,i,δ=0.75):
    '''Single bead displace update.

       NB: not implemented for periodic boundary conditions.
    '''
    # Calculate the action
    x = Path.beads[α,i]
    oldAction = Path.kinetic_action(α,i) + Path.potential_action(α)

    # Displace the bead
    Path.beads[α,i] += δ*(-1.0 + 2.0*np.random.random())

    # Compute the new action
    newAction = Path.kinetic_action(α,i) + Path.potential_action(α)

    # Accept the move, or reject and restore the bead position
    if np.random.random() < np.exp(-(newAction - oldAction)):
        return True
    else:
        Path.beads[α,i] = x
        return False
```

### Staging Multi-Slice Update

<pre>
  ┌─○────○────○──┐       ┌─○────○────○──┐
p │ ○    ○    ○  │       │ ○    ○    ○  │
r │ ○    ○    ○  │       │ ○    ⊗    ○  │α + m
o │  ○  ○     ○  │       │  ○   ●    ○  │
j │   ○ ○    ○   │       │   ○   ●  ○   │
e │   ○  ○   ○   │       │   ○    ● ○   │
c │   ○   ○   ○  │ ━━▶   │   ○    ●  ○  │
t │  ○    ○   ○  │       │  ○     ●  ○  │
i │ ○     ○   ○  │       │ ○     ⊗   ○  │α
o │ ○     ○    ○ │       │ ○     ○    ○ │
n │ ○     ○    ○ │       │ ○     ○    ○ │
  └─○─────○───○──┘       └─○─────○───○──┘
       space
</pre>

```python
def staging(Path,m,i):
    '''Multi-slice update which eactly samples the free particle
       propagator between two fixed beads.

       See: http://link.aps.org/doi/10.1103/PhysRevB.31.4234

       NB: not implemented for periodic boundary conditions.
    '''
    # choose the start and end of the stage
    α_start = np.random.randint(low=0,high=Path.num_time_slices)
    α_end = α_start + m

    # if we move off the end of the path, reject the move
    if α_end >= Path.num_time_slices:
        return False

    # Record the positions of the beads to be updated and store the action
    x = np.copy(Path.beads[α_start+1:α_end,i])
    oldAction = 0.0
    for α in range(α_start+1,α_end):
        oldAction += Path.potential_action(α)

    # Generate new positions and accumulate the new action
    newAction = 0.0;
    for α in range(α_start+1,α_end):
        Δτ1 = Path.Δτ
        Δτ2 = (α_end - α)*Path.Δτ
        avex = (Δτ2*Path.beads[α-1,i] + Δτ1*Path.beads[α_end,i])\
                / (Δτ1 + Δτ2)
        σ = np.sqrt(2.0*Path.λ / (1.0/Δτ1 + 1.0/Δτ2))
        Path.beads[α,i] = np.random.normal(loc=avex,scale=σ)
        newAction += Path.potential_action(α)

    # Perform the Metropolis step, if we reject, revert the worldline
    if np.random.random() < np.exp(-(newAction - oldAction)):
        return True
    else:
        Path.beads[α_start+1:α_end,i] = x
        return False
```

```python
def pigs(num_MC_steps,num_equil_steps,Path,m=8):
    '''Perform a path integral ground state Monte Carlo simulation.'''

    # initialize estimators and acceptance counters
    numAccept = {'displace':0,'staging':0}
    estimator = {'E':np.zeros(num_MC_steps-num_equil_steps),
                 'Vslice':np.zeros([num_MC_steps-num_equil_steps,Path.num_time_slices])}

    measure = 0
    for step in range(num_MC_steps):

        # for each particle and slice try a displace move
        for α in range(Path.num_time_slices):
            for i in range(Path.N):
                numAccept['displace'] += displace(Path,α,i)

        # for each particle try a number of staging moves
        num_stage = int(Path.num_time_slices/m)
        for stage in range(num_stage):
            for i in range(Path.N):
                numAccept['staging'] += staging(Path,m,i)

        # measure the energy
        if step >= num_equil_steps:
            measure = step-num_equil_steps
            estimator['E'][measure],estimator['Vslice'][measure] = Path.Energy()

    print('displace: %4.3f' %
          ((1.0*numAccept['displace'])/(num_MC_steps*Path.num_time_slices*Path.N)))
    print('staging:  %4.3f' %
          ((1.0*numAccept['staging'])/(num_MC_steps*Path.N*num_stage)))

    return estimator
```

## Perform the PIGS Simulation

```python
# setup the simulation
M,N,τ,λ = 10,1,4.0,0.5
Δτ = τ/M

# random initial positions
np.random.seed(1173)
beads = -0.5 + np.random.random([2*M+1,N])

# setup the paths
Path = Paths(beads,M,Δτ,λ)

# compute the energy via path integral ground state Monte Carlo
num_MC_steps = 10000
num_equil_steps = 1000
estimator = pigs(num_MC_steps,num_equil_steps,Path)
```

    displace: 0.678
    staging:  0.407

## Compute the averages and their standard error

```python
from scipy.stats import sem
Ē,ΔĒ = np.average(estimator['E']),sem(estimator['E'])
V̄slice,ΔV̄slice = np.average(estimator['Vslice'],axis=0),sem(estimator['Vslice'],axis=0)

print('Ē = %f ± %f' % (Ē,ΔĒ))
```

    Ē = 0.488688 ± 0.010648

## Plot the potential energy on each slice

```python
plt.axhline(y=0.25, color=green, linewidth=1.0, label='Exact')
plt.errorbar(np.linspace(0,2*τ,2*M+1), V̄slice,yerr=ΔV̄slice, marker='o', color=blue,
             linewidth=1.0, elinewidth=1.0, markersize=6, markerfacecolor=blue, markeredgecolor=blue,
             ecolor=blue, label='QMC')
plt.xlabel(r'$\tau$')
plt.ylabel('V')
plt.legend(loc='upper left');
```

```python

```