Nous allons cette semaine donner un aspect un peu plus complet/unifié à notre projet en créant un « système de simulation » regroupant tous les composants nécessaire pour vous permettre de faire une première simulation, en « mode texte ».
Je vous conseille une dernière fois de vous assurer avoir bien compris les concepts présentés en cours avant de vous lancer directement dans la réalisation de la nouvelle partie du projet, par exemple en faisant au préalable quelques exercices si nécessaire.
Pensez aussi à relire le descriptif afin de bien savoir ce que vous êtes en train de faire.
Commençons par la vue générale, la conception globale de nos programmes. J'en donne ici une version minimale. La même démarche est expliquée plus en détail dans le tutoriel sur le graphisme, mais dans un cadre un peu différent (celui du graphisme en général), ce qui fait qu'elle y est un peu plus compliquée, mais plus complète. Cela vaut peut-être la peine d'aller y jeter un œil si la version résumée ici n'est pas assez claire.
L'idée de cette conception générale est d'être très modulaire et de systématiquement séparer d'un coté les simulations physiques et de l'autre leur visualisation (sous forme textuelle, graphique, ou d'autres comme, par exemple, l'écriture dans un fichier, non abordée dans ce projet). On distingue pour cela (au moins) trois choses : le programme principal (le main()), le système à simuler (classe Systeme décrite ci-dessous) et la façon de l'afficher (classes Dessinable et SupportADessin, décrites ici).
Par ailleurs, nous souhaiterions que les particules et les obstacles utilisés dans
notre système puissent être de différente nature (surtout les particules -- on pourra en avoir de différentes sortes).
Le but de ce projet étant avant tout de vous faire concrètement
travailler les différents aspects de la POO, je vous demande
d'imaginer que la classe Particule (ou peut être même la classe Obstacle) aurait
différentes sous-classes. Ce sera l'objet de l'exercice P11 en semaine 10.
Plusieurs de ces objets de vos simulations seront «digne d'intérêt» dans ce sens que c'est ce que nous souhaitons observer. Cela nous conduit à l'abstraction suivante décrite ci-dessous: les «objets dessinables» (ou «observables»).
On souhaite pouvoir « interroger l'état » de différents objets, soit en mode texte comme cette semaine, soit de façon graphique comme abordé dans la semaine prochaine, les différents objets qui seront présents dans nos simulations (typiquement les Particule et peut être les Obstacle et les Source dans le cadre de ce projet-ci). On veut le faire de façon unifiée au moyen d'une méthode dessine_sur(SupportADessin&), laquelle écrira simplement du texte dans la version « mode texte » et dessinera effectivement quelque chose lorsque vous ajouterez du graphisme. Bien sûr, chaque dessin est spécifique à l'objet dessiné !
[Question P8.1] En termes de POO, quelle est donc la nature de la méthode dessine_sur() ?
Répondez à cette question dans votre fichier REPONSES.
Concrètement, je vous demande donc de créer une classe Dessinable contenant une méthode dessine_sur(SupportADessin&).
Vous pouvez pour cela (c'est même recommandé) vous insipirer du code fourni dans cette partie du tutoriel sur le graphisme.
Pour bien séparer les différents moyens de visualisation, nous introduisons également une classe (abstraite) SupportADessin, qui représentera de façon générique le moyen choisi pour dessiner (écran mode texte, mode graphique avec telle bibliothèque (p.ex. raylib), ou telle autre, ou dans un fichier (non abordé dans ce projet), etc.). Ces moyens possibles de dessiner seront représentés par des sous-classes (à faire plus tard), p.ex. TextViewer pour la « visualisation » en mode texte, VueOpenGL pour la visualisation avec du graphisme en 3D utilisant OpenGL, ou encore FileWritter (non abordé dans ce projet) pour écrire les résultats dans un fichier, etc..
Pour pouvoir être correctement « dessinées », de façon appropriée à chaque SupportADessin, toutes les classes « dessinables » devront posséder la même méthode dessine_sur() , exactement rigoureusement la même (« mot pour mot » ; cette méthode doit être :
virtual void dessine_sur(SupportADessin& support) override
{ support.dessine(*this); }
ATTENTION ICI ! Pour des raisons techniques
dépassant le cadre de ce cours, il ne faut pas que cette méthode soit
définie dans la classe Dessinable puis
héritée, mais bien que ce même bout de code soit recopié à l'identique
(copié-collé, une fois n'est pas coutume !) dans chaque sous-classe de Dessinable (masquage).
[ C'est
le prix à payer du fait qu'en C++ il n'y a pas de ce qu'on appelle « double dispatch ». ]
La classe SupportADessin est une classe abstraite qui ne doit contenir que (les méthodes usuelles et) les méthodes virtuelles de dessin pour tous les objets que l'on voudra dessiner. Par exemple (à adapter à votre propre projet et au fur et à mesure de son avancement) :
class SupportADessin
{
public:
virtual ~SupportADessin() = default;
// on suppose ici que les supports ne seront ni copiés ni déplacés
virtual void dessine(Particule const&) = 0;
virtual void dessine(Systeme const&) = 0;
virtual void dessine(Obstacle const&) = 0;
// ... autres choses que vous voudriez « dessiner »...
};
Faites déjà tout ceci, puis répercutez sur les classes Particule, Plan, Source ou autres, les modifications que vous jugez nécessaires.
Note : lorsque deux classes dépendent/ont besoin réciproquement l'une de l'autre (comme Particule et SupportADessin, par exemple) on ne peut pas simplement appeler le .h de l'une dans l'autre et réciproquement : cela soit crée une boucle infinie d'inclusions, soit ne compile pas pour la première des deux classes qui bloque le mécanisme d'inclusions multiples. Il faut dans ce cas remplacer l'inclusion du .h par une prédéclaration : juste « class A; » et c'est tout, dans l'un ou l'autre ou les deux fichiers (bien choisi(s) !). Avec une telle prédéclaration, on ne peut alors que utiliser des références et des pointeurs sur cette class prédéclarée ; rien d'autre.
Concrètement : dans SupportADessin.h on n'utilise que des références, par exemple sur Particule. Au lieu d'y mettre un #include "Particule.h", mettez-y simplement un class Particule;.
Créez une classe TextViewer qui est un SupportADessin dédié à la « visualisation » en mode texte dans un ostream qu'il aura comme attribut (par référence, bien sûr ! -- on ne peut pas faire de copie d'ostream).
Les méthodes dessine() de cette classe se contentent simplement d'appeler l'opérateur << de leur argument.
Pour plus de détails, voir si nécessaire cette partie du tutoriel graphique.
Avant de commencer, je voudrais attirer plus spécialement votre attention sur un point évoqué en cours :
[Question P8.2] A quoi faut-il faire attention pour les classes contenant des pointeurs ? Quelle(s) solution(s) est/sont envisageable(s) ?
Répondez à cette question dans votre fichier REPONSES.
La classe Systeme à laquelle on s'intéresse ici a pour but de représenter tout ce qui forme le système physique simulé, c.-à-d. une collection de particules, une collection d'obstacles, une collection de sources de particules, etc. Un Systeme aura aussi la « maîtrise du temps » (c'est lui qui connaît et décide du temps) et décidera du milieu (air ou eau) dans lequel sont les particules.
Comme discuté précédemment dans la conception générale : on
veillera à clairement dissocier ce qui relève de la simulation du
système physique (et qui devra être rattaché à la
classe Systeme) de tout ce qui relève de sa
représentation graphique, sa « visualisation » au sens
large.
En plus clair : il devra être possible, en utilisant la classe
Systeme, de faire tourner une simulation sans aucune
interface graphique (simulation en mode texte).
De plus, on devra pouvoir visualiser/« dessiner » un Systeme. Son « dessin » consistera simplement à dessiner tous les composants qu'il contient.
Enfin, je voudrais attirer votre attention sur le fait que dès la semaine 10, il y aura plusieurs sortes de particules possibles (neige, mais aussi boue ou terre, roche, etc.).
[Question P8.3] Comment représentez vous
la classe Systeme ?
Expliquez votre conception (attributs, interface, ...).
Répondez à cette question dans votre fichier REPONSES.
Cette classe devra au moins avoir comme méthodes (mais vous pouvez bien sûr en ajouter d'autres) :
On ne s'intéresse pour l'instant pas à l'évolution du système. Ceci est l'objet du prochain exercice.
Exemple d'affichage :
Le système est constitué des 3 particules suivantes : [ pos = (0 0 0), v = (0 0 0), m = 0.268083, r = 0.4 ] [ pos = (1 0 0), v = (0 0.2 0), m = 0.176715, r = 0.15 ] [ pos = (0 0 1), v = (0 0 -0.05), m = 0.05236, r = 0.1 ]
Créez un fichier testSystem.cc pour y tester votre nouvelle classe en reproduisant l'exemple suivant :
Exemple de déroulement pour quatre particules aux sommets d'un tétraèdre régulier (y3=sqrt(3)/2, y4=sqrt(3)/6, z4=sqrt(6)/3, ρ=1e3 kg/m3 = 1 mg/mm3, la masse étant ici donnée en mg (milligrammes) et le rayon (ainsi que les positions) en mm (millimètres)) :
./testSystem Le système est constitué des 4 particules suivantes : [ pos= (0 0 0), v= (0 0 0), m= 0.268083, r= 0.4 ] [ pos= (1 0 0), v= (0 0 0), m= 0.268083, r= 0.4 ] [ pos= (0.5 0.866025 0), v= (0 0 0), m= 0.268083, r= 0.4 ] [ pos= (0.5 0.288675 0.816497), v= (0 0 0), m= 0.268083, r= 0.4 ] et des 1 obstacles suivants : Plan d'origine (0 0 -0.9) et de normale (0 0 1)
Note 1 : attention aux unités des constantes du milieu ηair=1.8e-2 mg mm-3 s-1, ρair=1.3e-3 mg mm-3, ηeau=1 mg mm-3 s-1 et ρeau=1 mg mm-3.
Note 2 : ce sera surtout utile pour la suite (simulation) : pour les coordonnées des particules, entrez bien les formules ci-dessus (avec les racines carrées) et non pas des valeurs numériques approchées à 1e-6 près.
Il est maintenant temps d'intégrer tout ceci et de faire notre première simulation complète, reproduisant pour commencer simplement avec quelques particules de position et vitesse maîtrisées ; c'est-à-dire que l'on s'intéresse ici à l'écriture du moteur de simulation et non pas encore à l'aspect physiquement réaliste du système. Ceci interviendra plus tard dans le semestre.
On veut donc dans cet exercice :regrouper toutes les classes créées jusqu'ici et faire une « visualisation en mode texte » suivant les principes décrits dans l'exercice précédent ;
pouvoir faire évoluer le système, c'est-à-dire gérer l'évolution d'un pas de temps (à partir d'une situation donnée, calculer la situation résultante) et faire boucler le système sur plusieurs pas de temps consécutifs.
Pour le premier point, créez une classe TextViewer qui est un SupportADessin dédié à la « visualisation » en mode texte (voir à ce sujet cette partie du tutoriel graphique).
Pour le second point, implémentez dans la méthode evolue() de la classe Systeme une boucle sur toutes les particules du système qui, pour chacun :
Ces deux étapes peuvent se faire dans n'importe quel ordre sur les particules présentes dans le système, ce qui conduit à plusieurs méthodes de simulation. Lorsque le choix des particules à faire évoluer est aléatoire, on parle de simulation Monte-Carlo, par opposition à une simulation déterministe qui considère toujours les particules dans le même ordre.
Il existe même deux variantes de ce mode de simulation déterministe : avec ou sans sauvegarde individuelle des anciennes positions et vitesses.
Dans un autre extrême (Monte-Carlo), on peut choisir de tirer au hasard quelle particule mettre à jour ; et recommencer ainsi de suite (en tirant éventuellement à nouveau la même particule).
Pour la version minimale du projet expliquée ici nous nous
placerons dans le cas déterministe sans sauvegarde, mais celles et ceux qui sont
intéressé(e)s sont encouragé(e)s à implémenter plusieurs types de moteur de
simulation différents.
(Les plus motivé(e)s d'entre vous sont même encouragé(e)s à
utiliser ici le polymorphisme de sorte
à pouvoir choisir dynamiquement pendant l'exécution d'un même
programme quelle méthode de simulation est utilisée. Mais ceci est
vraiment pour celles et ceux qui sont à un haut niveau ! J'insiste qu'il
serait catastrophique pour des étudiant(e)s ne se sentant pas à l'aise,
d'essayer inutilement de grappiller ici des points inaccessibles !)
Pour en revenir au cas le plus simple, on peut donc simuler notre système en utilisant l'algorithme suivant (algorithme 1) : pour toute particule :
pour tous les obstacles, ajouter la force qu'exerce l'obstacle sur la particule (méthode ajouteForce(Obstacle)) ;
pour toutes les autres particules, ajouter la force qu'exerce cette particule sur la particule courante (méthode ajouteForce(Particule)) ;
Un autre algorithme (algorithme 2), extrêmement proche consiste à effectuer les étapes 1 à 3 pour toutes les particules, puis ensuite dans une seconde boucle sur toutes les particules à effectuer 4.
[Question P9.1] Quelle est la complexité de ces deux algorithmes
ci-dessus ?
Répondez à cette question dans votre
fichier REPONSES en explicitant votre réponse.
Implémentez au choix l'algorithme 1 ou l'algorithme 2 ci-dessus dans la méthode evolue() de la classe Systeme.
En prenant quatre particules similaires à celles de l'exemple précédent, et les valeurs suivantes pour les constantes physiques (attention aux unités !) :
g=(0,0,−9.81) ms-2=(0,0,−9.81e3) mm s-2, ρparticule= 1 mg/mm3, dt=0.001 s, σ=0.885 mm, ε=25 mg mm3 s-2, ηmilieu=ηair=1.8e-2 mg mm-1 s-1 et ρmilieu=ρair=1.3e-3 mg mm-3,
on obtient les résultats donnés dans ce fichier (algorithme 1 ; ici pour l'algorithme 2), avec tous les détails pour vous aider à corriger.
Dans un fichier exerciceP9.cc, créez puis faîtes évoluer un système ayant les caractéristiques données ci-dessus.
Sauvegardez bien cette étape du projet. Elle devra faire partie du rendu final.
Nous avons ici atteint une étape fondamental de notre projet, la simulation. Il est donc très important de bien pouvoir la tester. Pour celles et ceux qui le souhaitent, je vous propose ici des pistes pour bien tester vos programmes (vous pouvez vous répartir le travail ; il n'est pas nécessaire que ce soit celui/celle qui fait la classe Systeme qui écrive le code des tests !) :
Pour vérifiez vos programmes, je vous conseille deux méthodes complémentaires (c.-à-d. faites les deux !) :
vérifier localement, ponctuellement sur quelques valeurs exactes, comme celles données ici ou sur des calculs de votre choix, que les résultats correspondent ;
vérifier globalement sur des cas connus que le comportement à long terme est cohérent. Par exemple dans le premier cas ci-dessous, vous savez que la trajectoire doit être une parabole dans le second cas, que la situation d'équilibre est atteinte (et fait sens).
Le plus simple pour cette seconde méthode est d'utiliser un outil de dessin externe, comme par exemple gnuplot, un programme qui permet de dessiner facilement des données. Pour celles et ceux que cela intéresse, je détaille ci-dessous comment faire.
Commencez par produire un programme de test chute_libre.cc dans lequel est créé
un système ayant une particule ayant les caractéristiques indiquées ci-dessous, et un pas de temps de 0.01 s.
Déplacez cette particule sur plusieurs pas de temps consécutifs. Vous devriez trouver les résultats suivants :
Le système est composé des 1 particules suivantes : [ pos= (0 0 0), v= (5 0 7), m= 0.268083, r= 0.4 ]
puis l'évolution (détaillée dans ce fichier ci) :
[ pos= (0 0 0), v= (5 0 7), m= 0.268083, r= 0.4 ] [ pos= (0.0497469 0 -0.911354), v= (4.97469 0 -91.1354), m= 0.268083, r= 0.4 ] [ pos= (0.0992419 0 -2.7991), v= (4.9495 0 -188.774), m= 0.268083, r= 0.4 ] [ pos= (0.148486 0 -5.65828), v= (4.92445 0 -285.918), m= 0.268083, r= 0.4 ] [ pos= (0.197482 0 -9.48399), v= (4.89952 0 -382.571), m= 0.268083, r= 0.4 ] [ pos= (0.246229 0 -14.2713), v= (4.87471 0 -478.734), m= 0.268083, r= 0.4 ] ...
Pensez également à tester plusieurs cas : vitesse initiale nulle, uniquement vers le haut, λ non nul...
gnuplot est un programme (à lancer depuis la ligne de commande) de dessin de données et de fonctions. Pour dessiner des données, il faut d'abord les avoir dans un fichier. gnuplot lit des fichiers dont chaque ligne contient un point à dessiner, les coordonnées étant par colonnes. Par exemple (le caractère # indique des commentaires en gnuplot) :
# x y 2.2 4.4 # point 1 2.3 5.5 # point 2 1.3 3.3 # point 3 # etc..
Il vous faut donc tout d'abord créer un tel fichier. Il y a plein de façons de faire, mais pour votre projet j'en vois essentiellement deux :
Pour la première façon, il suffit de lancer votre programme dans la ligne de commande et d'utiliser « des petits programmes Unix » pour faire le travail. Par exemple avec la sortie de test donnée plus haut, on pourrait faire (dans un terminal) :
./chute_libre | grep '^\[ pos'
qui ne laissera plus sortir que les lignes commençant par la chaîne « [ pos ».
Pour mettre les positions des particules dans un fichier test.txt, il suffit alors de faire encore un peu de filtrage, par exemple comme ceci (copiez-collez à la souris) :
./chute_libre | grep '^\[ pos' | cut -d= -f2 | sed -e 's/^ (//' -e 's/), v$//' > test.txt
C'est aussi simple que cela ! Avec ça, vous devriez avoir un fichier test.txt dans le répertoire courant, contenant :
0 0 0 0.0497469 0 -0.911354 0.0992419 0 -2.7991 0.148486 0 -5.65828 0.197482 0 -9.48399 0.246229 0 -14.2713 0.294729 0 -20.0154 0.342984 0 -26.7115 0.390994 0 -34.3546 0.438762 0 -42.94 0.486287 0 -52.463 0.533565 0 -62.9172 0.580571 0 -74.2925 0.627284 0 -86.5778 0.673683 0 -99.7614
qui est au bon format pour gnuplot.
Si ce n'est pas le cas de vos programmes, en raison d'un affichage différent, je vous propose de modifier les affichages de vos programme :
soit pour que la commande ci-dessus fontionne ;
Une fois que vous avez un tel fichier (bon pour gnuplot), il suffit de lancer la commande gnuplot dans le terminal, puis, dans gnuplot, de taper
plot "test.txt" u 1:3 w linesp
Le « u 1:3 » signifie que l'on utilise la 1ere et la 3e colonne pour dessiner. Sinon gnuplot utilise les deux premières (c.-à-d. il fait un « u 1:2 » par défaut), ce qui ne nous intéresse pas trop ici.
Pour comparer vos données à une courbe connue, par exemple la parabole ici, vous pouvez faire (toujours dans gnuplot) :
plot "test.txt" u 1:3 w linesp, -196.2*x*x+1.4*x, -211*x*x-5.6*x
(note : 196 = 9.81e3 / (2 * 5 * 5) et 1.4 = 7 / 5) qui vous donnera quelque chose comme cela :
![[Figure : parabole + données]](pict/gnuplot-example.png)
(J'ai déplacé la légende avec la commande gnuplot : set key bottom left.)
Vous pouvez aussi dessiner en 3D avec splot (toujours dans gnuplot) :
splot "test.txt" w linesp
en cliquant sur l'image et bougeant la souris, vous pouvez changer le point de vue.
Vous pouvez bien sûr le faire avec beaucoup beaucoup plus de points, ce qui permet d'avoir une vue globale de l'évolution du système.
Par exemple sur le résultat de exerciceP9 vous pouvez faire ceci&nsp;:
./exerciceP9 | grep '^\[ pos' > output.txt ## infos sur les particules
cut -d= -f2 output.txt | sed -e 's/^ (//' -e 's/), v$$//' > particules-pos.txt ## positions des particules
for i in $(seq 4); do sed -n "${i}~4p" particules-pos.txt > test2-particule${i}.txt; done ## séparation des 4 particules
et dans gnuplot :
set style data linesp splot "test2-particule1.txt", "test2-particule2.txt", "test2-particule3.txt", "test2-particule4.txt"
pour obtenir ceci :
![[Figure : données de P9 en 3D]](pict/P9.png)
Pour quitter gnuplot : tapez simplement quit ou 'Control-D'.