IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Vous êtes nouveau sur Developpez.com ? Créez votre compte ou connectez-vous afin de pouvoir participer !

Vous devez avoir un compte Developpez.com et être connecté pour pouvoir participer aux discussions.

Vous n'avez pas encore de compte Developpez.com ? Créez-en un en quelques instants, c'est entièrement gratuit !

Si vous disposez déjà d'un compte et qu'il est bien activé, connectez-vous à l'aide du formulaire ci-dessous.

Identifiez-vous
Identifiant
Mot de passe
Mot de passe oublié ?
Créer un compte

L'inscription est gratuite et ne vous prendra que quelques instants !

Je m'inscris !

Python : simuler une planche de Galton sur un formulaire Tkinter,
Un billet blog de Denis Hulo

Le , par User

0PARTAGES

I. Introduction

Une planche de Galton est un dispositif qui illustre la convergence d'une loi binomiale vers une loi normale.

Des clous sont plantés sur la partie supérieure de la planche, de telle sorte qu'une bille lâchée sur la planche passe soit à droite soit à gauche pour chaque rangée de clous. Dans la partie inférieure les billes sont rassemblées en fonction du nombre de passages à gauche et de passages à droite qu'elles ont fait.

Ainsi chaque colonne correspond à un résultat possible d'une expérience binomiale (en tant qu'une expérience de Bernoulli répétée) et on peut remarquer que la répartition des billes dans les cases approche la forme d'une courbe de Gauss :


L'objectif de ce billet est de montrer comment simuler dans un formulaire Tkinter le déplacement aléatoire des billes sur une planche de Galton, pour obtenir à la fin, en bas de la planche, une répartition des billes suivant approximativement une loi normale.

Note : si vous souhaitez avoir plus d'information sur le sujet je vous invite à consulter la page Wikipedia Planche de Galton.

II. Classe PlancheGalton

Un widget canevas est un objet du module Tkinter permettant de dessiner des formes géométriques (rectangles, lignes, disques, etc.) et de les manipuler (personnalisation, déplacement, suppression, etc.) sur une surface.

On souhaite dans notre cas dessiner une planche de Galton sur une zone d'affichage d'un formulaire en utilisant un objet Canvas. Pour cela, on va créer une classe fille PlancheGalton qui va hériter de l'ensemble des attributs et des méthodes de la classe mère Canvas :


On va donc commencer par ajouter en haut de notre module l'instruction :

Code Python : Sélectionner tout
from tkinter import Canvas


Notre classe comportera donc un constructeur, c'est à dire une méthode particulière __init__() dont le code est exécuté quand la classe est instanciée.

Elle va nous permettre de définir en particulier le nombre de niveaux ou de rangées de clous sur la planche de Galton au moment de la création de l'objet :

Code Python : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class PlancheGalton(Canvas): # la classe hérite de tous les attributs et méthodes de Canvas 
  
    def __init__(self, parent, width, height, background, nb_niveaux=24): 
  
	# exécution de ma méthode __init__() de la classe Canvas 
        Canvas.__init__(self, parent, width=width, height=height, background=background) 
  
        # définit le nombre de rangées de clous ou de niveaux de la planche de Galton 
        self.nb_niveaux = nb_niveaux 
  
        # définit la hauteur des colonnes du bas 
        self.hauteur_colonnes = nb_niveaux 
  
        # dessine la planche de Galton avec nb_niveaux 
        self.redessiner()


Elle contient également une méthode pour dessiner les rangées de clous sur la planche et une autre pour afficher une bille à une position précise :

Code Python : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class PlancheGalton(Canvas): # la classe hérite de tous les attributs et méthodes de Canvas 
    ... 
    def dessiner_bille(self, position_x, position_y, couleur): 
        # largeur et hauteur du Canvas 
        w = self.winfo_width()  
        h = self.winfo_height() 
  
        # valeurs mini et maxi sur le Canvas suivant x et y 
        min_x = -self.nb_niveaux-1; max_x = self.nb_niveaux + 1.05 
        min_y = -0.05; max_y = self.nb_niveaux + self.hauteur_colonnes + 1 
  
        # fonction de conversion des x et des y par rapport à la largeur w et la hauteur h du Canvas 
        tx = lambda x: w * (x - min_x)/(-min_x + max_x) 
        ty = lambda y: h * (y - min_y)/(-min_y + max_y) 
  
        # l'écart entre 2 clous horizontaux sur la planche de Galton vaut 2 
        # la hauteur entre 2 niveaux/rangée de clous vaut 1 
  
        # valeurs des coordonnées x et y 
        x = position_x 
        y = min_y + position_y + 1.5 
  
        # écart entre 2 rangée de clous 
        ecart_y = 1 
  
        # définition du rayon du cercle en fonction de l'espace entre les clous donc du nombre de niveaux 
        r = int(0.85*(ty(ecart_y))/2)  
  
        # trace le cercle de rayon r et centré sur le point de coordonnées (tx(x), ty(y))         
        self.creer_cercle(tx(x), ty(y), r, fill=couleur, outline=couleur) 
     ...


Le module contenant notre classe est enregistré sous le même nom PlancheGalton.py.

III. Formulaire Tkinter

On va présenter maintenant les différents composants du formulaire, puis on va décrire le code permettant de simuler le déplacement aléatoire des billes sur le planche de Galton.

III-A. Composants du formulaire

Il comporte sur sa partie supérieure des boutons de commandes pour lancer, arrêter ou initialiser la simulation.

Deux zones de texte permettent en plus de définir le nombre de rangées de clous horizontales sur la planche de Galton, et le nombre de billes pris en compte lors de la simulation.

Enfin, le reste du formulaire contient le widget canevas construit à partir de la classe PlancheGalton et sur lequel on va simuler le déplacement aléatoire des billes.

On a donc besoin d'ajouter en haut du module du formulaire des lignes de code permettant d'importer Tkinter et notre classe PlancheGalton contenue dans le fichier PlancheGalton.py :

Code Python : Sélectionner tout
1
2
import tkinter 
from PlancheGalton import PlancheGalton # on importe notre classe PlancheGalton à partir du fichier PlancheGalton.py

Ensuite, les différents objets sont ajoutés dans le formulaire Tkinter au moment de sa création, c'est à dire quand sa méthode __init__() est executée :

Code Python : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class SimulateurPlancheGalton(Tk): 
  
    def __init__(self): 
  
        # instantiation de la classe parente 
        Tk.__init__(self) 
  
        # conteneur vertical occupant tout le formulaire et destiné à contenir le widget à canvas   
        verticalPane = PanedWindow( self, orient=VERTICAL ) 
  
        # conteneur horizontal placé en haut du formulaire pour les différents contrôles du formulaire 
        horizontalPane = PanedWindow( verticalPane, orient=HORIZONTAL ) 
  
        # création des boutons de commande 
        btnLancer=Button( horizontalPane, text="Lancer", command=self.btnLancerClicked, width = 10) 
        btnArreter=Button( horizontalPane, text="Arreter", command=self.btnArreterClicked, width = 10) 
        btnInit=Button( horizontalPane, text="Initialiser", command=self.btnInitClicked, width = 10) 
  
        # création des labels et des zones de texte 
        lblNbNiveaux = Label( horizontalPane, text = "Nombre de niveaux")   
        self.txtNbNiveaux= Text(horizontalPane, height = 1, width = 11) 
  
        lblNbBilles = Label( horizontalPane, text = "Nombre de billes")  
        self.txtNbBilles = Text(horizontalPane, height = 1, width = 11) 
  
        # création d'un bouton supplémentaire permettant de finaliser la simulation 
        btnFinaliser=Button( horizontalPane, text="Finaliser", command=self.btnFinaliserClicked, width = 11) 
  
        # ajout des contrôles au conteneur horizontal 
        horizontalPane.add( btnLancer ) 
        horizontalPane.add( btnArreter ) 
        horizontalPane.add( btnInit ) 
  
        horizontalPane.add( lblNbNiveaux ) 
        horizontalPane.add( self.txtNbNiveaux ) 
  
        horizontalPane.add( lblNbBilles ) 
        horizontalPane.add( self.txtNbBilles ) 
  
        horizontalPane.add( btnFinaliser ) 
  
        # ajout du conteneur horizontal 
        verticalPane.add( horizontalPane ) 
        verticalPane.pack(expand=True, fill='both') 
  
        # création de l'objet PlancheGalton 
        self.planche_galton = PlancheGalton(verticalPane, 700, 700, "white", nb_niveaux=24) 
  
        # ajout de l'objet au conteneur vertical 
        verticalPane.add( self.planche_galton ) 
        verticalPane.pack(expand=True, fill='both') 
  
        # met à jour l'objet Canvas pour récupérer les bonnes valeurs de winfo_width() et winfo_height()  
        self.planche_galton.update() 
  
        self.planche_galton.redessiner() 
  
        # définition du titre du formulaire 
        self.title( "Planche de Galton V1.0" ) 
  
        # saisie des valeurs par défaut dans les zones de texte txtNbNiveaux et txtNbBilles 
        self.txtNbNiveaux.insert(END,self.planche_galton.nb_niveaux) 
        self.txtNbBilles.insert(END,100) 
  
        # état du simulateur mis par défaut sur "Désactivé" 
        self.etat_simulateur = "Désactivé"

Aperçu du formulaire :



III-B. Contrôle du simulateur

On associe ensuite du code Python aux boutons de commande permettant de lancer, d'arrêter ou d'initialiser le simulateur :

Code Python : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    def btnLancerClicked(self): 
  
        if self.etat_simulateur == "Désactivé": 
            # initialisation de la simulation 
            self.init_simulation() 
  
        # lancement de la simulation : état "Activé" 
        self.etat_simulateur = "Activé" 
        # exécute la méthode simuler() de l'objet 
        self.simuler() 
  
    def btnArreterClicked(self): 
  
        # arrêt de la simulation : état "Interrompu" 
        self.etat_simulateur = "Interrompu" 
  
    def btnInitClicked(self): 
  
        # initialisation de la simulation : état "Désactivé" 
        self.etat_simulateur = "Désactivé" 
  
        # initialise les paramètres de la simulation et redessine la planche de Galton 
        self.init_simulation()


III-C. Simulation du déplacement aléatoire des billes sur la planche

Dans cette partie, on va d'abord tirer au hasard les trajets des billes sur la planche, pour ensuite simuler la descente des billes à partir du tirage obtenu.

III-C-1. Tirage aléatoire des trajets des billes

Pour chaque bille, on parcourt les niveaux ou rangées de la planche de Galton, et pour chaque niveau on tire au hasard une valeur entre 0 et 1 :

  • 0 : descente à gauche du clou ;
  • 1 : descente à droite du clou.


On utilise pour cela la fonction choice du module random :

Code Python : Sélectionner tout
dep_x = random.choice([0,1])

On ajoute ensuite la valeur obtenue à une liste représentant le trajet de la bille :

Code Python : Sélectionner tout
trajet.append(dep_x)

Une fois la bille arrivée en bas de la planche, on obtient une liste de 0 et de 1 correspondant au parcours complet de la bille sur la planche :

[0, 0, 0, 0] : représente 4 descentes à gauche ;
[0, 0, 0, 1] : représente 3 descente à gauche et 1 à droite ;
[0, 0, 1, 1] : représente 2 descente à gauche et 2 à droite ;

etc..

Pour connaitre l'indice de la colonne finale dans laquelle tombe la bille, il suffit de faire la somme des 1 de la liste obtenue :

Code Python : Sélectionner tout
indice_colonne = sum(trajet)

[0, 0, 0, 0] -> 0
[0, 0, 0, 1] -> 1
[0, 0, 1, 1] -> 2


etc..

Note : cet indice de colonne représente également le nombre de cas favorables ou de succès dans un schéma de bernoulli pour lequel le nombre d'épreuves correspond au nombre de rangées de clous.

On donne maintenant la fonction complète qui effectue le tirage aléatoire des trajets des billes :

Code Python : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def tirer_trajets(nb_niveaux, nb_billes): 
  
    # initialise la liste des résultats avec des colonnes à 0 
    liste_resultats = [0]*(nb_niveaux+1) 
  
    # initialise la liste des trajets des billes 
    tirage_trajets=[] 
  
    # initialise le générateur aléatoire 
    random.seed() 
  
    # parcours des indices des billes : 0, 1, 2, .., nb_billes-1 
    for indice_bille in range(nb_billes): 
  
        # initialisation de la liste représentant le trajet aléatoire de la bille 
        trajet = [] 
  
        # parcours des indices des niveaux : 0, 1, 2, .., nb_niveaux-1 
        for indice_niveau in range(nb_niveaux): 
  
            # on tire au hasard une valeur entre 0 et 1 (0 : descente vers la gauche du clou, 1 : descendre vers la droite) 
            dep_x = random.choice([0,1]) 
  
            # ajout du choix au trajet 
            trajet.append(dep_x) 
  
        # ajout du trajet à la liste 
        tirage_trajets.append(trajet) 
  
        # indice de la colonne finale égal à la somme des 0 et des 1 de la liste obtenue 
        indice_colonne = sum(trajet) 
  
        # incrémentation du nombre de billes dans la colonne 
        liste_resultats[indice_colonne] += 1 
  
    # renvoi des listes obtenues 
    return (tirage_trajets, liste_resultats)


III-C-2. Simulation des trajets des billes à partir du tirage précédent

On dispose à ce stade de la liste des trajets des billes qui sont représentés par des séquences de 0 et de 1. Par exemple pour une planche à 4 niveaux on peut avoir [0, 1, 0, 1], [0, 1, 1, 1], etc.

Il ne reste donc plus qu'à dessiner les billes à leurs positions successives sur le canvas en parcourant les séquences de 0 et de 1.

On ajoute pour cela une méthode simuler() à la classe SimulateurPlancheGalton permettant de simuler le déplacement aléatoire des billes sur la planche de Galton :

Code Python : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
    def simuler(self): 
  
        # si une simulation est en cours 
        if self.etat_simulateur == "Activé": 
  
            en_cours = False 
  
            # parcours des indices des billes 
            for indice_bille in range(self.nb_billes): 
  
                # position actuelle ou prochaine position de la bille : (position_x, position_y) 
                position_x, position_y = self.positions_billes[indice_bille][0], self.positions_billes[indice_bille][1] 
  
                # si la bille est à cette position 
                if self.tableau_etats[position_y][position_x] == indice_bille: 
  
                    position_suiv_x = position_x 
  
                    if position_y < self.planche_galton.nb_niveaux: 
  
                        dep_x = self.tirage_trajets[indice_bille][position_y] # dep_x = random.choice([0,1]) pour une simulation en temps réel 
  
                        if dep_x==0: # si le caractère de descente vers la gauche est choisi 
                            position_suiv_x = position_x - 1 # déplacement vers la gauche 
                        else: # sinon 
                            position_suiv_x = position_x + 1 # déplacement vers la droite 
  
                    position_suiv_y = position_y + 1 # déplacement vers le bas 
  
                    # si pas de bille à la position (position_suiv_x, position_suiv_y) 
                    if self.tableau_etats[position_suiv_y][position_suiv_x] is None: 
  
                        en_cours = True 
  
                        # efface la bille à la position de coordonnées (position_x, position_y) 
                        self.planche_galton.dessiner_bille(position_x, position_y, "white") 
  
                        # met l'état de la position dans le tableau à None 
                        self.tableau_etats[position_y][position_x] = None 
  
                        # affiche une bille bleu à la position de coordonnées (position_suiv_x, position_suiv_y) 
                        self.planche_galton.dessiner_bille(position_suiv_x, position_suiv_y, "blue") 
  
                        self.positions_billes[indice_bille] = (position_suiv_x, position_suiv_y) 
  
                        if position_suiv_y<(self.planche_galton.nb_niveaux + self.planche_galton.hauteur_colonnes - 1): 
                            self.tableau_etats[position_suiv_y][position_suiv_x] = indice_bille 
                        else: 
                            self.tableau_etats[position_suiv_y][position_suiv_x] = -1 
  
                # sinon, si pas de bille à la position de coordonnées (position_x, position_y) 
                elif self.tableau_etats[position_y][position_x] is None and (position_y==0): 
  
                    en_cours = True 
                    # on positionne la bille à cet emplacement 
                    self.tableau_etats[position_y][position_x] = indice_bille 
                    self.positions_billes[indice_bille] = (position_x, position_y) 
  
                    self.planche_galton.dessiner_bille(position_x, position_y, "blue") 
  
            # si au moins une bille est toujours en progression sur la planche de Galton 
            if en_cours: 
                self.after(250, self.simuler) # 
            else: 
                self.etat_simulateur = "Désactivé" 
                self.finaliser_simulation() # finalise la simulation

Cette méthode est exécutée toutes les 250 millisecondes grâce à la commande :

Code Python : Sélectionner tout
self.after(250, self.simuler)

Note : quand le nombre de billes sur la planche devient important, il se produit un ralentissement de la procédure, c'est pourquoi on a prévu une fonction permettant de finaliser la simulation et ainsi d'obtenir directement le résultat dans les colonnes du bas :

Code Python : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
    def finaliser_simulation(self): 
        # finalisation de la descente des billes sur la planche de Galton 
        # comparaison entre la répartition des billes obtenue dans les colonnes du bas de la plannche et une répartition suivant une loi normale 
  
        # nombre de colonnes du bas et dernière ligne de la planche de Galton   
        nb_colonnes = self.planche_galton.nb_niveaux + 1         
        position_max_y = self.planche_galton.nb_niveaux + self.planche_galton.hauteur_colonnes 
  
        # moyenne et écart-type de la loi normale 
        m = self.planche_galton.nb_niveaux/2.0; s = pow(self.planche_galton.nb_niveaux,0.5)/2.0 
  
        # parcours des indices de colonne : 0, 1, 2, .., nb_colonnes-1  
        for indice_colonne in range(nb_colonnes): 
  
            # position par rapport à l'axe x 
            position_x = 2*indice_colonne - self.planche_galton.nb_niveaux 
  
            # évaluation du nombre de billes attendu dans la colonne repérée par indice_colonne : 
            # nombre approximatif de billes évalué à l'aide de la fonction de répartition de la loi normale 
            nb_billes_col = (stats.norm.cdf(indice_colonne+1-0.5, loc=m, scale=s) - stats.norm.cdf(indice_colonne-0.5, loc=m, scale=s))*self.nb_billes 
  
            # position supérieure et inférieure du rectangle représentant le nombre de billes donné par la fonction de répartition de la loi normale 
            position_y1 = position_max_y - nb_billes_col 
            position_y2 = position_max_y  
  
            # dessine un rectangle de coordonnées (position_x-0.8, position_y1, position_x+0.8, position_y2) : hauteur donnée par la fonction de répartition de la loi normale 
            self.planche_galton.dessiner_rectangle(position_x-0.8, position_y1, position_x+0.8, position_y2, "orange") 
  
            position_min_y = position_max_y - self.resultats[indice_colonne] 
  
            # parcours des positions suivant l'axe y 
            for position_y in range(position_min_y, position_max_y):                 
                # dessine la bille à la position de coordonnées (position_x, position_y) 
                self.planche_galton.dessiner_bille(position_x, position_y, "blue") 
  
        # traçage de la courbe de Gauss 
        # parcours des indices de colonne : 0, 1, 2, .., nb_colonnes-1  
        for indice_colonne in range(nb_colonnes): 
  
            # traçage de la courbe sur l'intervalle [a, b] 
            a = indice_colonne-0.5 ; b=indice_colonne+1-0.5 
  
            # pas de subdivision 
            pas = (b-a)/40 
  
            # parcours des subdivisions de l'intervalle [a,b] 
            for i in range(40): 
  
                # calcul des coordonnées de x et y 
                x = a + i*pas 
                # valeur de y obtenue à l'aide de la fonction de densité de la loi normale 
                y = stats.norm.pdf(x, loc=m, scale=s)*self.nb_billes  
  
                # positions correspondantes sur le Canvas 
                position_x = 2*x - self.planche_galton.nb_niveaux 
                position_y = position_max_y - y 
  
                # dessine un point rouge de coordonnées (position_x, position_y) 
                self.planche_galton.dessiner_point(position_x, position_y, "red")

Il s'agit donc d'une simulation pré-enregistrée, mais vous pouvez également changer dans la fonction simuler() la commande :

Code Python : Sélectionner tout
dep_x = self.tirage_trajets[indice_bille][position_y]
En :

Code Python : Sélectionner tout
dep_x = random.choice([0,1])

Pour obtenir ainsi une simulation en temps-réel.

IV. Modules de test

On a donc besoin de 2 modules pour réaliser cette simulation :

  • PlancheGalton.py est utilisé pour dessiner la planche de Galton sur une zone d'affichage ;
  • SimulateurPlancheGalton.py permet de simuler dans un formulaire Tkinter la descente aléatoire des billes sur une planche de Galton.

Si vous le souhaitez, vous pouvez télécharger le dossier complet pour effectuer les tests :



On teste maintenant le simulateur en choisissant par exemple 24 rangées de clous pour la planche de Galton, et 100 billes en entrée.

Après avoir lancé la simulation, on peut visualiser la descente aléatoire des billes sur la planche de Galton :


A la fin, on obtient une répartition des billes suivant approximativement une loi normale :



V. Conclusion

Après avoir créé la classe PlancheGalton, nous avons pu l'utiliser pour simuler sur un formulaire Tkinter une planche de Galton.

Chacun pourra ensuite librement personnaliser ou améliorer la simulation en ajoutant par exemple un léger effet de rebond des billes sur les clous.

Sources :

https://fr.wikipedia.org/wiki/Planche_de_Galton
https://fr.wikipedia.org/wiki/%C3%89...a_de_Bernoulli
https://docs.python.org/fr/3.11/library/tkinter.html
https://www.tutorialspoint.com/python/tk_canvas.htm
https://www.mathweb.fr/euclide/simul...ton-en-python/

Téléchargement :

Une erreur dans cette actualité ? Signalez-nous-la !