<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Calcul Haute Performance | Valentin Caries</title><link>https://vcaries.github.io/tag/calcul-haute-performance/</link><atom:link href="https://vcaries.github.io/tag/calcul-haute-performance/index.xml" rel="self" type="application/rss+xml"/><description>Calcul Haute Performance</description><generator>Hugo Blox Builder (https://hugoblox.com)</generator><language>fr-fr</language><lastBuildDate>Fri, 12 Jun 2026 00:00:00 +0000</lastBuildDate><image><url>https://vcaries.github.io/media/icon_hu536d0651d21529ac336731b24f3e073f_144279_512x512_fill_lanczos_center_3.png</url><title>Calcul Haute Performance</title><link>https://vcaries.github.io/tag/calcul-haute-performance/</link></image><item><title>Web-LBM : une soufflerie CFD temps réel dans le navigateur</title><link>https://vcaries.github.io/project/web-lbm/</link><pubDate>Fri, 12 Jun 2026 00:00:00 +0000</pubDate><guid>https://vcaries.github.io/project/web-lbm/</guid><description>&lt;h2 id="en-une-minute">En une minute&lt;/h2>
&lt;p>&lt;strong>Web-LBM&lt;/strong> est une soufflerie numérique bidimensionnelle interactive qui s&amp;rsquo;exécute entièrement dans votre navigateur. Derrière le canvas se trouve un véritable solveur de &lt;strong>mécanique des fluides numérique (CFD)&lt;/strong> : un moteur &lt;strong>Boltzmann sur réseau (D2Q9)&lt;/strong> écrit en C, compilé en &lt;strong>WebAssembly&lt;/strong> et rendu sur le GPU via &lt;strong>WebGL2&lt;/strong>. On peut y déposer un cylindre ou un profil NACA, changer l&amp;rsquo;angle d&amp;rsquo;attaque, mettre un obstacle en rotation pour ressentir l&amp;rsquo;effet Magnus, ou peindre sa propre géométrie à la souris et observer la réponse du sillage. Les coefficients de portance et de traînée s&amp;rsquo;actualisent en direct, et des microphones virtuels permettent même de &lt;em>voir le son&lt;/em> du lâcher tourbillonnaire.&lt;/p>
&lt;p>La CFD vit d&amp;rsquo;ordinaire sur des clusters et des calculs de nuit. L&amp;rsquo;amener dans un onglet de navigateur, sans installation, sans serveur, sans framework, et hébergée en simples fichiers statiques sur GitHub Pages, est à la fois un parti pris pédagogique et un exercice d&amp;rsquo;ingénierie. Chaque contrainte de la plateforme a dû être résolue sans sacrifier ni la fidélité physique ni l&amp;rsquo;interactivité : pas de mémoire partagée par défaut, pas de système de fichiers, et un seul thread sauf à le mériter.&lt;/p>
&lt;p>👉 &lt;strong>&lt;a href="https://vcaries.github.io/web_lbm/" target="_blank" rel="noopener">Essayer la simulation&lt;/a>&lt;/strong> · &lt;strong>&lt;a href="https://github.com/vcaries/web_lbm" target="_blank" rel="noopener">Code source sur GitHub&lt;/a>&lt;/strong>&lt;/p>
&lt;h3 id="points-clés">Points clés&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Un vrai solveur.&lt;/strong> Boltzmann sur réseau D2Q9 avec collision BGK (ou MRT à la compilation), un modèle de turbulence &lt;strong>LES de Smagorinsky&lt;/strong>, une collision régularisée de Hermite pour la stabilité, une entrée en vitesse de Zou–He, des couches éponges absorbantes et une évaluation des efforts par échange de quantité de mouvement qui produit des $C_l$ et $C_d$ en direct.&lt;/li>
&lt;li>&lt;strong>Des performances temps réel.&lt;/strong> Une boucle chaude sans branchement et vectorisée SIMD, associée à des &lt;strong>threads WebAssembly&lt;/strong> (pthreads sur &lt;code>SharedArrayBuffer&lt;/code>) et à un partitionnement par lignes déterministe et sans verrou.&lt;/li>
&lt;li>&lt;strong>Un rendu sans copie.&lt;/strong> Les champs voyagent du tas wasm jusqu&amp;rsquo;à l&amp;rsquo;écran sans la moindre copie intermédiaire : des vues typées alimentent une texture WebGL2 &lt;code>R32F&lt;/code>, et une table de couleurs est appliquée dans le fragment shader.&lt;/li>
&lt;li>&lt;strong>L&amp;rsquo;aéroacoustique en prime.&lt;/strong> La LBM étant faiblement compressible, le moteur résout les ondes acoustiques. Il propose des vues schlieren ($|\nabla\rho|$) et dilatation ($\nabla\cdot\mathbf{u}$), plus des microphones virtuels avec FFT en direct qui captent le sifflement éolien du sillage.&lt;/li>
&lt;li>&lt;strong>Un déploiement 100 % statique.&lt;/strong> Deux variantes du moteur (mono- et multi-thread) sont sélectionnées à l&amp;rsquo;exécution, et un service worker fournit les en-têtes d&amp;rsquo;isolation cross-origin que GitHub Pages ne peut pas envoyer.&lt;/li>
&lt;/ul>
&lt;h2 id="pourquoi-la-cfd-dans-le-navigateur-">Pourquoi la CFD dans le navigateur ?&lt;/h2>
&lt;p>Trois raisons, par ordre de difficulté croissante.&lt;/p>
&lt;p>&lt;strong>La pédagogie.&lt;/strong> La mécanique des fluides s&amp;rsquo;enseigne avec des équations et se valide avec des simulations coûteuses, mais &lt;em>l&amp;rsquo;intuition&lt;/em> naît du lien immédiat entre cause et effet : inclinez un profil et observez le décrochage, faites tourner un cylindre et regardez la portance apparaître. Une simulation interactive dont la boucle de rétroaction se mesure en millisecondes est un outil d&amp;rsquo;apprentissage fondamentalement différent d&amp;rsquo;une figure statique.&lt;/p>
&lt;p>&lt;strong>L&amp;rsquo;accessibilité.&lt;/strong> Une page web sans installation ni backend touche tout le monde (étudiants, collègues, recruteurs) sur n&amp;rsquo;importe quelle machine, indéfiniment, pour un coût d&amp;rsquo;hébergement nul. Cela impose que tout le solveur tourne côté client, exactement ce que WebAssembly rend possible.&lt;/p>
&lt;p>&lt;strong>Le défi d&amp;rsquo;ingénierie.&lt;/strong> La CFD temps réel met toute la pile à l&amp;rsquo;épreuve d&amp;rsquo;un seul coup : une méthode numérique assez stable pour survivre à tout ce que l&amp;rsquo;utilisateur lui impose, une organisation mémoire que le compilateur sait vectoriser, un modèle de threading compatible avec les contraintes de sécurité du navigateur et un chemin de rendu assez rapide pour ne jamais devenir le goulot d&amp;rsquo;étranglement. Résoudre les quatre à la fois est le cœur de ce projet.&lt;/p>
&lt;h2 id="la-physique--boltzmann-sur-réseau-en-bref">La physique : Boltzmann sur réseau en bref&lt;/h2>
&lt;h3 id="un-autre-chemin-vers-la-dynamique-des-fluides">Un autre chemin vers la dynamique des fluides&lt;/h3>
&lt;p>La CFD classique discrétise directement les &lt;strong>équations de Navier–Stokes&lt;/strong> : vitesse et pression vivent sur un maillage, et chaque pas de temps exige la résolution de systèmes couplés &lt;em>globaux&lt;/em>, typiquement une équation de Poisson pour la pression, dont la solution relie chaque cellule du domaine à toutes les autres.&lt;/p>
&lt;p>La &lt;strong>méthode de Boltzmann sur réseau (LBM)&lt;/strong> travaille un niveau en dessous de la description macroscopique. Elle fait évoluer des &lt;strong>fonctions de distribution de particules&lt;/strong> $f_i(\mathbf{x}, t)$, la quantité de fluide au nœud $\mathbf{x}$ se déplaçant le long d&amp;rsquo;un petit ensemble de vitesses discrètes $\mathbf{c}_i$. Les champs macroscopiques sont simplement les moments de ces distributions :&lt;/p>
$$\rho = \sum_i f_i, \qquad \rho\,\mathbf{u} = \sum_i f_i\,\mathbf{c}_i$$
&lt;p>Un développement de Chapman–Enskog montre que ce système cinétique reproduit les équations de Navier–Stokes incompressibles dans la limite des faibles nombres de Mach. La viscosité du fluide est pilotée par un unique temps de relaxation $\tau$ :&lt;/p>
$$\nu = c_s^2\left(\tau - \tfrac{1}{2}\right)$$
&lt;p>Ce qui rend la LBM exceptionnelle pour ce projet, c&amp;rsquo;est sa &lt;strong>localité&lt;/strong> : chaque nœud ne se met à jour qu&amp;rsquo;à partir de ses voisins immédiats. Pas de résolution globale, pas d&amp;rsquo;algèbre linéaire, pas de boucle de convergence. Le coût par pas de temps est donc fixe et prévisible, l&amp;rsquo;algorithme se parallélise presque trivialement, et la simulation est &lt;em>naturellement instationnaire&lt;/em>. Le lâcher tourbillonnaire n&amp;rsquo;est pas une option que l&amp;rsquo;on active ; c&amp;rsquo;est ce que la méthode calcule par défaut. En bonus, la LBM est faiblement compressible : les ondes de pression se propagent physiquement, et la simulation fait de l&amp;rsquo;acoustique gratuitement.&lt;/p>
&lt;h3 id="collision-et-propagation">Collision et propagation&lt;/h3>
&lt;p>Chaque pas de temps se ramène à deux opérations conceptuellement simples. La &lt;strong>collision&lt;/strong> relaxe les distributions vers un équilibre local (approximation BGK) :&lt;/p>
$$f_i(\mathbf{x}, t^{+}) = f_i(\mathbf{x}, t) - \frac{1}{\tau}\left[f_i(\mathbf{x}, t) - f_i^{\mathrm{eq}}(\rho, \mathbf{u})\right]$$
&lt;p>où l&amp;rsquo;équilibre est un développement au second ordre de la distribution de Maxwell–Boltzmann :&lt;/p>
$$f_i^{\mathrm{eq}} = w_i\,\rho\left[1 + \frac{\mathbf{c}_i\cdot\mathbf{u}}{c_s^2} + \frac{(\mathbf{c}_i\cdot\mathbf{u})^2}{2c_s^4} - \frac{\mathbf{u}\cdot\mathbf{u}}{2c_s^2}\right]$$
&lt;p>La &lt;strong>propagation&lt;/strong> (&lt;em>streaming&lt;/em>) décale ensuite chaque distribution post-collision vers le nœud voisin dans sa direction de déplacement :&lt;/p>
$$f_i(\mathbf{x} + \mathbf{c}_i\,\Delta t,\; t + \Delta t) = f_i(\mathbf{x}, t^{+})$$
&lt;p>La collision est de l&amp;rsquo;arithmétique purement locale ; la propagation, un pur déplacement mémoire. Du calcul intensif sans communication, suivi de la communication sans calcul, est exactement la structure qu&amp;rsquo;adorent les unités SIMD et les processeurs multicœurs, et l&amp;rsquo;implémentation décrite plus bas l&amp;rsquo;exploite délibérément.&lt;/p>
&lt;h3 id="pourquoi-d2q9">Pourquoi D2Q9&lt;/h3>
&lt;p>Le moteur utilise le réseau &lt;strong>D2Q9&lt;/strong> : deux dimensions et neuf vitesses (repos, quatre axiales, quatre diagonales) avec les poids $w_0 = 4/9$, $w_{1\text{–}4} = 1/9$ et $w_{5\text{–}8} = 1/36$. C&amp;rsquo;est le plus petit réseau 2D dont les moments de vitesse sont suffisamment isotropes pour retrouver correctement Navier–Stokes. Moins de directions biaiseraient la physique le long des axes de la grille, et davantage coûteraient mémoire et bande passante sans gain à ce niveau de description. Neuf champs &lt;code>float&lt;/code> par nœud fixent aussi le budget mémoire : une grille 600 × 300 tient confortablement dans quelques dizaines de mégaoctets de tas wasm.&lt;/p>
&lt;h3 id="conditions-aux-limites">Conditions aux limites&lt;/h3>
&lt;p>Les frontières sont l&amp;rsquo;endroit où une implémentation LBM fait ses preuves, car après la propagation les distributions entrant dans le domaine depuis l&amp;rsquo;extérieur sont inconnues et doivent être reconstruites.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Entrée.&lt;/strong> Une condition en vitesse de &lt;strong>Zou–He&lt;/strong> impose le profil d&amp;rsquo;entrée (uniforme, couche cisaillée ou jet) en reconstruisant les distributions inconnues à partir des contraintes de masse et de quantité de mouvement. Un démarrage progressif sur 400 pas accompagne chaque (ré)initialisation, pour que le démarrage impulsif ne lance pas une onde de choc dans la veine.&lt;/li>
&lt;li>&lt;strong>Sortie.&lt;/strong> Une extrapolation à gradient nul, adossée à une &lt;strong>couche éponge absorbante&lt;/strong> qui augmente progressivement la dissipation près de la frontière, pour que tourbillons et ondes acoustiques quittent le domaine au lieu de s&amp;rsquo;y réfléchir.&lt;/li>
&lt;li>&lt;strong>Obstacles.&lt;/strong> Le &lt;strong>rebond (&lt;em>bounce-back&lt;/em>)&lt;/strong> renvoie d&amp;rsquo;où elles viennent les distributions qui frappent un nœud solide, produisant une paroi adhérente sans aucun maillage. Sa variante à &lt;strong>paroi mobile&lt;/strong> ajoute la quantité de mouvement de la paroi aux populations réfléchies, ce qui constitue toute l&amp;rsquo;implémentation du cylindre tournant, l&amp;rsquo;effet Magnus découlant alors de la physique. La somme des quantités de mouvement échangées dans ces réflexions donne l&amp;rsquo;effort aérodynamique sur chaque corps (les $C_l$ et $C_d$ affichés en direct) pour un coût quasi nul.&lt;/li>
&lt;li>&lt;strong>Parois de la veine.&lt;/strong> Au choix : glissement libre (réflexion spéculaire) ou adhérence (rebond).&lt;/li>
&lt;/ul>
&lt;h2 id="mise-en-œuvre">Mise en œuvre&lt;/h2>
&lt;h3 id="architecture">Architecture&lt;/h3>
&lt;p>Le projet se compose de deux parties proprement séparées, reliées par un unique tampon mémoire partagé :&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Moteur C (lbm.c, C99) --emcc--&amp;gt; WebAssembly + tas wasm
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ vues Float32Array sans copie
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Frontend ES6 vanilla ── textures WebGL2 R32F ──&amp;gt; LUT de couleurs dans le shader
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;strong>Le moteur&lt;/strong> est un unique fichier C99 réunissant le solveur D2Q9, la LES, les conditions aux limites, les rastériseurs analytiques de formes (cylindre, plaque plane, NACA 0012 / 4412), l&amp;rsquo;évaluation des efforts et le calcul des champs dérivés (vitesse, pression, vorticité, schlieren, dilatation). Il expose une petite API C plate (&lt;code>lbm_init&lt;/code>, &lt;code>lbm_step&lt;/code>, &lt;code>lbm_set_params&lt;/code>, …) et ignore tout de JavaScript.&lt;/li>
&lt;li>&lt;strong>Le frontend&lt;/strong> est en modules ES6 vanilla, sans framework, sans bundler et sans aucune étape de build au-delà d&amp;rsquo;&lt;code>emcc&lt;/code> lui-même. &lt;code>main.js&lt;/code> pilote la boucle de simulation et l&amp;rsquo;interface, &lt;code>renderer.js&lt;/code> possède WebGL2, et &lt;code>obstacles.js&lt;/code> définit les formes analytiques.&lt;/li>
&lt;li>&lt;strong>Deux variantes du moteur&lt;/strong> sont produites à partir de la même source : une version mono-thread qui fonctionne partout, et une version &lt;code>-pthread&lt;/code> utilisant les threads WebAssembly. L&amp;rsquo;application détecte &lt;code>crossOriginIsolated&lt;/code> au démarrage, choisit la meilleure, et assure un repli transparent.&lt;/li>
&lt;/ul>
&lt;h3 id="le-chemin-de-données-sans-copie">Le chemin de données sans copie&lt;/h3>
&lt;p>La règle cardinale du pipeline de rendu : &lt;strong>les champs ne sont jamais copiés côté JavaScript&lt;/strong>. Le moteur C calcule par exemple la vorticité dans un tableau &lt;code>float&lt;/code> du tas wasm. JavaScript enveloppe cette mémoire dans une &lt;em>vue&lt;/em> &lt;code>Float32Array&lt;/code> (aucune allocation, aucune copie) et la passe directement à &lt;code>texSubImage2D&lt;/code>, qui la téléverse dans une texture flottante mono-canal (&lt;code>R32F&lt;/code>). Le fragment shader fait ensuite correspondre les valeurs aux couleurs via une petite texture de table de correspondance, si bien que tout l&amp;rsquo;habillage visuel coûte une lecture de texture par pixel et vit sur le GPU.&lt;/p>
&lt;p>Deux subtilités maintiennent cette promesse. Quand le tas wasm grandit (l&amp;rsquo;utilisateur choisit une grille plus fine), l&amp;rsquo;&lt;code>ArrayBuffer&lt;/code> sous-jacent se détache et chaque vue doit être reconstruite, ce que l&amp;rsquo;application fait paresseusement, à la demande. Et dans la version multithread, le tas est un &lt;code>SharedArrayBuffer&lt;/code>, que certains navigateurs refusent de téléverser directement ; un unique petit tampon de transit constitue la seule exception sanctionnée à la règle du zéro copie.&lt;/p>
&lt;h3 id="du-multithreading-dans-un-navigateur">Du multithreading dans un navigateur&lt;/h3>
&lt;p>Le balayage intérieur est &lt;strong>partitionné par lignes&lt;/strong> sur un pool de pthreads wasm, avec des barrières séparant les phases de chaque pas de temps. Le partitionnement garantit que chaque case du tableau de destination a &lt;em>exactement un&lt;/em> producteur. Les threads ne sont jamais en concurrence, donc ni verrous ni atomiques dans le chemin chaud. Les contributions aux efforts sont réduites dans un ordre de threads fixe, ce qui rend les résultats &lt;strong>déterministes au bit près à nombre de threads donné&lt;/strong>, une exigence empruntée aux bonnes pratiques du HPC, où « rapide mais légèrement différent à chaque exécution » est la cachette préférée des bugs.&lt;/p>
&lt;p>Le threading en navigateur a un piège : &lt;code>SharedArrayBuffer&lt;/code> exige que la page soit &lt;strong>isolée cross-origin&lt;/strong>, ce qui requiert des en-têtes HTTP COOP/COEP qu&amp;rsquo;un hébergeur statique comme GitHub Pages ne peut pas envoyer. Web-LBM résout cela avec un &lt;strong>service worker&lt;/strong> de portée racine qui injecte les en-têtes côté client (un rechargement automatique à la première visite). Si quoi que ce soit échoue dans cette chaîne (navigateur ancien, isolation partielle), l&amp;rsquo;application démarre silencieusement le moteur mono-thread. C&amp;rsquo;est l&amp;rsquo;amélioration progressive appliquée au HPC.&lt;/p>
&lt;h3 id="interaction">Interaction&lt;/h3>
&lt;p>Tout, dans la soufflerie, se manipule pendant que la simulation tourne. Les obstacles se placent au clic, pivotent, tournent sur eux-mêmes (cylindres en rotation) ou se &lt;strong>peignent à main levée&lt;/strong> à la souris, directement dans le masque d&amp;rsquo;obstacles. Les cellules peintes sont rééchantillonnées quand la résolution de grille change, tandis que les formes prédéfinies sont re-rastérisées &lt;em>analytiquement&lt;/em> depuis leur registre, si bien qu&amp;rsquo;un profil NACA reste un profil NACA net à n&amp;rsquo;importe quelle résolution. L&amp;rsquo;écoulement se visualise en vitesse, pression, vorticité, schlieren ou dilatation, avec des surcouches de lignes de courant et de particules. Des &lt;strong>microphones virtuels&lt;/strong> se déposent n&amp;rsquo;importe où dans le champ pour tracer la FFT en direct du signal de pression local ; en placer un derrière un cylindre révèle le pic spectral net de la fréquence de lâcher, la même physique que le sifflement du vent dans les fils.&lt;/p>
&lt;h2 id="défis-dingénierie">Défis d&amp;rsquo;ingénierie&lt;/h2>
&lt;h3 id="la-stabilité-numérique-sans-arbitre">La stabilité numérique sans arbitre&lt;/h3>
&lt;p>Une simulation interactive ne peut pas refuser les entrées de l&amp;rsquo;utilisateur : quels que soient le nombre de Reynolds, la forme d&amp;rsquo;obstacle ou la résolution choisis, le solveur ne doit pas diverger. Le BGK brut devient instable quand $\tau \to 1/2$ (hauts nombres de Reynolds) ; le moteur superpose donc plusieurs défenses, chacune visant un mode de défaillance distinct.&lt;/p>
&lt;ul>
&lt;li>Un &lt;strong>modèle LES de Smagorinsky&lt;/strong> ($C_s = 0{,}18$) calcule une viscosité turbulente locale à partir du taux de déformation (obtenu en LBM directement depuis les distributions hors équilibre, sans différences finies). Il modélise à la fois les échelles turbulentes non résolues et éloigne le $\tau$ effectif de la limite instable précisément là où l&amp;rsquo;écoulement est le plus cisaillé.&lt;/li>
&lt;li>La &lt;strong>régularisation de Hermite&lt;/strong> projette la partie hors équilibre des distributions sur sa composante physiquement signifiante (Hermite d&amp;rsquo;ordre deux) avant la collision, filtrant les modes parasites d&amp;rsquo;ordre supérieur qui, sinon, s&amp;rsquo;accumulent et déstabilisent les grilles grossières.&lt;/li>
&lt;li>Un &lt;strong>amortissement de viscosité de volume&lt;/strong> dissipe sélectivement la partie acoustique du tenseur des contraintes, calmant les oscillations de pression sans toucher à la physique du cisaillement qui crée les tourbillons.&lt;/li>
&lt;li>Les &lt;strong>couches éponges et le démarrage progressif de l&amp;rsquo;entrée&lt;/strong> éliminent les deux transitoires classiques : les ondes réfléchies en sortie et l&amp;rsquo;impulsion initiale.&lt;/li>
&lt;/ul>
&lt;p>Le résultat est une soufflerie qui survit aux mauvais traitements, ce qui fait la différence entre une démo et un produit.&lt;/p>
&lt;h2 id="ce-que-vous-pouvez-explorer">Ce que vous pouvez explorer&lt;/h2>
&lt;p>Quelques expériences d&amp;rsquo;une minute chacune dans la &lt;a href="https://vcaries.github.io/web_lbm/" target="_blank" rel="noopener">soufflerie en ligne&lt;/a> :&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Le lâcher tourbillonnaire.&lt;/strong> Gardez le cylindre par défaut, passez en vue vorticité et regardez l&amp;rsquo;allée de von Kármán s&amp;rsquo;établir ; le HUD montre $C_l$ oscillant à la fréquence de lâcher.&lt;/li>
&lt;li>&lt;strong>Le décrochage.&lt;/strong> Placez un profil NACA 4412 et augmentez l&amp;rsquo;angle d&amp;rsquo;attaque : le coefficient de portance grimpe, puis l&amp;rsquo;écoulement décolle à l&amp;rsquo;extrados.&lt;/li>
&lt;li>&lt;strong>L&amp;rsquo;effet Magnus.&lt;/strong> Faites tourner un cylindre et observez le sillage dévier et une portance stationnaire apparaître, la physique des coups francs enroulés.&lt;/li>
&lt;li>&lt;strong>L&amp;rsquo;aéroacoustique.&lt;/strong> Passez en vue schlieren ou dilatation pour voir les ondes de pression rayonner depuis le sillage, puis déposez un microphone derrière le cylindre et retrouvez le ton de lâcher dans le spectre en direct.&lt;/li>
&lt;li>&lt;strong>Votre propre géométrie.&lt;/strong> Peignez un obstacle arbitraire à la souris et regardez l&amp;rsquo;écoulement composer avec.&lt;/li>
&lt;/ol>
&lt;h2 id="compétences-démontrées">Compétences démontrées&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Mécanique des fluides numérique :&lt;/strong> la méthode de Boltzmann sur réseau de bout en bout, incluant le choix du réseau, les modèles de collision (BGK, MRT, régularisation), les conditions aux limites, la modélisation de la turbulence (LES de Smagorinsky), l&amp;rsquo;évaluation des efforts et l&amp;rsquo;aéroacoustique.&lt;/li>
&lt;li>&lt;strong>Méthodes numériques et calcul scientifique :&lt;/strong> l&amp;rsquo;analyse de stabilité en pratique, c&amp;rsquo;est-à-dire identifier les modes de défaillance et superposer des remèdes ciblés, dans un moteur physique en C99 portable avec tests de validation embarqués.&lt;/li>
&lt;li>&lt;strong>Ingénierie de la performance :&lt;/strong> des noyaux sans branchement adaptés au SIMD, une organisation mémoire en structure de tableaux, un multithreading déterministe sans verrou et une séparation des chemins chauds et froids guidée par le profilage.&lt;/li>
&lt;li>&lt;strong>Architecture logicielle :&lt;/strong> une frontière stricte moteur/frontend avec un contrat de données zéro copie à travers la frontière C/JavaScript, et une détection de capacités à l&amp;rsquo;exécution avec dégradation élégante.&lt;/li>
&lt;li>&lt;strong>Technologies web :&lt;/strong> WebAssembly (Emscripten, threads wasm, mémoire partagée), WebGL2 (textures flottantes, colorisation dans le shader), service workers et déploiement entièrement statique.&lt;/li>
&lt;li>&lt;strong>Visualisation scientifique interactive et conception d&amp;rsquo;interface :&lt;/strong> rendu de champs en temps réel, vues multiples physiquement signifiantes, et une interface qui invite à l&amp;rsquo;expérimentation sans mode d&amp;rsquo;emploi.&lt;/li>
&lt;li>&lt;strong>Communication scientifique :&lt;/strong> rendre tangible, et ludique, une mécanique des fluides de niveau master pour un public technique généraliste.&lt;/li>
&lt;/ul>
&lt;h2 id="dépôt">Dépôt&lt;/h2>
&lt;p>Tout le code est ouvert : le moteur C (abondamment commenté, jusqu&amp;rsquo;à chaque drapeau de compilation et sa justification), le moteur de rendu WebGL2, les scripts de build et les tests physiques de validation.&lt;/p>
&lt;p>Le code vit sur &lt;strong>&lt;a href="https://github.com/vcaries/web_lbm" target="_blank" rel="noopener">github.com/vcaries/web_lbm&lt;/a>&lt;/strong>, et la soufflerie elle-même tourne sur &lt;strong>&lt;a href="https://vcaries.github.io/web_lbm/" target="_blank" rel="noopener">vcaries.github.io/web_lbm&lt;/a>&lt;/strong>.&lt;/p>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>Web-LBM condense tout l&amp;rsquo;éventail de l&amp;rsquo;ingénierie de simulation dans une seule page que chacun peut ouvrir : une méthode numérique cinétique, la modélisation de la turbulence, l&amp;rsquo;optimisation SIMD et multithread, le rendu GPU et une interaction de qualité produit. La méthode de Boltzmann sur réseau est le choix qui rend tout possible, car elle est locale, parallèle, naturellement instationnaire et discrètement capable d&amp;rsquo;acoustique. Le navigateur est la contrainte qui structure tout, puisque chacune de ses limites a dû être contournée par l&amp;rsquo;ingénierie plutôt qu&amp;rsquo;éludée. Le résultat : une soufflerie dans un onglet, et la démonstration que le &lt;em>vrai&lt;/em> calcul scientifique n&amp;rsquo;est pas condamné à vivre derrière une file d&amp;rsquo;attente sur un cluster.&lt;/p></description></item><item><title>BoidForge : ingénierie des comportements collectifs du Python au C natif</title><link>https://vcaries.github.io/project/boid-forge/</link><pubDate>Fri, 05 Jun 2026 00:00:00 +0000</pubDate><guid>https://vcaries.github.io/project/boid-forge/</guid><description>&lt;h2 id="en-une-minute">En une minute&lt;/h2>
&lt;p>&lt;strong>BoidForge&lt;/strong> est un simulateur de nuées 2D bâti autour d&amp;rsquo;un principe unique : une nuée est &lt;em>calculée&lt;/em> une fois puis &lt;em>rendue&lt;/em> ensuite, jamais les deux en même temps. Un &lt;strong>solveur&lt;/strong> fait avancer la physique et écrit chaque pas de temps dans un fichier binaire compact. Un &lt;strong>moteur de visualisation&lt;/strong> distinct rejoue ensuite ce fichier sur le GPU. Les deux moitiés ne partagent aucune mémoire, seulement un format sur disque, de sorte que chacune peut être optimisée séparément.&lt;/p>
&lt;p>Il en résulte un projet à parts égales calcul numérique et graphisme temps réel. Le même modèle de nuée est implémenté dans &lt;strong>quatre backends interchangeables&lt;/strong>, depuis une référence Python naïve jusqu&amp;rsquo;à une &lt;strong>extension C&lt;/strong> écrite à la main, et tous les quatre produisent des résultats strictement identiques jusqu&amp;rsquo;au dernier bit. Un moteur de rendu &lt;strong>ModernGL&lt;/strong> transforme ensuite les trajectoires brutes en la séquence cinématographique ci-dessous.&lt;/p>
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
&lt;iframe src="https://www.youtube.com/embed/5iVkLb3eyhE" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" allowfullscreen title="YouTube Video">&lt;/iframe>
&lt;/div>
&lt;figcaption style="text-align:center;font-size:0.9em;margin-top:0.5em;">&lt;strong>Vidéo.&lt;/strong> La scène &lt;em>turbulence&lt;/em>, rendue hors-ligne en 4K à 60&amp;nbsp;ips et colorée par la vitesse. Elle a été produite en rejouant une simulation enregistrée dans le moteur de visualisation GPU décrit plus bas.&lt;/figcaption>
&lt;p>👉 &lt;strong>&lt;a href="https://github.com/vcaries/boid_forge" target="_blank" rel="noopener">Code source sur GitHub&lt;/a>&lt;/strong>&lt;/p>
&lt;h3 id="points-forts">Points forts&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Un modèle, quatre backends, une sortie identique.&lt;/strong> Python naïf (L1), NumPy vectorisé (L2), un hachage spatial sur grille uniforme (L3) et un noyau natif C/CPython (L4), tous identiques bit à bit sous un contrat de déterminisme strict.&lt;/li>
&lt;li>&lt;strong>Jusqu&amp;rsquo;à environ 330 fois plus rapide.&lt;/strong> Le backend C atteint environ 120 à 330 fois le débit de la référence naïve sur la plage testée, et est conçu pour passer à l&amp;rsquo;échelle de dizaines de milliers d&amp;rsquo;agents.&lt;/li>
&lt;li>&lt;strong>Visualisation GPU en ModernGL.&lt;/strong> Sprites instanciés, accumulation HDR additive, traînées de mouvement, bloom et tonemapping ACES, rendus sans affichage et transmis directement à FFmpeg pour l&amp;rsquo;export 4K.&lt;/li>
&lt;li>&lt;strong>Reproductible par construction.&lt;/strong> Chaque résultat est une fonction pure de la configuration et d&amp;rsquo;une graine, si bien que les mêmes entrées donnent les mêmes octets sur n&amp;rsquo;importe quelle machine.&lt;/li>
&lt;/ul>
&lt;h2 id="inspirations">Inspirations&lt;/h2>
&lt;p>Ce projet est né de deux références de la culture scientifique et technique en ligne. La première est &lt;strong>Smarter Every Day&lt;/strong>, dont la &lt;a href="https://www.youtube.com/watch?v=4LWmRuB-uNU" target="_blank" rel="noopener">vidéo sur les murmurations d&amp;rsquo;étourneaux&lt;/a> montre comment des milliers d&amp;rsquo;oiseaux peuvent se comporter comme un seul corps fluide, et pose la question naturelle de la façon dont un tel ordre peut émerger sans chef et sans plan. La seconde est &lt;strong>Sebastian Lague&lt;/strong> et sa série &lt;em>Coding Adventures&lt;/em>, dont l&amp;rsquo;&lt;a href="https://www.youtube.com/watch?v=bqtqltqcQhw" target="_blank" rel="noopener">épisode consacré aux boids&lt;/a> montre comment quelques règles locales se traduisent en code et combien de beauté visuelle une simulation simple peut contenir. BoidForge est ma manière de prendre cette curiosité au sérieux en ingénieur : reproduire le phénomène fidèlement, le pousser à une échelle où l&amp;rsquo;émergence devient évidente, et le rendre assez bien pour rendre justice aux véritables murmurations.&lt;/p>
&lt;h2 id="le-modele">Le modèle de nuée&lt;/h2>
&lt;p>Le comportement de nuée est un phénomène &lt;em>émergent&lt;/em>. Il n&amp;rsquo;y a ni chef ni plan global, et pourtant des milliers d&amp;rsquo;agents s&amp;rsquo;organisent en un mouvement cohérent, semblable à un fluide. La formulation classique est le modèle des &lt;strong>boids&lt;/strong> de Craig Reynolds (1987), dans lequel chaque agent obéit à trois règles de pilotage locales, calculées uniquement à partir des voisins situés dans un rayon donné.&lt;/p>
&lt;p>Soit un agent $i$ de position $\mathbf{p}_i$ et de vitesse $\mathbf{v}_i$. Notons $\mathcal{N}_i$ l&amp;rsquo;ensemble de ses voisins situés dans le rayon de la règle évaluée. Chaque règle produit une accélération de pilotage.&lt;/p>
&lt;p>&lt;strong>La séparation&lt;/strong> empêche la nuée de s&amp;rsquo;effondrer sur elle-même. Chaque agent est repoussé par ses voisins proches, et la poussée est d&amp;rsquo;autant plus forte qu&amp;rsquo;ils sont près :&lt;/p>
$$\mathbf{a}_{\text{sep}} = w_{\text{sep}} \sum_{j \in \mathcal{N}_i} \frac{\mathbf{p}_i - \mathbf{p}_j}{\lVert \mathbf{p}_i - \mathbf{p}_j \rVert^{2}}$$
&lt;p>La pondération en inverse du carré est le détail important : un voisin deux fois plus proche repousse quatre fois plus fort, ce qui espace les agents et évite les collisions.&lt;/p>
&lt;p>&lt;strong>L&amp;rsquo;alignement&lt;/strong> fait voyager les voisins de concert, en orientant chaque agent vers la vitesse moyenne $\overline{\mathbf{v}}$ de son voisinage :&lt;/p>
$$\mathbf{a}_{\text{ali}} = w_{\text{ali}} \left( \overline{\mathbf{v}} - \mathbf{v}_i \right)$$
&lt;p>&lt;strong>La cohésion&lt;/strong> maintient le groupe rassemblé, en dirigeant chaque agent vers le centre de masse local $\overline{\mathbf{p}}$ de son voisinage :&lt;/p>
$$\mathbf{a}_{\text{coh}} = w_{\text{coh}} \left( \overline{\mathbf{p}} - \mathbf{p}_i \right)$$
&lt;p>Les trois contributions sont sommées en une seule accélération $\mathbf{a}_i$, dont la norme est plafonnée à une force de pilotage maximale. La vitesse est alors avancée par un pas d&amp;rsquo;Euler explicite, puis bornée à une plage $[v_{\min}, v_{\max}]$ pour que les agents ne se figent jamais ni ne s&amp;rsquo;emballent :&lt;/p>
$$\mathbf{v}_i \leftarrow \mathbf{v}_i + \mathbf{a}_i \Delta t$$
&lt;p>Enfin, la position est avancée avec cette nouvelle vitesse, et une règle de bord (rebouclage ou réflexion) maintient la nuée à l&amp;rsquo;intérieur du domaine :&lt;/p>
$$\mathbf{p}_i \leftarrow \mathbf{p}_i + \mathbf{v}_i \Delta t$$
&lt;p>Chaque règle possède son &lt;strong>propre rayon&lt;/strong>, et les trois poids $w_{\text{sep}}$, $w_{\text{ali}}$, $w_{\text{coh}}$ donnent à une simulation son caractère. Leur réglage est ce qui produit les scènes &lt;em>murmuration&lt;/em>, &lt;em>banc de poissons&lt;/em>, &lt;em>plasma&lt;/em> et &lt;em>turbulence&lt;/em> du dépôt. Le modèle est court à énoncer. Le travail d&amp;rsquo;ingénierie consiste à le rendre rapide et reproductible.&lt;/p>
&lt;h2 id="goulot">Le goulot d&amp;rsquo;étranglement de calcul&lt;/h2>
&lt;p>Le coût du modèle est dominé par une seule question. Pour chaque agent, quels sont les autres agents qui sont ses voisins ? Répondue naïvement, chaque agent est comparé à tous les autres, soit $O(N^2)$ calculs de distance par pas de temps. À quelques centaines d&amp;rsquo;agents, cela reste inoffensif. À des dizaines de milliers, c&amp;rsquo;est sans espoir, car la croissance quadratique signifie qu&amp;rsquo;une nuée dix fois plus grande coûte cent fois plus cher.&lt;/p>
&lt;p>BoidForge s&amp;rsquo;attaque au problème selon deux axes indépendants, et donne à chacun son propre backend afin que la contribution de chaque idée puisse être &lt;em>mesurée&lt;/em> plutôt que supposée :&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Abaisser le facteur constant.&lt;/strong> Effectuer le même travail $O(N^2)$, mais l&amp;rsquo;exprimer en moins d&amp;rsquo;instructions, plus rapides. C&amp;rsquo;est le chemin de la boucle Python vers le NumPy vectorisé, puis vers le C natif.&lt;/li>
&lt;li>&lt;strong>Abaisser la complexité.&lt;/strong> Cesser totalement d&amp;rsquo;effectuer un travail $O(N^2)$, en n&amp;rsquo;inspectant que les agents susceptibles d&amp;rsquo;être voisins. C&amp;rsquo;est la grille spatiale, qui ramène la recherche à environ $O(N)$.&lt;/li>
&lt;/ol>
&lt;p>Les quatre backends sont soumis à un unique &lt;strong>contrat de déterminisme&lt;/strong> : pour une même configuration et une même graine, ils doivent émettre une sortie identique octet par octet. Le backend naïf est la référence, et tout backend qui s&amp;rsquo;en écarte est faux par définition. Un test exécute chaque backend et compare leurs images. C&amp;rsquo;est une contrainte exigeante, car elle force même le noyau C à reproduire l&amp;rsquo;ordre de sommation &lt;code>float32&lt;/code> exact de NumPy, mais c&amp;rsquo;est elle qui rend les gains de vitesse dignes de confiance plutôt que simplement rapides.&lt;/p>
&lt;h2 id="backends">Quatre backends, et ce que chacun apporte&lt;/h2>
&lt;h3 id="l1">L1, Python naïf (la référence)&lt;/h3>
&lt;p>Une implémentation directe, toutes paires, avec une boucle Python explicite sur chaque paire d&amp;rsquo;agents. Elle est volontairement simple, car son seul rôle est de &lt;em>définir la correction&lt;/em>. Tous les autres backends sont validés contre elle. C&amp;rsquo;est aussi, sans surprise, le plus lent, puisque le surcoût par élément de l&amp;rsquo;interpréteur Python s&amp;rsquo;ajoute au travail $O(N^2)$.&lt;/p>
&lt;h3 id="l2">L2, NumPy vectorisé&lt;/h3>
&lt;p>Le même algorithme toutes paires, réexprimé avec NumPy. Les termes de déplacement, de distance et de séparation par paire sont construits une fois sous forme de tableaux entiers, et les bornages ainsi que l&amp;rsquo;intégration s&amp;rsquo;exécutent en opérations groupées sur ces tableaux.&lt;/p>
&lt;p>&lt;em>Pourquoi c&amp;rsquo;est plus rapide.&lt;/em> L&amp;rsquo;arithmétique sort de l&amp;rsquo;interpréteur Python pour passer dans les boucles C compilées de NumPy, ce qui supprime le surcoût par élément. Sur les petites et moyennes nuées, le gain est d&amp;rsquo;environ un facteur deux. Il n&amp;rsquo;est pas gratuit pour autant. Les tableaux par paire coûtent une &lt;em>mémoire&lt;/em> $O(N^2)$, et au sommet de la plage testée, le coût du transfert de ces grands tableaux en mémoire annule le gain, au point que L2 est légèrement plus lent que L1 à 2000 agents. Ce compromis est la leçon en soi : la vectorisation achète de la vitesse avec de la mémoire, et à grande échelle, la mémoire devient la ressource limitante.&lt;/p>
&lt;h3 id="l3">L3, hachage spatial sur grille uniforme&lt;/h3>
&lt;p>Le premier changement &lt;em>algorithmique&lt;/em>. Les agents sont rangés dans une grille dont le côté de cellule égale le rayon de voisinage. Chaque agent n&amp;rsquo;inspecte alors que sa propre cellule et les huit qui l&amp;rsquo;entourent, un bloc de trois sur trois, au lieu de toute la nuée. Comme le côté de cellule égale le rayon, tout voisin réel tombe forcément dans ce bloc, de sorte que la grille élague du travail sans jamais manquer un voisin.&lt;/p>
&lt;p>&lt;em>Pourquoi c&amp;rsquo;est plus rapide.&lt;/em> Cela fait passer la complexité de la recherche de voisins de $O(N^2)$ à environ $O(N)$. Le travail par agent cesse de croître avec la taille de la nuée et ne dépend plus que de la densité locale. C&amp;rsquo;est l&amp;rsquo;idée qui rend les grandes nuées traitables en principe. En Python pur, toutefois, la gestion de la construction de la grille et du rassemblement des candidats comporte assez de surcoût pour que, aux tailles modestes testées ici, son temps reste proche de celui de la boucle naïve. L&amp;rsquo;algorithme est correct, mais il attend un langage hôte plus rapide pour porter ses fruits.&lt;/p>
&lt;h3 id="l4">L4, extension C native (la cible de production)&lt;/h3>
&lt;p>Cet hôte plus rapide est une &lt;strong>extension C de CPython&lt;/strong> écrite à la main, développée avec l&amp;rsquo;API C brute et compilée avec CMake via scikit-build-core. Elle combine la grille $O(N)$ de L3 à un ensemble d&amp;rsquo;optimisations de bas niveau hors de portée de Python :&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Des tampons en structure de tableaux, partagés sans copie.&lt;/strong> L&amp;rsquo;état réside en quatre tableaux &lt;code>float32&lt;/code> contigus (positions et vitesses) partagés avec NumPy via le protocole tampon. Il n&amp;rsquo;y a pas de copie, pas de surcoût d&amp;rsquo;objet par agent, et une disposition mémoire que le processeur peut parcourir efficacement en flux.&lt;/li>
&lt;li>&lt;strong>Un tri par comptage en cellules.&lt;/strong> La grille est construite par un tri par comptage dans une disposition compacte plutôt que par une table de hachage, de sorte que le rassemblement des voisins est une boucle serrée sur des indices contigus, et non des recherches dans un dictionnaire.&lt;/li>
&lt;li>&lt;strong>Le verrou de l&amp;rsquo;interpréteur est libéré&lt;/strong> autour de la boucle de calcul, si bien que le gros du travail numérique s&amp;rsquo;exécute en véritable code natif, affranchi du verrou global de Python.&lt;/li>
&lt;li>&lt;strong>Aucune allocation mémoire dans la boucle critique.&lt;/strong> Un unique espace de travail est dimensionné une fois par pas de temps, et la boucle interne par agent n&amp;rsquo;appelle jamais l&amp;rsquo;allocateur.&lt;/li>
&lt;/ul>
&lt;p>&lt;em>Pourquoi c&amp;rsquo;est plus rapide.&lt;/em> Chacun de ces points supprime une couche de surcoût à laquelle les backends Python ne peuvent échapper, à savoir la répartition de l&amp;rsquo;interpréteur, l&amp;rsquo;indirection d&amp;rsquo;objets et la pression sur l&amp;rsquo;allocateur, tout en conservant la recherche de voisins $O(N)$. Ensemble, ils procurent environ 120 à 330 fois le débit de la référence naïve et mettent des dizaines de milliers d&amp;rsquo;agents à portée. Et tout cela en produisant une sortie identique à L1 jusqu&amp;rsquo;au bit, ce qui a exigé de reproduire exactement en C l&amp;rsquo;algorithme de sommation par paires de NumPy afin que l&amp;rsquo;arrondi &lt;code>float32&lt;/code> corresponde.&lt;/p>
&lt;h2 id="benchmarks">Bancs d&amp;rsquo;essai&lt;/h2>
&lt;p>Le banc de mesure chronomètre chaque backend sur un balayage de tailles de nuée, de 100 à 2000 agents, sur 200 pas chacun et après un préchauffage, en enregistrant le temps moyen par image et l&amp;rsquo;accélération relative à L1.&lt;/p>
&lt;figure>
&lt;a href="https://vcaries.github.io/media/boid-forge/scaling.png" target="_blank" rel="noopener">&lt;img src="https://vcaries.github.io/media/boid-forge/scaling.png" alt="Temps par image en fonction du nombre d'agents, log-log, pour les quatre backends" style="width:100%;height:auto;">&lt;/a>
&lt;figcaption>&lt;strong>Figure 1.&lt;/strong> Temps par image en fonction de la taille de la nuée, en axes logarithmiques. Les backends naïf (L1), vectorisé (L2) et hachage spatial Python (L3) restent proches à ces tailles, tandis que le backend C natif (L4) s'exécute un à deux ordres de grandeur en dessous. L'écart qui se creuse vers la droite est la grille $O(N)$ qui se détache du coût toutes paires $O(N^2)$. &lt;em>(Cliquez pour agrandir.)&lt;/em>&lt;/figcaption>
&lt;/figure>
&lt;figure>
&lt;a href="https://vcaries.github.io/media/boid-forge/speedup.png" target="_blank" rel="noopener">&lt;img src="https://vcaries.github.io/media/boid-forge/speedup.png" alt="Accélération de chaque backend par rapport à la référence naïve, par taille de nuée" style="width:100%;height:auto;">&lt;/a>
&lt;figcaption>&lt;strong>Figure 2.&lt;/strong> Accélération par rapport à la référence naïve. Le backend natif domine, culminant autour de 330 fois à 200 agents et se maintenant bien au-dessus de 100 fois sur toute la plage, tandis que les optimisations Python pur (L2 et L3) restent proches de 1 à ces tailles. Cela confirme que le gain décisif vient du passage de l'algorithme $O(N)$ en C, et non de la seule vectorisation.&lt;/figcaption>
&lt;/figure>
&lt;p>À 2000 agents, la référence naïve dépense environ 202 ms par image, soit l&amp;rsquo;équivalent de 5 images par seconde, tandis que le backend C dépense environ 1,7 ms, soit l&amp;rsquo;équivalent de 590 images par seconde. C&amp;rsquo;est la même physique calculée environ 120 fois plus vite, avec une avance qui continue de croître à mesure que la nuée grandit.&lt;/p>
&lt;h2 id="visualisation">Visualisation avec ModernGL&lt;/h2>
&lt;p>Le moteur de rendu ne simule jamais. Il lit les images précalculées dans le flux binaire et transforme chacune en image sur le GPU. Il est bâti directement sur &lt;strong>ModernGL&lt;/strong>, une liaison fine et moderne vers OpenGL 3.3, avec des shaders GLSL écrits à la main, et c&amp;rsquo;est délibérément un petit pipeline de post-traitement plutôt qu&amp;rsquo;un moteur de jeu :&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Téléversement et tracé.&lt;/strong> Chaque image est téléversée dans un tampon de sommets GPU et chaque agent est tracé comme un unique &lt;strong>sprite ponctuel&lt;/strong>. Le fragment shader façonne chaque sprite en un cœur lumineux diffus entouré d&amp;rsquo;un large halo, et sa couleur est lue dans une petite texture de palette indexée par la vitesse, le cap, la densité locale ou une valeur fixe.&lt;/li>
&lt;li>&lt;strong>Accumulation HDR additive.&lt;/strong> Les sprites sont mélangés de manière additive dans un tampon en virgule flottante, si bien que les agents qui se chevauchent accumulent une véritable luminosité. C&amp;rsquo;est ce qui donne aux grappes denses leur aspect lumineux.&lt;/li>
&lt;li>&lt;strong>Traînées de mouvement.&lt;/strong> Au lieu d&amp;rsquo;effacer ce tampon entre deux images, on l&amp;rsquo;estompe légèrement vers le noir. Les agents laissent donc derrière eux des traînées décroissantes, source de la sensation de mouvement soyeuse et fluide.&lt;/li>
&lt;li>&lt;strong>Bloom.&lt;/strong> Une passe de seuil ne conserve que les pixels les plus lumineux, un flou gaussien séparable les étale, et la lueur est réinjectée, produisant le halo cinématographique autour des régions rapides et denses.&lt;/li>
&lt;li>&lt;strong>Composition.&lt;/strong> Un dernier shader plein écran applique un tonemapping filmique ACES pour ramener la scène à grande plage dynamique dans une plage affichable, avec exposition, vignette optionnelle, teinte de fond et correction gamma.&lt;/li>
&lt;/ol>
&lt;p>Comme le contexte GPU peut être créé hors écran, ce même moteur de rendu fonctionne sans affichage sur une machine sans écran. Pour la vidéo, les images rendues sont transmises directement à un sous-processus &lt;strong>FFmpeg&lt;/strong> qui encode en H.264, sans jamais conserver tout le clip en mémoire. C&amp;rsquo;est ainsi qu&amp;rsquo;a été produite la séquence 4K en tête de page.&lt;/p>
&lt;h2 id="architecture">Architecture : deux sous-systèmes, un contrat&lt;/h2>
&lt;p>La règle de conception derrière tout ce qui précède est une séparation stricte entre &lt;em>calculer&lt;/em> la nuée et la &lt;em>dessiner&lt;/em> :&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Solveur (calcul) ---&amp;gt; flux binaire .bfs ---&amp;gt; Moteur de visualisation ---&amp;gt; images / vidéo
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Le solveur n&amp;rsquo;importe rien de la couche de visualisation, sans OpenGL ni aucun graphisme, et la couche de visualisation ne fait jamais avancer la physique. Leur seule connaissance partagée est un petit &lt;strong>format binaire&lt;/strong> versionné (&lt;code>.bfs&lt;/code>) : un en-tête de 32 octets suivi d&amp;rsquo;un enregistrement par image contenant le nombre d&amp;rsquo;agents et les quatre tableaux &lt;code>float32&lt;/code> bout à bout. Cette disposition est choisie pour que le solveur puisse écrire chaque tableau en une seule opération groupée et que le moteur de rendu puisse le mapper directement dans un tampon GPU sans réempaquetage de part et d&amp;rsquo;autre. Un test statique sur les frontières d&amp;rsquo;import garantit la séparation pour qu&amp;rsquo;elle ne s&amp;rsquo;érode pas avec le temps.&lt;/p>
&lt;p>Ce découplage est ce qui permet d&amp;rsquo;optimiser le solveur pour le débit brut et le moteur de rendu pour la qualité d&amp;rsquo;image, chacun selon ses propres critères. C&amp;rsquo;est aussi pourquoi une longue simulation peut être calculée une fois puis rendue à nouveau plus tard avec d&amp;rsquo;autres couleurs, traînées ou résolutions, autant de fois que nécessaire.&lt;/p>
&lt;h2 id="competences">Compétences démontrées&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Calcul haute performance :&lt;/strong> réduction de complexité algorithmique de $O(N^2)$ à $O(N)$ par hachage spatial, vectorisation NumPy et ses compromis mémoire, et une extension C de CPython écrite à la main avec tampons sans copie, libération du verrou de l&amp;rsquo;interpréteur et boucle critique sans allocation.&lt;/li>
&lt;li>&lt;strong>Rigueur numérique :&lt;/strong> un contrat de déterminisme exact au bit près entre quatre implémentations indépendantes, dont la reproduction en C de la sommation par paires &lt;code>float32&lt;/code> de NumPy.&lt;/li>
&lt;li>&lt;strong>GPU et graphisme temps réel :&lt;/strong> un pipeline ModernGL et GLSL avec accumulation HDR, traînées de mouvement, bloom et tonemapping ACES, fonctionnant sans affichage et exportant via FFmpeg.&lt;/li>
&lt;li>&lt;strong>Architecture logicielle :&lt;/strong> deux sous-systèmes strictement découplés reliés seulement par un format binaire versionné, avec une frontière d&amp;rsquo;import imposée.&lt;/li>
&lt;li>&lt;strong>Discipline d&amp;rsquo;ingénierie :&lt;/strong> un code Python typé, documenté et testé (&lt;code>ruff&lt;/code>, &lt;code>mypy --strict&lt;/code>, &lt;code>pytest&lt;/code>), une compilation native CMake et scikit-build-core, et la reproductibilité traitée comme une exigence de premier ordre.&lt;/li>
&lt;/ul>
&lt;h2 id="depot">Dépôt&lt;/h2>
&lt;p>L&amp;rsquo;intégralité du code source est ouverte, y compris les quatre backends de solveur, le noyau C, la couche d&amp;rsquo;entrée et sortie binaire, le banc de mesure et le moteur de visualisation ModernGL.&lt;/p>
&lt;p>Le code se trouve sur &lt;strong>&lt;a href="https://github.com/vcaries/boid_forge" target="_blank" rel="noopener">github.com/vcaries/boid_forge&lt;/a>&lt;/strong>, accompagné d&amp;rsquo;un document d&amp;rsquo;architecture qui décrit en détail le format binaire et le contrat de déterminisme.&lt;/p>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>Un modèle de nuée tient en trois lignes d&amp;rsquo;algèbre vectorielle. Le faire &lt;em>passer à l&amp;rsquo;échelle&lt;/em> est un projet d&amp;rsquo;ingénierie. BoidForge mène le modèle d&amp;rsquo;une référence Python transparente à un noyau C natif deux ordres de grandeur plus rapide, prouve à chaque étape que la vitesse supplémentaire ne coûte rien en correction, puis rend le résultat sous une forme réellement digne d&amp;rsquo;être regardée, le tout dans une architecture nette où calcul et rendu ne se touchent jamais. Le plus difficile n&amp;rsquo;a jamais été les boids. C&amp;rsquo;était de les rendre rapides, reproductibles et beaux.&lt;/p></description></item></channel></rss>