Add entire report

This commit is contained in:
Antoine Bartuccio 2018-06-25 19:38:54 +02:00
parent 68e793689c
commit e4979378b4
Signed by: klmp200
GPG Key ID: E7245548C53F904B
11 changed files with 561 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
rapport.pdf

BIN
Firework.pdf Normal file

Binary file not shown.

BIN
Fractal.pdf Normal file

Binary file not shown.

BIN
MVC.pdf Normal file

Binary file not shown.

View File

@ -1,2 +1,9 @@
# AC20
This project is about creating demos for the SARL language.
You can compile the report with pandoc with the command
```shell
pandoc --template=default.latex rapport.md -o rapport.pdf
```

201
ac20.cls Normal file
View File

@ -0,0 +1,201 @@
\NeedsTeXFormat{LaTeX2e}[1995/12/01]
\ProvidesClass{ge11}[2017/06/13 v1 Mise en forme AC20]
% Passe les options de la classe à la classe article
\DeclareOption*{\PassOptionsToClass{\CurrentOption}{article}}
\ProcessOptions
\LoadClass[titlepage, a4paper, french,]{article}
\usepackage[right=2.3cm,left=2.3cm]{geometry}
\usepackage{pifont}
\usepackage{enumitem}
\usepackage{lmodern}
\usepackage{amssymb,amsmath}
\usepackage{ifxetex,ifluatex}
\usepackage{fixltx2e} % provides \textsubscript
\ifnum 0\ifxetex 1\fi\ifluatex 1\fi=0 % if pdftex
\usepackage[T1]{fontenc}
\usepackage[utf8]{inputenc}
\usepackage{eurosym}
\else % if luatex or xelatex
\ifxetex
\usepackage{mathspec}
\else
\usepackage{fontspec}
\fi
\defaultfontfeatures{Ligatures=TeX,Scale=MatchLowercase}
\newcommand{\euro}{}
\fi
% use upquote if available, for straight quotes in verbatim environments
\IfFileExists{upquote.sty}{\usepackage{upquote}}{}
% use microtype if available
\IfFileExists{microtype.sty}{%
\usepackage[]{microtype}
\UseMicrotypeSet[protrusion]{basicmath} % disable protrusion for tt fonts
}{}
\PassOptionsToPackage{hyphens}{url} % url is loaded by hyperref
\usepackage[unicode=true]{hyperref}
\hypersetup{
pdftitle={La programmation orientée agent},
pdfauthor={Bartuccio Antoine},
pdfborder={0 0 0},
breaklinks=true}
\urlstyle{same} % don't use monospace font for urls
\ifnum 0\ifxetex 1\fi\ifluatex 1\fi=0 % if pdftex
\usepackage[shorthands=off,main=french]{babel}
\else
\usepackage{polyglossia}
\setmainlanguage[]{french}
\fi
\usepackage{color}
\usepackage{fancyvrb}
\newcommand{\VerbBar}{|}
\newcommand{\VERB}{\Verb[commandchars=\\\{\}]}
\DefineVerbatimEnvironment{Highlighting}{Verbatim}{commandchars=\\\{\}}
% Add ',fontsize=\small' for more characters per line
\newenvironment{Shaded}{}{}
\newcommand{\KeywordTok}[1]{\textcolor[rgb]{0.00,0.44,0.13}{\textbf{#1}}}
\newcommand{\DataTypeTok}[1]{\textcolor[rgb]{0.56,0.13,0.00}{#1}}
\newcommand{\DecValTok}[1]{\textcolor[rgb]{0.25,0.63,0.44}{#1}}
\newcommand{\BaseNTok}[1]{\textcolor[rgb]{0.25,0.63,0.44}{#1}}
\newcommand{\FloatTok}[1]{\textcolor[rgb]{0.25,0.63,0.44}{#1}}
\newcommand{\ConstantTok}[1]{\textcolor[rgb]{0.53,0.00,0.00}{#1}}
\newcommand{\CharTok}[1]{\textcolor[rgb]{0.25,0.44,0.63}{#1}}
\newcommand{\SpecialCharTok}[1]{\textcolor[rgb]{0.25,0.44,0.63}{#1}}
\newcommand{\StringTok}[1]{\textcolor[rgb]{0.25,0.44,0.63}{#1}}
\newcommand{\VerbatimStringTok}[1]{\textcolor[rgb]{0.25,0.44,0.63}{#1}}
\newcommand{\SpecialStringTok}[1]{\textcolor[rgb]{0.73,0.40,0.53}{#1}}
\newcommand{\ImportTok}[1]{#1}
\newcommand{\CommentTok}[1]{\textcolor[rgb]{0.38,0.63,0.69}{\textit{#1}}}
\newcommand{\DocumentationTok}[1]{\textcolor[rgb]{0.73,0.13,0.13}{\textit{#1}}}
\newcommand{\AnnotationTok}[1]{\textcolor[rgb]{0.38,0.63,0.69}{\textbf{\textit{#1}}}}
\newcommand{\CommentVarTok}[1]{\textcolor[rgb]{0.38,0.63,0.69}{\textbf{\textit{#1}}}}
\newcommand{\OtherTok}[1]{\textcolor[rgb]{0.00,0.44,0.13}{#1}}
\newcommand{\FunctionTok}[1]{\textcolor[rgb]{0.02,0.16,0.49}{#1}}
\newcommand{\VariableTok}[1]{\textcolor[rgb]{0.10,0.09,0.49}{#1}}
\newcommand{\ControlFlowTok}[1]{\textcolor[rgb]{0.00,0.44,0.13}{\textbf{#1}}}
\newcommand{\OperatorTok}[1]{\textcolor[rgb]{0.40,0.40,0.40}{#1}}
\newcommand{\BuiltInTok}[1]{#1}
\newcommand{\ExtensionTok}[1]{#1}
\newcommand{\PreprocessorTok}[1]{\textcolor[rgb]{0.74,0.48,0.00}{#1}}
\newcommand{\AttributeTok}[1]{\textcolor[rgb]{0.49,0.56,0.16}{#1}}
\newcommand{\RegionMarkerTok}[1]{#1}
\newcommand{\InformationTok}[1]{\textcolor[rgb]{0.38,0.63,0.69}{\textbf{\textit{#1}}}}
\newcommand{\WarningTok}[1]{\textcolor[rgb]{0.38,0.63,0.69}{\textbf{\textit{#1}}}}
\newcommand{\AlertTok}[1]{\textcolor[rgb]{1.00,0.00,0.00}{\textbf{#1}}}
\newcommand{\ErrorTok}[1]{\textcolor[rgb]{1.00,0.00,0.00}{\textbf{#1}}}
\newcommand{\NormalTok}[1]{#1}
\usepackage{graphicx,grffile}
\makeatletter
\def\maxwidth{\ifdim\Gin@nat@width>\linewidth\linewidth\else\Gin@nat@width\fi}
\def\maxheight{\ifdim\Gin@nat@height>\textheight\textheight\else\Gin@nat@height\fi}
\makeatother
% Scale images if necessary, so that they will not overflow the page
% margins by default, and it is still possible to overwrite the defaults
% using explicit options in \includegraphics[width, height, ...]{}
\setkeys{Gin}{width=\maxwidth,height=\maxheight,keepaspectratio}
\IfFileExists{parskip.sty}{%
\usepackage{parskip}
}{% else
\setlength{\parindent}{0pt}
\setlength{\parskip}{6pt plus 2pt minus 1pt}
}
\setlength{\emergencystretch}{3em} % prevent overfull lines
\providecommand{\tightlist}{%
\setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}}
% Redefines (sub)paragraphs to behave more like sections
\ifx\paragraph\undefined\else
\let\oldparagraph\paragraph
\renewcommand{\paragraph}[1]{\oldparagraph{#1}\mbox{}}
\fi
\ifx\subparagraph\undefined\else
\let\oldsubparagraph\subparagraph
\renewcommand{\subparagraph}[1]{\oldsubparagraph{#1}\mbox{}}
\fi
\usepackage{float}
\floatplacement{figure}{!h}
% set default figure placement to htbp
\makeatletter
\def\fps@figure{htbp}
\newcommand\UV[1]{\newcommand\@UV{#1}}
\newcommand\semester[1]{\newcommand\@semester{#1}}
\newcommand\UVManager[1]{\newcommand\@UVManager{#1}}
\newcommand\UVFollower[1]{\newcommand\@UVFollower{#1}}
% Entête et pied de page
\usepackage{fancyhdr}
\usepackage{lastpage}
\pagestyle{fancy}
\fancyhead{}
\fancyfoot{}
\fancyheadoffset{0cm}
\rfoot{
\begin{minipage}{6cm}
\includegraphics{utbm_logo.png}
\end{minipage}
}
\lfoot{\thepage/\textbf{\pageref{LastPage}}}
\lhead{\bsc{\@author}}
\rhead{\textit{\@title}}
\fancypagestyle{titlepage}{
\fancyhf{}
\renewcommand{\headrulewidth}{0pt}
\fancyfoot[R]{\includegraphics{utbm_logo.png}}
\fancyfootoffset{-0.5cm}
}
\newcommand\cover{
\begin{titlepage}
\thispagestyle{titlepage}
\begin{center}
{\Huge\textbf{\@title}}
\hspace{20cm}
{\Large Réalisé dans le cadre de l'unité de valeur}
\end{center}
\begin{center}
{\Large\textbf{\@UV}}
\vspace{10em}
{\Large Écrit par \textbf{\@author}}
{\Large\@date}
\vspace{10em}
Gestionnaire de l'UV : \@UVManager
Professeur encadrant : \@UVFollower
\end{center}
\end{titlepage}
}
\makeatother
% Puces modifiées
\setlist[itemize]{label=\textbullet}
% Changement du titre de la table des matières
\addto{\captionsfrench}{\renewcommand*{\contentsname}{Sommaire}}
\renewcommand\thesection{\arabic{section}}
\renewcommand\thesubsection{\thesection.\arabic{subsection}}
\renewcommand\thesubsubsection{\thesubsection.\arabic{subsubsection}}
% Saut de page automatique après chaque section
\let\oldsection\section
\renewcommand\section{\clearpage\oldsection}

28
default.latex Normal file
View File

@ -0,0 +1,28 @@
\documentclass[11pt]{ac20}
\title{$title$}
\author{$author$}
\date{$date$}
\UV{$UV$}
\UVManager{$UVManager$}
\UVFollower{$UVFollower$}
\semester{$semester$}
\begin{document}
\cover
\clearpage%
\thispagestyle{empty}%
\null%
\clearpage
\tableofcontents
\newpage
$body$
\newpage
\listoffigures
\end{document}

BIN
firework_screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

324
rapport.md Normal file
View File

@ -0,0 +1,324 @@
---
title: La programmation orientée Agent
author: Bartuccio Antoine
UV: AC20
semester: TC04
date: Printemps 2017
UVManager: FLESCH Alexis
UVFollower: GAUD Nicolas
---
# Découvrir un nouveau paradigme
## Les paradigmes dans les langages de programmation
Il y a de nombreuses manières de penser un programme informatique et c'est pour cela qu'il existe de nombreux langages pour les écrire. Au delà de la différence de syntaxe, des idiomes et des philosophies différentes, autre chose change : le ou les paradigmes. Le *paradigme* correspond à la manière de formuler la solution à son problème dans un langage de programmation et influe grandement sur l'esprit du développeur lors de la conception de son algorithme.
Il existe de nombreux paradigme. Le plus simple (pour la machine) est la programmation impérative que l'on retrouve en assembleur ainsi qu'en C. On retrouve également la programmation orientée objet (POO) plus récente que la procédurale, très répandue et très appréciée. Il est tout à fait possible de mélanger différents paradigme dans le même langage de programmation comme par exemple en Python où il n'est pas choquant d'appeler des objets en plein milieu d'un programme pensé de manière procédurale.
Découvrir un nouveau paradigme permet de penser ses programmes différemment, de penser à de nouvelles méthodes de résolution d'un algorithme, penser plus loin que ce à quoi on est déjà habitué, de réfléchir à ses pratiques et de ce fait de les remettre en question.
## L'orienté agent
Ici, nous allons nous intéresser à la programmation orientée agent.
La programmation orientée agent (POA) se base sur le concept d'agent logiciel. L'POA est une abstraction de la POO offrant des agents capable d'agir indépendamment et déchanger des messages entre eux pour interagir et atteindre leur objectif. L'agent tire parti du multitâche en créant automatiquement des sous-processus propre à chaque agent et crée des espaces de communication pouvant recevoir des événements normalisés.
## Le choix du SARL
Il est possible de faire de la POA avec un langage orienté objet classique. Cependant, cela nécessite un important travail d'abstraction et d'implémentation. Même après cela, utiliser l'agent n'est ni intuitif ni naturel pour le programmeur. C'est comme faire de l'objet dans un langage structuré comme le C, c'est possible mais pénible.
Il existe très peu de langages nativement orientés agent. Le choix était donc aisé puisqu'un langage développé par l'UTBM existe, il s'agit du SARL.
Le SARL est construit sur le Java. C'est un langage multi-paradigme puisqu'il supporte entre autres l'agent et l'objet. Le langage est construit autour de la bibliothèque Janus qui fourni les bases de l'POA en Java. De plus, puisque SARL est trans-pillé en Java, il peut faire appel à des objets écrits pour le Java, ce qui permet d'accéder à de très nombreuses librairies et de travailler efficacement.
En plus du paradigme différent, le SARL propose une architecture d'agent assez intéressante. Les agents sont holoniques, ils peuvent contenir leurs propre agents et leur propre espace de communication. En plus de cela, grâce à la plate-forme Janus, les agents peuvent être redistribués sur le réseau et être exécutés sur différentes machines.
## L'agent en SARL
En SARL, un agent peut, comme un simple objet, contenir des attributs et des méthodes. Cependant, celui-ci possède son propre processus et peut percevoir des événements grâce à ses perceptions. Ainsi, un agent peut réagir ou non à un événement donné selon les conditions (garde) choisi par le programmeur. Un agent peut réagir au même événement avec plusieurs perceptions différentes qui s'exécuteront simultanément.
Initialisation d'un agent simple pouvant recevoir un événement de renommage
```Scala
agent MyAgent {
var name : String
on Initalize {
name = "Bob"
}
on Rename [name !== null]{
name = "Jean Charles"
}
}
```
Un agent peut également obtenir des capacités étendant ses fonctionnalités. Un agent peut écrire dans les logs, émettre des événement dans l'espace où il est enregistré, détecter la présence d'autres agents, accéder à son espace interne…
Exemple d'utilisation de l'utilisation de la capacité *logging*
```Scala
import io.sarl.core.Logging
agent Steeve {
uses logging
on Hack {
println("Haked")
}
}
```
Un agent peut, comme expliqué précédemment, posséder son propre contexte interne. À lintérieur de ce contexte interne, il est possible d'instancier d'autres agents qui eux mêmes possèdent leur propre contexte interne. Cela permet une organisation holonique de son application.
Exemple d'agent qui crée (*spawn*) un nouvel agent dans son contexte interne :
```Scala
import io.sarl.core.InnerContextAcess
agent Smith {
uses InnerContextAccess
on CreateInnerAgent {
spawnInContext(Smith, innerContext)
}
}
```
## Les événements
Comme dit précédemment, les agents communiquent à l'aide d'événements. Les événements peuvent s'apparenter à des appels de méthodes en POO. Cependant, l'événement est bien plus puissant. Il peut, au travers de la plate-forme Janus, se propager sur le réseau et interagir avec les agents partagés.
```Scala
/* Déclaration d'un événement simple */
event MyEvent
```
Un événement est également capable de transmettre des informations. Il est également possible pour l'agent de récupérer des informations sur l'émetteur de l'événement.
```Scala
/* Événement possédant des propriétés */
event CoolEvent {
var name : String
var nb : Integer
/* Constructeur de l'événement */
new(_name : String, _nb : Integer) {
name = _name
nb = _nb
}
}
```
Un événement atteint par défaut tous les agents présents sur l'espace d'émission. Il est également possible pour un agent de s'envoyer un événement à lui même (avec la méthode *wake* qui est très rapide) ou à un agent en particulier si son ID est connu.
```Scala
agent EventSender {
on Initialize {
emit(new MyEvent)
wake(new CoolEvent("Cool", 7))
}
}
```
Puisqu'un événement est envoyé sur un espace, il faut comprendre comment ceux-ci fonctionnent pour gérer efficacement les communications entre agents. Un espace est contenu dans un contexte. Un contexte peut contenir $n$ espaces. Chaque contexte contient un espace par défaut. Il existe un contexte par défaut généré lors de la création d'une application SARL. Il est possible de créer autant de contexte que l'on souhaite et un contexte peut être contenu dans un agent.
# Penser agent dans son application
## Une architecture adaptée
Des agents seuls, part nature, ne peuvent constituer une application complète. Cependant, il est possible de s'en servir comme d'un noyau applicatif sur lequel lire des informations et communiquer. L'interface utilisateur serait un simple objet qui communiquerai avec les agents.
Il existe plusieurs manières d'agencer son application pour parvenir à cela, celle retenu est la méthode Modèle-Vue-Contrôleur (MVC). L'idée est de séparer le Modèle (ici les agents) de la vue (GUI) ainsi que du Contrôleur qui ordonne le modèle et la vue.
![Le modèle MVC mis en place dans ce projet](MVC.pdf)
## Prise en main du langage
### Démo de feux d'artifices
Le but de cette démo est de faire apparaître des feux d'artifices à partir d'agents SARL. L'application est découpée en 4 agents distincts et le tout est construit sur une structure holonique.
![Organisation des agents pour la démo de feux d'artifices](Firework.pdf){ width=150px, height=300px }
L'agent principal est la *LaunchingArea* qui fait le lien entre la GUI et le reste des agents SARL. C'est elle qui va créer les *RocketLauncher*, un lanceur par roquette demandé par l'utilisateur, et leur transmettre la gravité et autres paramètres. Cet agent se charge aussi d'enregistrer la GUI sur un espace de communication dédié.
```Scala
agent LaunchingArea {
...
/*
* Réception de l'événement de configuration venant de la GUI
*/
on SetupSettings {
this.rocketsQuantity = occurrence.rocketsQuantity
this.fireQuantity = occurrence.fireQuatity
this.gravity = occurrence.gravity
this.maxWidth = occurrence.maxWidth
}
/*
* Initialisation de l'agent lorsqu'il
* est généré par la GUI
* Un espace de communication est ouvert
* entre la GUI et l'agent
*/
on Initialize [!occurrence.parameters.empty] {
var ctrl = occurrence.parameters.get(0) as FXMLViewerController
var ispace = defaultContext.createSpace(
OpenEventSpaceSpecification, UUID.randomUUID)
ctrl.setGUISpace(ispace)
ispace.register(asEventListener)
ctrl.listenAndDraw(grid)
info("Finishing initialization of Launching Area")
}
...
}
```
![Rendu de l'application sur la GUI](firework_screenshot.png){ width=350px, height=300px }
Viennent ensuite les RocketLauncher qui chacun font apparaître une *Rocket*. Dès que celle-ci est détruite, le *RocketLauncher* va en générer une nouvelle. Cela permet de déporter la logique de vérification d'existence des *Rocket* hors de la LaunchingArea et d'isoler les *Rocket*, et ainsi d'éviter que les événements émis par celles-ci ne viennent perturber leur fonctionnement.
```Scala
agent RocketLauncher {
...
/*
* Lancement d'une nouvelle roquette lorsque
* la précédente est détruite
*/
on MemberLeft [!isFromMe(occurrence) && !exited] {
// wake permet d'envoyer l'événement à l'agent lui même
wake(new Launch)
}
...
}
```
Les *Rocket* sont au cœur de la démo. Elles lancent chacune une tache à délais fixé où elles mettent à jour leur position et l'inscrivent dans l'objet partagé avec la GUI ici nommé *Positions*. Une fois leur durée de vie dépassée, elle génèrent dans leur contexte interne des *Fire* en accord avec la quantité demandé par l'utilisateur et attend leur destruction pour se détruire.
```Scala
agent Rocket {
...
on Initialize {
...
/*
* Crée une tâche de fond pour mettre à jour
* sa position à délai fixé
*/
move = atFixedDelay(
Configuration.RocketLifeCycleSchedulingRate) [
try {
wake(new UpdateRocketPosition);
} catch (e : Exception) {
e.printStackTrace
}
]
}
on UpdateRocketPosition [isFromMe(occurrence) &&
!frozen && !exploded] {
var vect = new Vector(2)
x = x + speedx
y = y + speedy
vect.clear()
vect.add(x)
vect.add(y)
lifetime = lifetime - 10
/* Mise à jour de l'objet Position */
if (grid !== null)
grid.setRocketPosition(id, vect)
if (lifetime <= 0) {
exploded = true
move.cancel(true)
grid.hideHocketPosition(id)
wake(new Explode)
}
}
...
}
```
Les *Fire* sont les derniers maillons de cette démo. Ils possèdent une liste de positions et sont soumis à la gravité. Tout comme l'agent *Rocket*, le *Fire* lance une tâche à délai fixé pour mettre à jour sa position actuelle, la rajouter à la fin de sa liste de positions et rajouter cette liste dans l'objet *Positions*. Lorsque sa durée de vie est dépassée, le *Fire* se détruit.
Lorsque la GUI est fermée, un événement d'extinction *Exit* est envoyé à la *launchingArea* qui se charge de le transmettre aux agents qu'elle contient et attend leur destruction pour se détruire et ainsi de suite en descendant dans la hiérarchie.
\newpage
### Démo de fractales
Cette démo présente des fractales de Sierpinski. Cela correspond à mettre 3 triangles dans un triangle.
![Organisation des agents pour la démo de fractales](Fractal.pdf){ width=150px, height=300px }
Pour réaliser cette démo, un seul agent est nécessaire. Le principe est que cet agent principal crée 3 autres agents dans son contexte interne et devient ensuite un simple transmetteur d'événement et ainsi de suite pour chacune des fractales nouvellement crées.
```Scala
agent Fractal {
...
on Initialize {
if (occurrence.parameters.size >= 2){
// Si initialisé avec arguments
screenSurface = occurrence.parameters.get(0) as Square
positions = occurrence.parameters.get(1) as Positions
if (occurrence.parameters.size.equals(3)){
// Si initialisé par la GUI
var ctrl = occurrence.parameters.get(2) as FXMLViewerController
guiSpace = defaultContext.createSpace(
OpenEventSpaceSpecification, UUID.randomUUID)
ctrl.setGUISpace(guiSpace)
guiSpace.register(asEventListener)
}
} else {
// Si initialisé sans arguments
screenSurface = new Square
positions = new Positions
}
screenWidth = screenSurface.width
this.generatePoints
}
...
}
```
![Rendu de l'application sur la GUI](sierpinski_screenshot.png){ width=250px, height=250px }
\newpage
## Difficultés rencontrées
Le fait que l'agent s'exécute dans un sous-processus change complètement la manière de penser son application. Lors de la réalisation de la démo de feux d'artifice, il a fallu synchroniser les départs de fusées et les départs de flammes lors de l'explosion. D'habitude, il aurait suffi de calculer les positions de tous les éléments puis de lancer l'affichage. Ici, puisque chaque agent est indépendant, on ne peut que lire les positions et donc les positions d'une roquette changent pendant qu'une autre roquette est générée. Il a donc fallu exploiter les capacités des agents et leur faculté à détecter l'apparition d'un autre agent dans un espace donné.
Pour fermer l'application SARL, il faut tuer tous les agents qu'elle contient. Pour fermer un agent, il faut qu'il ne contienne aucun agent dans son contexte interne. Puisque les deux démos sont basées sur une structure holonique il a fallu trouver un moyen de d'arrêter tous les agents. Il faut ainsi transmettre l'événement d'extinction à tous les agents contenu. La solution retenue a est de tuer l'agent s'il ne contiens aucun autre agent et de retransmettre sinon. Dans ce deuxième cas, l'agent vérifie si il reste des agents dans son contexte interne à chaque sortie d'un agent et de se détruire si il ne reste plus aucun agent.
Lors du développement, il a été fréquent de se heurter à de nombreuses erreurs de *refus de tâche* au niveau du noyau Janus (bibliothèque permettant de faire de l'agent en java et qui est au cœur de l'implémentation du SARL). Cela s'explique par la relative jeunesse du langage et le fait qu'il soit encore en développement. Cela survient généralement lors de la fermeture d'une des démos ou lors de l'utilisation du bouton *stop* pour les feux d'artifice. Dans ces situations, des érreurs de *tâche refusée* empêchent le bon déroulement du programme. Ces erreurs seront réglées dans une future versions de SARL mais m'ont fait perdre plusieurs heures avant de comprendre qu'il n'était pas possible pour moi d'intervenir.
# Netographie
Site internet de SARL: [sarl.io](http://sarl.io)

BIN
sierpinski_screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
utbm_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB