D2 – Monte-Carlo & Mandelbrot¶
Python numérique – devoir n° 2 – à rendre pour le dimanche 27 septembre 2026, 23 h 59 (la séance 3 est le mardi 29)
Bienvenue dans votre deuxième devoir ! Nous allons y mettre en pratique les notions vues en séance 2 : fonctions vectorisées (ufunc), agrégations avec axis, broadcasting, masques booléens et générateur de nombres aléatoires. Le sujet comporte trois parties indépendantes, avec une règle commune : on ne boucle jamais sur les points, les tirages ou les pixels. La seule boucle Python autorisée est celle sur les itérations de la partie C (et, le cas échéant, une petite boucle sur quelques graines aléatoires en A.3).
Vous avez deux semaines devant vous ; durée indicative : 2 h 30.
{admonition}
:class: important
* Ce notebook reste dans le dossier `D2/` de votre dépôt `pe-homework` (celui que le zip du sujet a créé en se dézippant), avec ses figures à côté, puis il doit être **commité et poussé** (`git add`, `git commit`, `git push`) avant la deadline. Attention, c'est la **date du commit** qui fait foi, pas celle du push ; le nom du fichier, lui, n'a pas d'importance (`sujet.ipynb` convient très bien).
* Juste avant de rendre, **pensez à ré-exécuter toutes les cellules** via *Kernel → Restart Kernel and Run All Cells* : toutes doivent s'exécuter dans l'ordre et sans erreur. En effet, un notebook qui plante à mi-chemin sera corrigé jusqu'à l'erreur…
* Les trois figures demandées (`pi.png`, `marche.png` et `mandelbrot.png`) sont à enregistrer dans le dossier du notebook (`plt.savefig`), et doivent aussi apparaître dans le notebook. Pensez à les habiller : une figure sans titre, sans légende ou sans nom d'axes perdra des points.
* Les cellules `# votre code` sont à compléter ; vous pouvez ajouter autant de cellules que vous le souhaitez. Les cellules de test (`assert`) sont là pour vous aider : si elles passent sans erreur, c'est bon signe. Ne les modifiez pas !
* Répondez aux questions "en français" dans une cellule *Markdown*, en deux ou trois phrases.
{admonition}
:class: note
* La documentation de `numpy` / `matplotlib`, les notebooks du cours, vos notes.
* Les assistants IA sont tolérés, avec la même règle qu'au D1 : **vous devez être capable d'expliquer chaque ligne que vous rendez**, et je pourrai vous demander de le faire à l'oral en séance. Le devoir est individuel.
* En revanche, les boucles `for` / `while` sur les éléments d'un tableau (points, pas, pixels) restent interdites : une solution correcte mais qui boucle sur les éléments vaudra au plus la moitié des points de la question.
Barème¶
| Partie | Contenu | Points |
|---|---|---|
| A | Monte-Carlo : estimation de π, convergence, pi.png |
6 |
| B | Marches aléatoires 2D vectorisées, marche.png |
6 |
| C | Ensemble de Mandelbrot, mandelbrot.png |
6 |
| – | Notebook propre : exécuté sans erreur, code vectorisé, figures légendées, réponses rédigées | 2 |
| Total | 20 | |
| Bonus | Zoom, palette, itération sans avertissement | +2 |
Inspiré des exercices de https://numerique-exos.info-mines.paris.
import os
import numpy as np
import matplotlib.pyplot as plt
Partie A – Estimer π par la méthode de Monte-Carlo (6 pts)¶
Le principe est le suivant : on tire n points uniformément dans le carré $[-1, 1]^2$, et l'on compte ceux qui tombent dans le disque unité ($x^2 + y^2 \le 1$). Cette proportion approche en effet l'aire du disque divisée par celle du carré, c'est-à-dire $\pi / 4$ : il suffit donc de la multiplier par 4 pour estimer π.
{admonition}
:class: note
C'est la façon moderne de tirer des nombres au hasard avec `numpy` : `rng = np.random.default_rng(0)` fabrique un générateur, puis `rng.uniform(-1, 1, size=(5, 2))` tire un tableau `(5, 2)` de flottants et `rng.integers(0, 4, size=10)` dix entiers pris dans `{0, 1, 2, 3}`. La graine passée à `default_rng` rend la suite reproductible : deux exécutions donnent exactement les mêmes tirages, ce qui est bien pratique pour retrouver un bug… et pour vous corriger !
A.1 Écrivez estimer_pi(n, seed=0) qui :
- crée un générateur
rng = np.random.default_rng(seed); - tire un tableau
pointsde forme(n, 2)de flottants uniformes dans $[-1, 1]$ (rng.uniform) ; - calcule le masque booléen
dedansdes points du disque (sans boucle) ; - renvoie
4 * (proportion de points dedans).
Grâce au seed, deux appels avec les mêmes arguments donneront exactement le même résultat.
# SOLUTION
def estimer_pi(n, seed=0):
rng = np.random.default_rng(seed)
points = rng.uniform(-1, 1, size=(n, 2))
dedans = (points**2).sum(axis=1) <= 1
return 4 * dedans.mean()
estimer_pi(1_000_000)
np.float64(3.141576)
assert 3.0 < estimer_pi(1000) < 3.3
assert abs(estimer_pi(1_000_000, seed=0) - np.pi) < 0.01
assert estimer_pi(5000, seed=42) == estimer_pi(5000, seed=42)
assert estimer_pi(5000, seed=1) != estimer_pi(5000, seed=2)
print("ok")
ok
{admonition}
:class: note
Autour d'un `plt.plot(...)` ou d'un `plt.scatter(...)`, on ajoute `plt.title("…")`, `plt.xlabel("…")`, `plt.ylabel("…")` et `plt.legend()`, qui affiche les `label=` passés aux tracés. Enfin, `plt.savefig("fig.png", dpi=120)` enregistre la figure dans un fichier ; faites-le **avant** `plt.show()`, car celui-ci referme la figure et l'on obtiendrait sinon un fichier vide. Nous verrons `matplotlib` en détail en séance 5.
A.2 Passons à l'illustration : tirez 3000 points (avec un générateur de graine 0) et tracez-les avec plt.scatter, en colorant différemment les points du disque et les autres (argument c= de scatter). Utilisez plt.axis("equal") afin que le disque soit bien rond, et donnez à la figure un titre qui mentionne l'estimation de π obtenue avec ces 3000 points.
# SOLUTION
rng = np.random.default_rng(0)
pts = rng.uniform(-1, 1, size=(3000, 2))
masque = (pts**2).sum(axis=1) <= 1
plt.figure(figsize=(5, 5))
plt.scatter(pts[masque, 0], pts[masque, 1], s=4, label="dans le disque")
plt.scatter(pts[~masque, 0], pts[~masque, 1], s=4, label="dehors")
plt.axis("equal")
plt.legend(loc="upper right")
plt.title(f"3000 points : π ≈ {4 * masque.mean():.4f}")
plt.show()
{admonition}
:class: note
`np.logspace(2, 6, 9)` donne 9 valeurs régulièrement espacées *en échelle logarithmique* entre $10^2$ et $10^6$ : en clair, c'est `linspace` appliqué aux exposants. `plt.loglog(x, y)` trace comme `plt.plot` mais avec les deux axes en échelle logarithmique, où une loi de puissance $y = x^a$ apparaît comme une droite de pente $a$. On reverra les figures en séance 5.
A.3 Étudions maintenant la convergence. Pour chaque n de ns = np.logspace(2, 6, 9).astype(int) (de 100 à 1 000 000), calculez l'erreur absolue $|\hat\pi_n - \pi|$, moyennée sur 5 graines différentes (seed = 0, 1, 2, 3, 4) afin de lisser le hasard. Rangez les résultats dans un tableau erreurs de forme (9,).
Tracez ensuite erreurs en fonction de ns en échelle log-log (plt.loglog) avec des marqueurs, et superposez la droite de référence $1 / \sqrt{n}$ en pointillés. Nommez les axes, ajoutez une légende et un titre, puis enregistrez la figure dans pi.png (plt.savefig("pi.png"), avant plt.show()).
Enfin, dans une cellule Markdown : que constatez-vous sur la pente ? Et combien de points faut-il, en ordre de grandeur, pour gagner une décimale de précision ?
{admonition}
:class: tip
Ici, une boucle Python sur les 9 valeurs de `n` et sur les 5 graines est tout à fait acceptable (cela ne fait que 45 appels) : ce sont les *points* qu'on ne doit pas parcourir un à un.
# SOLUTION
ns = np.logspace(2, 6, 9).astype(int)
graines = range(5)
erreurs = np.array([[abs(estimer_pi(n, seed=s) - np.pi) for s in graines] for n in ns]).mean(axis=1)
plt.loglog(ns, erreurs, "o-", label="erreur moyenne sur 5 graines")
plt.loglog(ns, 1 / np.sqrt(ns), "k--", label=r"$1/\sqrt{n}$")
plt.xlabel("nombre de points n")
plt.ylabel(r"$|\hat\pi_n - \pi|$")
plt.title("Convergence de l'estimation Monte-Carlo de π")
plt.legend()
plt.grid(True, which="both", alpha=0.3)
plt.savefig("pi.png", dpi=120)
plt.show()
assert ns.shape == (9,) and ns[0] == 100 and ns[-1] == 1_000_000
assert erreurs.shape == (9,)
assert erreurs[-1] < erreurs[0]
assert erreurs[-1] < 0.01
assert os.path.exists("pi.png")
print("ok")
ok
{admonition}
La pente en log-log est proche de −1/2 : l'erreur décroît comme $1/\sqrt{n}$ (c'est le théorème central limite). Pour gagner une décimale, c'est-à-dire diviser l'erreur par 10, il faut donc 100 fois plus de points : la méthode est simple, mais lente.
Partie B – Marches aléatoires dans le plan (6 pts)¶
Un marcheur part de l'origine et, à chaque pas, se déplace d'une unité dans l'une des quatre directions (est, ouest, nord, sud), choisie au hasard. Nous allons simuler K = 500 marcheurs faisant chacun n = 1000 pas — et tout cela d'un coup, sans la moindre boucle !
{admonition}
:class: note
Au lieu d'un entier ou d'une slice, on peut indexer un tableau par un *tableau d'indices* : `np.array([10, 20, 30])[np.array([0, 2, 2, 1])]` renvoie `[10, 30, 30, 20]`. Le résultat a la forme du tableau d'indices, et si le tableau indexé a plusieurs dimensions, chaque indice est remplacé par la ligne correspondante. C'est une notion optionnelle du cours (notebook `1-14`).
{admonition}
:class: note
`tab.cumsum(axis=...)` ne renvoie pas un total, mais tous les totaux intermédiaires : `np.array([2, 3, 1]).cumsum()` vaut `[2, 5, 6]`. Le résultat a exactement la même forme que le tableau de départ, et `axis=` désigne la dimension le long de laquelle on accumule. C'est précisément ce qui transforme une suite de déplacements en une suite de positions.
B.1 Avec un générateur de graine 0 :
- tirez
tirages = rng.integers(0, 4, size=(K, n)): le numéro de direction de chaque pas de chaque marcheur ; - définissez la table
directions, un tableau(4, 2)contenant les quatre déplacements(1, 0),(-1, 0),(0, 1),(0, -1); - calculez
pas = directions[tirages]. Quelle est la forme depas? - calculez
positions, les positions successives de chaque marcheur, par somme cumulée le long de l'axe des pas.
# SOLUTION
K, n = 500, 1000
rng = np.random.default_rng(0)
tirages = rng.integers(0, 4, size=(K, n))
directions = np.array([[1, 0], [-1, 0], [0, 1], [0, -1]])
pas = directions[tirages]
positions = pas.cumsum(axis=1)
print(pas.shape, positions.shape)
(500, 1000, 2) (500, 1000, 2)
assert tirages.shape == (500, 1000)
assert directions.shape == (4, 2)
assert pas.shape == (500, 1000, 2)
assert positions.shape == (500, 1000, 2)
assert np.all(np.abs(pas).sum(axis=2) == 1) # chaque pas a une longueur 1
assert np.array_equal(positions[:, 0, :], pas[:, 0, :])
assert np.array_equal(positions[:, -1, :], pas.sum(axis=1))
print("ok")
ok
B.2 Intéressons-nous maintenant aux distances. Calculez :
finales: les positions finales desKmarcheurs, forme(K, 2);distances: leur distance à l'origine, forme(K,);- comparez la racine de la moyenne des distances au carré à $\sqrt{n}$, et la distance moyenne à $\sqrt{\pi n} / 2 \approx 0{,}886\sqrt{n}$.
- Quelle proportion des marcheurs termine à moins de 20 unités de l'origine ? Et quelle est la distance maximale atteinte par un marcheur au cours de sa marche, et pas seulement à la fin ? Pour cette dernière, calculez
dist_t, tableau(K, n)des distances à l'origine à chaque instant.
# SOLUTION
finales = positions[:, -1, :]
distances = np.sqrt((finales**2).sum(axis=1))
print("RMS :", np.sqrt((distances**2).mean()), " sqrt(n) =", np.sqrt(n))
print("moyenne :", distances.mean(), " 0.886 sqrt(n) =", np.sqrt(np.pi * n) / 2)
print("proportion à moins de 20 :", (distances < 20).mean())
dist_t = np.sqrt((positions**2).sum(axis=2))
print("distance max atteinte (tous marcheurs, tous instants) :", dist_t.max())
print("distance max atteinte, marcheur par marcheur (5 premiers) :", dist_t.max(axis=1)[:5])
RMS : 31.73824191728332 sqrt(n) = 31.622776601683793 moyenne : 28.11196907157812 0.886 sqrt(n) = 28.024956081989643 proportion à moins de 20 : 0.324 distance max atteinte (tous marcheurs, tous instants) : 86.03487664894976 distance max atteinte, marcheur par marcheur (5 premiers) : [21.21320344 45.79301257 44.68780594 28.0713377 19.84943324]
assert finales.shape == (500, 2) and distances.shape == (500,)
rms = np.sqrt((distances**2).mean())
assert 0.85 * np.sqrt(n) < rms < 1.15 * np.sqrt(n)
assert dist_t.shape == (500, 1000)
assert np.all(dist_t.max(axis=1) >= distances)
print("ok")
ok
B.3 Passons à la figure marche.png : tracez les trajectoires des 5 premiers marcheurs (plt.plot(x, y) avec une couleur par marcheur), marquez l'origine d'un point noir, et pensez à plt.axis("equal"), au titre et à la légende. Enregistrez le tout dans marche.png.
Puis, dans une seconde figure (pas besoin de l'enregistrer, celle-là), tracez la distance moyenne à l'origine en fonction du temps, dist_t.mean(axis=0), et superposez la courbe $0{,}886\sqrt{t}$ pour t = np.arange(1, n + 1). Commentez en une phrase ce que vous observez.
# SOLUTION
plt.figure(figsize=(6, 6))
for k in range(5):
plt.plot(positions[k, :, 0], positions[k, :, 1], lw=0.8, label=f"marcheur {k}")
plt.plot(0, 0, "ko", ms=8, label="origine")
plt.axis("equal")
plt.legend()
plt.title(f"5 marches aléatoires de {n} pas")
plt.savefig("marche.png", dpi=120)
plt.show()
t = np.arange(1, n + 1)
plt.plot(t, dist_t.mean(axis=0), label="distance moyenne (500 marcheurs)")
plt.plot(t, 0.886 * np.sqrt(t), "k--", label=r"$0{,}886\sqrt{t}$")
plt.xlabel("pas t"); plt.ylabel("distance à l'origine")
plt.legend(); plt.title("Éloignement moyen au cours du temps")
plt.show()
assert os.path.exists("marche.png")
print("ok")
ok
B.4 Terminons par un peu de masques : sans boucle, calculez la proportion de marcheurs qui repassent par l'origine au moins une fois après le départ, c'est-à-dire dont la position vaut (0, 0) à un instant quelconque.
# SOLUTION
retour = (positions == 0).all(axis=2).any(axis=1)
print("proportion de retours à l'origine :", retour.mean())
proportion de retours à l'origine : 0.7
assert retour.shape == (500,) and retour.dtype == bool
assert 0.5 < retour.mean() < 0.95
print("ok")
ok
{admonition}
RMS ≈ 31,6 = √1000 (chaque pas a une norme 1 et les pas sont indépendants), distance moyenne ≈ 28. La courbe `dist_t.mean(axis=0)` colle à $0{,}886\sqrt{t}$. Environ 70 % des marcheurs repassent par l'origine en 1000 pas (la marche 2D est récurrente : avec une infinité de pas, ce serait 100 %).
Partie C – L'ensemble de Mandelbrot (6 pts)¶
Pour un nombre complexe $c$, on itère $z_0 = 0$, $z_{k+1} = z_k^2 + c$. Si la suite reste bornée, $c$ appartient à l'ensemble de Mandelbrot ; or, dès que $|z_k| > 2$, on est certain qu'elle diverge, ce qui nous donne un critère d'arrêt simple. On colorie alors chaque point $c$ du plan selon le nombre d'itérations effectuées avant que $|z_k|$ ne dépasse 2, avec un plafond max_iter pour les points qui ne divergent pas.
C.1 Écrivez grille(xmin, xmax, ymin, ymax, nx, ny) qui renvoie le tableau complexe c de forme (ny, nx) tel que c[i, j] = x[j] + 1j * y[i], avec x = np.linspace(xmin, xmax, nx) et y = np.linspace(ymin, ymax, ny). Utilisez le broadcasting vu en séance 2 (np.newaxis ou reshape).
# SOLUTION
def grille(xmin, xmax, ymin, ymax, nx, ny):
x = np.linspace(xmin, xmax, nx)
y = np.linspace(ymin, ymax, ny)
return x[np.newaxis, :] + 1j * y[:, np.newaxis]
grille(-1, 1, -1, 1, 3, 2)
array([[-1.-1.j, 0.-1.j, 1.-1.j],
[-1.+1.j, 0.+1.j, 1.+1.j]])
g = grille(-2, 1, -1.5, 1.5, 4, 3)
assert g.shape == (3, 4)
assert g.dtype == complex
assert g[0, 0] == -2 - 1.5j and g[2, 3] == 1 + 1.5j and g[1, 0] == -2 + 0j
print("ok")
ok
{admonition}
:class: note
Certains calculs `numpy` (dépassement de capacité, `inf`, `nan`) produisent un `RuntimeWarning` sans interrompre le programme. `np.errstate` fait taire ces avertissements sur une portion de code :
```python
with np.errstate(over="ignore", invalid="ignore"):
resultat = mon_calcul()
```
Ne l'utilisez que lorsque vous avez compris d'où viennent les avertissements et que vous les jugez inoffensifs !
C.2 Écrivez mandelbrot(c, max_iter=100) qui renvoie un tableau d'entiers n_iter, de même forme que c, contenant pour chaque point le nombre d'itérations pendant lesquelles $|z_k| \le 2$ (donc max_iter pour les points de l'ensemble).
La seule boucle Python autorisée est celle sur les itérations : à chaque tour, tous les points de c sont mis à jour d'un coup.
Un mot d'avertissement : les points qui divergent "explosent" rapidement vers inf ou nan, ce qui déclenche des avertissements (RuntimeWarning: overflow, invalid value). Ils sont sans gravité ici, et vous pourrez les faire taire en entourant l'appel de with np.errstate(over="ignore", invalid="ignore"): — ou vous en débarrasser pour de bon, cf. le bonus.
# SOLUTION
def mandelbrot(c, max_iter=100):
z = np.zeros_like(c)
n_iter = np.zeros(c.shape, dtype=int)
for k in range(max_iter):
z = z**2 + c
encore = np.abs(z) <= 2
n_iter += encore
return n_iter
with np.errstate(over="ignore", invalid="ignore"):
m = mandelbrot(grille(-2, 1, -1.5, 1.5, 300, 300), max_iter=50)
m.shape, m.min(), m.max()
((300, 300), np.int64(0), np.int64(50))
with np.errstate(over="ignore", invalid="ignore"):
m = mandelbrot(grille(-2, 1, -1.5, 1.5, 300, 300), max_iter=50)
assert m.shape == (300, 300)
assert m.dtype.kind in "iu"
assert m.max() == 50 and m.min() >= 0
assert m[150, 100] == 50 # c ≈ -1 : dans l'ensemble
assert m[0, 0] <= 2 # c = -2 - 1.5i : diverge tout de suite
assert m[150, 250] <= 5 # c ≈ 0.5 : diverge vite
assert 0.14 < (m == 50).mean() < 0.21 # aire de l'ensemble ≈ 1.5 sur un cadre de 9
print("ok")
ok
C.3 Passons à la figure mandelbrot.png : calculez mandelbrot sur la grille [-2, 1] × [-1.5, 1.5] avec nx = 900, ny = 900 et max_iter = 100, puis affichez le résultat avec plt.imshow(..., extent=[xmin, xmax, ymin, ymax], origin="lower") afin que les axes soient gradués en coordonnées complexes, et non en numéros de pixel. Ajoutez plt.colorbar(label="itérations"), les noms d'axes (partie réelle / partie imaginaire), un titre, et enregistrez dans mandelbrot.png.
Enfin, dans une cellule Markdown : que vaut n_iter pour les points de l'ensemble, et comment les reconnaît-on sur l'image ?
# SOLUTION
xmin, xmax, ymin, ymax = -2, 1, -1.5, 1.5
with np.errstate(over="ignore", invalid="ignore"):
M = mandelbrot(grille(xmin, xmax, ymin, ymax, 900, 900), max_iter=100)
plt.figure(figsize=(7, 7))
plt.imshow(M, extent=[xmin, xmax, ymin, ymax], origin="lower", cmap="magma")
plt.colorbar(label="itérations", shrink=0.8)
plt.xlabel("Re(c)"); plt.ylabel("Im(c)")
plt.title("Ensemble de Mandelbrot (max_iter = 100)")
plt.savefig("mandelbrot.png", dpi=120)
plt.show()
assert M.shape == (900, 900)
assert os.path.exists("mandelbrot.png")
print("ok")
ok
{admonition}
Les points de l'ensemble ont `n_iter == max_iter` (100) : ce sont les points de la couleur « maximale » de la barre (la grande zone intérieure, cardioïde + bulbes). Le dégradé à l'extérieur indique la vitesse de divergence.
Bonus (+2)¶
Bo.1 Un zoom, pour commencer : calculez et affichez la zone [-0.8, -0.7] × [0.05, 0.15] — la « vallée des hippocampes » — avec max_iter = 300, toujours avec extent. Essayez au passage une autre palette (cmap="twilight", "inferno", "viridis", …).
Bo.2 Puis une version sans aucun avertissement, et sans np.errstate : au lieu de mettre à jour z partout, ne mettez à jour que les points encore actifs, à l'aide d'un masque booléen et de l'indexation par masque (attention à compter exactement comme mandelbrot !). Vérifiez que le résultat est identique (np.array_equal) à celui de mandelbrot sur la grille de C.2, puis comparez les temps d'exécution avec %timeit.
# SOLUTION
def mandelbrot_masque(c, max_iter=100):
z = np.zeros_like(c)
n_iter = np.zeros(c.shape, dtype=int)
actif = np.ones(c.shape, dtype=bool)
for k in range(max_iter):
z[actif] = z[actif]**2 + c[actif]
encore = np.abs(z[actif]) <= 2 # parmi les points actifs, ceux qui restent bornés
n_iter[actif] += encore
actif[actif] = encore # on désactive les autres
return n_iter
c2 = grille(-2, 1, -1.5, 1.5, 300, 300)
with np.errstate(over="ignore", invalid="ignore"):
ref = mandelbrot(c2, 50)
print(np.array_equal(mandelbrot_masque(c2, 50), ref))
zx = (-0.8, -0.7, 0.05, 0.15)
Z = mandelbrot_masque(grille(*zx, 600, 600), max_iter=300)
plt.figure(figsize=(7, 7))
plt.imshow(Z, extent=zx, origin="lower", cmap="twilight")
plt.colorbar(label="itérations", shrink=0.8)
plt.xlabel("Re(c)"); plt.ylabel("Im(c)")
plt.title("Zoom : vallée des hippocampes (max_iter = 300)")
plt.show()
True
# CORRIGE-ONLY
with np.errstate(over="ignore", invalid="ignore"):
%timeit mandelbrot(c2, 50)
%timeit mandelbrot_masque(c2, 50)
8.65 ms ± 163 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
10.5 ms ± 74.8 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
{admonition}
`encore` est calculé sur les seuls points actifs ; `n_iter[actif] += encore` compte l'itération pour ceux qui restent bornés, et `actif[actif] = encore` désactive les autres : on obtient exactement le même comptage que la version simple (piège classique : compter `+= 1` avant de tester donne une itération de trop). La version masquée évite de calculer sur les points divergés (et donc les `inf`/`nan`) ; elle est souvent plus rapide quand beaucoup de points divergent tôt, mais l'indexation par masque a un coût, donc les deux versions sont proches sur cette grille.
Bon courage à tous ! Pour toute question sur le sujet, vous pouvez me contacter par email : raphael@imagine-app.fr.
Au plaisir de vous lire,
votre responsable de cours, Raphael Prasquier (sujet conçu par Aurélien Noce)