Astronomie : L’amas de trous noirs

Art

« L’amas de trous noirs » est le nom de notre premier projet développé en Python. Notre petit programme vous permettra de profiter d’un magnifique ciel étoilé… avec quelques trous noirs. En effet, ce programme générera pour vous une image d’une beauté à couper le souffle… Enfin, nous espérons !

Introduction

Représenter ce que l’être humain ne peut atteindre est une de ses passions favorites. En effet, nous allons vous présenter notre projet Python se prénommant : L’amas de trous noirs. Effrayant, n’est-ce pas ?

Le résultat final doit ressembler à ceci, mais vous le savez déjà :

Cette image est un des multiples résultats de notre code. Nous y reviendrons plus tard.

Comme vous pouvez le voir, il y a plusieurs éléments sur cette image : des trous noirs, des étoiles et un arrière-plan noir. Commençons par le programme pour former les trous noirs.

Nous avons utilisé le module turtle de python qui permet de dessiner à l’écran. Dans la suite de l’article, nous ferons référence à ce module sous le nom de tortue.

Les trous noirs

Un code non optimisé

C’est la première chose sur laquelle nous avons travaillé. Afin que vous conserviez votre santé mental, nous éviterons de vous montrer le code qui forme cette image :

Si vous le voulez vraiment :

def cercle(rayon, x, y, r=1, v=1, b=1):
    pensize(5)
    while rayon > 372 and r < 254:
        penup()
        goto(x, y - rayon)
        pendown()
        pencolor((r, v, b))
        circle(rayon)
        rayon -= 1
        r += 2
    while rayon > 244 and v < 254:
        penup()
        goto(x, y - rayon)
        pendown()
        pencolor((r, v, b))
        circle(rayon)
        rayon -= 1
        v += 2
    while rayon > 119 and b < 254:
        penup()
        goto(x, y - rayon)
        pendown()
        pencolor((r, v, b))
        circle(rayon)
        rayon -= 1
        b += 2
    while r > 0 and v > 0 and b > 0:
        penup()
        goto(x, y - rayon)
        pendown()
        pensize(2)
        pencolor((r, v, b))
        circle(rayon)
        rayon -= 1
        b -= 16
        v -= 16
        r -= 16


cercle(500, 0, 0)

Ce code ne nous satisfait pas car il a des limites d’utilisations. C’est-à-dire qu’il est impossible de dessiner des trous noirs de n’importe quel diamètre. On a par exemple ce cas où l’on choisi un rayon de 200 :

Ici, on constate que le dégradé n’est pas celui que l’on souhaitait.

Maintenant que nous avons vu un code qui vous pique encore les yeux, nous allons voir une autre version plus optimisée et bien plus flexible.

Un code optimisé

Avant toute chose, nous définissons au début de notre programme colormode(255) qui nous permettra de définir des couleurs au format (r, v, b), r pour rouge, v pour vert et b pour bleu qui sont les trois teintes permettant de composer n’importe quelle couleur.

Ensuite nous définissons notre fonction trou_noir(). Nous allons la découper en plusieurs portions de code afin de vous l’expliquer étape par étape.

  • Première étape : Initialisation de diverses informations.
colormode(255)

def trou_noir(x, y, rayon):
    color(0, 0, 0)
    pensize(5)
    r = -1
    v = -1
    b = -1
    penup()
    goto(x, y - rayon - 20)
    pendown()

Notre fonction aura donc besoin de trois paramètres : les coordonnées x, y et le rayon du trou noir. Nous indiquons que la couleur du stylo sera noir, en raison de l’arrière-plan qui sera noir également. Nous définissons le taille du trait à 5, qui est la valeur la plus basse tout en évitant des artéfacts au niveau du trou noir comme ceci :

Nous définissons trois variables qui, comme leur nom l’indique, seront les variables liées au changement de couleur. Nous les définissons avec une valeur négative, ce qui peut paraitre assez étrange, car les valeurs minimales pour le vert, le rouge ou le bleu sont zéro. Nous vous expliquerons un peu plus bas pourquoi nous faisons cela.

La fonction penup() permet de lever le stylo, la fonction goto() permet de le déplacer. On met en paramètre les coordonnées x et y en paramètre. La fonction pendown() permet elle, de poser le stylo.

Concernant la fonction goto(), elle prend en paramètre les coordonnées x et y, qui représente un point de l’écran. La tortue n’est capable de dessiner un cercle qu’à partir de sa base (le point le plus bas du cercle). Nous souhaitons utiliser le centre géométrique d’un cercle pour les positionner. Nous allons donc décaler la coordonnée y de la distance du rayon. Cela donne le code suivant : goto(x, y - rayon).

Nous avons choisi d’ajouter autour de notre trou noir une zone noire (🥴) de 20 pixels d’épaisseur (cette valeur a été défini de façon arbitraire). Nous verrons par la suite que c’est un choix esthétique, cette marge de 20 pixels se retrouve donc dans l’appel de notre fonction goto() : goto(x, y - rayon - 20).

  • Deuxième étape : Création d’un disque noir.
	begin_fill()
    fillcolor("black")
    circle(rayon + 20)
    end_fill()

Ce disque a été rajouté afin d’améliorer la fonction trou_noir() : en effet, après une première version de la fonction trou noir, nous avons amélioré le rendu graphique en dessinant un premier disque noir sur lequel la tortue dessine le trou noir. Cela apporte deux avantages : produire la marge de 20 pixels autour du trou noir (cf. paragraphe au-dessus) et remplir le centre de notre trou noir. Voyez ainsi :

Ce disque noir sert de transition entre le ciel étoilé (que vous verrons par la suite) et le trou noir en lui-même.

  • Troisième étape : Construction du trou noir (dégradé du rouge vers le blanc).
    while r < 255:
        penup()
        goto(x, y - rayon)
        pendown()
        r += 2
        pencolor((r, 0, 0))
        circle(rayon)
        rayon -= rayon/500

    while v < 255:
        penup()
        goto(x, y - rayon)
        pendown()
        v += 2
        pencolor((r, v, 0))
        circle(rayon)
        rayon -= rayon/500

    while b < 255:
        penup()
        goto(x, y - rayon)
        pendown()
        b += 2
        pencolor((r, v, b))
        circle(rayon)
        rayon -= rayon/500

Ces trois boucles while sont très similaires. Elles servent à créer le dégradé du trou noir. Ce que notre tortue fait est de dessiner un cercle d’une couleur différente à chaque fois que la boucle se répète. La boucle se termine une fois que la variable de la couleur donnée dans la condition while a atteint son maximum, soit 255. A chaque itération, nous incrémentons de 2 chaque variable de couleur, et nous le faisons juste avant de dessiner le cercle. Avec cette incrémentation, il nous faudrait 128 itérations pour arriver à la valeur 256. Cette valeur est donc supérieur à 255, nous avons donc décidé d’initialiser chaque variable de couleur à -1 afin de compenser ce problème. Pour la variable r par exemple, la première fois que notre boucle va s’exécuter, elle va s’incrémenter de 2 ( -1 + 2 = 1 ), puis choisir une couleur pour le stylo : pencolor((r, 0, 0)), puis dessiner le cercle. Lors de la dernière itération, r est égal à 253, donc 253 + 2 = 255, la tortue dessine le cercle avec cette valeur. Nous sortirons ensuite de la boucle. Nous faisons ceci trois fois, pour les trois variables de couleurs.

La dernière ligne qui est intéressant dans l’extrait cité est rayon -= rayon/500. La valeur 500 provient de notre « programme d’essai », celui non optimisé. Nous avons utilisé la proportionnalité par rapport à la valeur trouvée grâce à ce programme. Dans le programme d’essai nous retirions 1 au rayon pour un cercle de rayon 500. Ici nous retirons au rayon le résultat de la division du rayon par 500. Par exemple si le rayon est 500, on retirera 1 ( 500/500 = 1 ), si c’est 250, on retirera 0,5 ( 250/500 = 0,5 ), si c’est 750, on retirera 1,5 ( 750/500 = 1,5 ), etc. Ceci permet de faire des trous noir de la taille que l’on souhaite.

  • Quatrième étape : Dégradé du blanc au noir de finition.
    while r > 0 and v > 0 and b > 0:
        penup()
        goto(x, y - rayon)
        pendown()
        pencolor((r, v, b))
        circle(rayon)
        rayon -= rayon / 500
        b -= 16
        v -= 16
        r -= 16
    pencolor("black")
    circle(rayon)

Cette dernière boucle permet de faire un fort dégradé entre le blanc et le centre du trou noir, qui est… noir (🥴). Elle n’a rien de particulier par rapport à ce que l’on a expliqué plus haut mis à part la valeur forte de la réduction des valeurs des trois variables de couleurs (que l’on décrémente de 16) et également la condition de la boucle qui permet qu’elle s’arrête une fois que la couleur atteinte est le noir.

Les deux dernières lignes de l’extrait permettent d’éviter ce problème :

Les étoiles

Voici le code pour former les étoiles :

def etoile(x, y, branche, longueur, couleur=(255, 230, 0)):
    penup()
    goto(x, y)
    degre_angle = 180 - (360 / (branche * 2))
    begin_fill()
    fillcolor(couleur)
    for i in range(branche):
        forward(longueur)
        right(degre_angle)
    end_fill()

Cette fonction prend en paramètre les coordonnées x et y, le nombre de branche, la longueur des branches (plus précisément la longueur des segments constituants l’étoile) et la couleur (avec une couleur par défaut qui est un jaune-orange qui tend plus vers le jaune). On lève notre stylo, on se positionne de manière à ce que le centre de l’étoile soit les coordonnées données en paramètres et… pas de pendown() ? En effet celui-ci serait inutile pour former les étoiles car nous n’allons pas utiliser la fonction de tracé de ligne mais plutôt utiliser la fonction de remplissage (de couleur) de zone. Ensuite, nous créons une variable degre_angle. Elle est très importante car elle va définir le degré des angles qui vont permettre de former l’étoile. Regardez :

L’étoile est formée de la même manière que l’extrait cité sauf qu’ici le stylo est baissé, et il n’y a pas de couleur de remplissage. Les angles qui nous intéressent sont au bout des branches. Ce sont ces angles là que le programme contrôle.

En A), notre tortue avance. Elle est à une position avec un angle de 0°. L’angle en rouge est égal à 180°. C’est notre 180 dans la formule ! Et (360 / (branche * 2)) est en fait l’angle intérieur. Pour savoir la valeur de cet angle, il faut imaginer l’étoile dans un cercle. La somme de tous les sommets de cette étoile dans un cercle donne 360°. Et dans une étoile, le nombre de sommet est le double du nombre de branche.

Ensuite en B), elle s’oriente de l’angle calculé dans la variable degre_angle. Le trait violet est un trait de construction rajouté pour que le schéma soit plus clair.

Pour finir en C), la tortue avance. Ici on voit bien l’angle formé qui est celui de degre_angle. Le trait en cyan est un trait de construction.

D’une pierre deux coup, nous vous avons expliqué aussi la boucle qui est dans le programme. Avec begin_fill(), fillcolor(couleur) et end_fill() nous remplissons l’intérieur de l’étoile avec la couleur donnée en paramètre.

Le ciel (arrière-plan et étoiles)

Cool ! On a bien avancé. On continue avec cette fois-ci un bon bgcolor("black") qui est censé faire un arrière-plan en noir et… non. Cela ne va pas marcher, nous verrons cela un peu après, une fois le code entièrement présenté. Nous allons donc concevoir une fonction arriere_plan() :

def arriere_plan():
    # .ps est incapable de récupérer le bgcolor() selon internet...
    # Et des barres blanches apparaissent avec ce code dans le .png généré...
    penup()
    goto(-640, 0)
    pendown()
    pensize(900)
    forward(1280)

Il y a un bavard dans le code de ce que je vois, mais il a raison (ou on a tous les deux torts, c’est une éventualité). Encore une fois, nous verrons ceci un peu plus tard. Cette fonction est très simple, elle conçoit simplement un fond noir, en se positionnant le plus à gauche du canvas (zone de dessin) et en traçant un trait de la taille du canvas (le résultat doit avoir une taille de 1280 pixels de largeur par 720 pixels d’hauteur) avec une taille exagérée.

def ciel():
    arriere_plan()
    liste_couleur = [(255, 255, 255), (255, 230, 0)]
    for i in range(0, randint(71, 121)):
        liste_couleur[1] = (255, randint(150, 230), 0)
        etoile(randint(-630, 630), randint(-350, 350), 5, 10, choice(liste_couleur))
    for i in range(0, randint(16, 31)):
        liste_couleur[1] = (255, randint(150, 230), 0)
        etoile(randint(-620, 620), randint(-340, 340), 7, 20, choice(liste_couleur))
    for i in range(0, randint(6, 16)):
        liste_couleur[1] = (255, randint(150, 230), 0)
        etoile(randint(-600, 600), randint(-320, 320), 15, 40, choice(liste_couleur))

Occupons nous maintenant de la fonction ciel(). Nous appelons la fonction arriere_plan() puis le programme dessine des étoiles dans notre ciel. Nous utilisons simplement trois boucles qui dessineront 3 types d’étoiles différentes. En effet grâce aux paramètres branche et longueur, nous pouvons former des étoiles complètements différentes, diversifiant le ciel. Concernant le paramètre couleur, nous avons une liste qui varie à chaque fois que la boucle se répète. C’est plus précisément le second élément qui varie. On modifie aléatoirement la quantité de vert dans la couleur, rendant l’étoile plus ou moins orangée. La fonction choice() permet de choisir un élément aléatoirement parmi une liste donnée en paramètre. Pour finir sur ce bout de code, chaque boucle for génère un nombre aléatoire d’étoile entre deux valeurs précisées (ex : randint(71, 121)). On remarque que les étoiles plus petites apparaissent plus nombreuses.

Voici un rendu de la fonction ciel() :

Taille du canvas : 2560×1080 pixels
Taille du canvas : 1280×720 pixels

On remarque bien le fond généré « artificiellement » dans la première image, et comment cela rend dans les bonnes dimensions.

Assemblement et problèmes

Bien, nous avons notre ciel, plus qu’à rajouter les trous noirs. Nous en ferons trois :

ciel()
trou_noir(randint(-490, -250), randint(-260, 260), randint(50, 150))
trou_noir(randint(100, 250), randint(-260, 260), randint(50, 150))
trou_noir(randint(400, 490), randint(-260, 260), randint(50, 150))

On appelle donc notre fonction ciel() et nous générons trois trous noirs. Ils sont positionnés aléatoirement mais pour éviter une fusion de trou noir, nous les plaçons dans des coordonnées où ils ne peuvent s’entrechoquer. On remarque qu’il y a un décalage de 150 entre chaque plage de coordonnées x. C’est dû à la taille qui est aléatoire entre 50 et 150.

Une fusion de trou noir… C’est très beau, vous ne trouvez pas ?

Nous vous avions proposé un résultat en début d’article, en voici un autre :

Le rendu turtle
Le fichier PNG généré

On remarque diverse imperfection. La plupart provienne d’un problème lors de la conception du fichier postscript et nous n’avons pas trouver comment régler ce problème.

Un autre problème que nous avons eu a été celui-là :

Effectivement, je n’ai plus l’impression de voir un ciel…

Bon, qu’est-ce qui s’est passé ? Lorsque nous avons généré cette image, nous utilisions la fonction bgcolor("black") pour générer le fond noir sauf qu’après une petite recherche sur internet, le fichier postscript ne prend pas en compte cette action, il ne la « voit » pas. Nous avons donc dû changer par la fonction arriere_plan() que nous vous avons expliqué plus haut.

Conclusion

Vous pouvez former des images complètement différentes de ce que l’on a généré simplement en modifiant quelques valeurs ou en rajoutant une boucle for… Vous pouvez faire un ciel beaucoup plus dense, des dégradés plus profonds, des trous noirs plus grands, etc. Nous avons voulu faire un programme qui puisse avec très peu de modification générer des résultats d’une grande diversité.

Télécharger le .py

Si l’envie vous prend de rendre ces personnalisations plus simples d’accès et/ou optimiser notre code, voici un petit cadeau :

L’image finale