Notebook rétrospectif — Racines pré-Stable-Diffusion. Premier jalon de l’Epic #16774 : la préhistoire avant l’apprentissage — les moteurs symboliques de l’art génératif. Ici, pas de réseau de neurones : des règles, des grammaires et des automates. Le point d’entrée documentaire est la page Generative art (Wikipedia) ; la leçon technique exécutable est la nôtre.
La thèse du notebook en une ligne : dès 1960-1970, l’art génératif savait produire de l’image riche à partir d’une description minuscule — la compression était déjà le moteur esthétique, bien avant que le mot « génération » rime avec apprentissage. Ce notebook l’exécute et le mesure : la taille de la description contre la taille du rendu.
1. Trois familles de règles, une même signature
Trois moteurs symboliques historiques couvrent déjà presque tout l’espace des « œuvres par procédé » :
Moteur
Invention
Règle
Exemple canonique
Turtle graphics
Abelson & diSessa / logo des années 1960-80
une séquence de commandes (forward, turn)
spirales, rosaces
L-systèmes (Lindenmayer 1968)
grammaire de réécriture en parallèle sur l’algue du botaniste
F → F+F−F−F+F appliquée à toutes les lettre à chaque itération
Koch quadratique (variante), fougère de Barnsley
Automates cellulaires
von Neumann 1940s, Ulam ; popularisés par Conway (1970) et Wolfram (1982)
une règle locale appliquée partout en parallèle
Game of Life B3/S23, règle 110
La colonne commune : l’artefact visuel (l’image riche) est une fonction déterministe et courte de la description (quelques octets). Le reste du rayon — DiscoDiffusion, CLIPasso — remplacera la fonction par un réseau entraîné ; la signature, elle, ne changera pas : une description courte engendre une image riche. Le zoo A4 du Game of Life du dépôt (Lean-16b, port Lean Conway.Life.*) est déjà la moitié formelle de cette histoire.
2. Turtle : la rosace, première compression
La tortue Logo est le générateur le plus simple du corpus : quelques dizaines de caractères de code, un rendu d’une complexité visuelle surprenante. La rosace est l’exemple canonique : répéter N fois un polygone en tournant d’un peu plus que l’angle du polygone.
import matplotlibimport matplotlib.pyplot as pltimport numpy as npdef turtle_rosace(n_petales=12, cote=40, cotes_polygone=4):# Rosace classique de Logo : n_petales polygones reguliers, chacun tourne# de 2*pi/n_petales autour du centre. La rotation de depart depend du# petale k -- sans elle, les n_petales polygones se superposent exactement. fig, ax = plt.subplots(figsize=(5, 5))for k inrange(n_petales): theta =2* np.pi * k / n_petales # rotation de depart par petale x, y = [0.0], [0.0]for _ inrange(cotes_polygone): x.append(x[-1] + cote * np.cos(theta)) y.append(y[-1] + cote * np.sin(theta)) theta +=2* np.pi / cotes_polygone ax.plot(x, y, lw=0.7, alpha=0.6, color=plt.cm.viridis(k / n_petales)) ax.set_aspect('equal'); ax.axis('off')return fig, axfig, ax = turtle_rosace()plt.title('Rosace a 12 petals - une double boucle de code')plt.show()
Interprétation : la rosace comme description courte
La figure ci-dessus — douze carrés tournés de 30° les uns des autres, 48 segments au total, en rosace à 12 branches — est engendrée par une double boucle de six lignes. Chaque carré pris isolément est banal ; c’est leur rotation régulière qui fait le motif. Le geste minimal de l’art génératif est déjà là : l’œil voit de la complexité, la description tient dans un tweet. On le mesurera formellement en section 5 — et on relira ce motif à l’épreuve des deux sections suivantes, où la figure sera moins symétrique mais produite par une description plus courte encore.
3. L-systèmes : la courbe de Koch quadratique
Un L-système est une grammaire de réécriture en parallèle : chaque itération remplace simultanément toutes les occurrences du symbole à réécrire. La courbe de Koch quadratique est le cas d’école ici : à chaque itération, chaque segment F devient cinq segments F+F−F−F+F (la règle contient cinq F, tours de ±60°). La Koch triadique canonique (1904) est sa cousine à quatre segments et tours de 120° ; nous gardons la variante quadratique, plus simple à tracer, en citant ses vrais chiffres. Le programme qui dessine n’est plus écrit à la main : il est généré par la règle.
KOCH = {'F': 'F+F-F-F+F', '+': '+', '-': '-'} # regle de KochDIRECTIONS = {'+': np.pi /3, '-': -np.pi /3} # 60 degresdef lsystem(axiom, rules, n_iter):# Reecrit l'axiome n_iter fois selon la grammaire (parallele).for _ inrange(n_iter): axiom =''.join(rules.get(s, s) for s in axiom)return axiomdef trace(axiom, angle_map, cote=1.0, figsize=(7, 3)):# Interprete la chaine comme une sequence de tortue et trace. fig, ax = plt.subplots(figsize=figsize) x, y, theta = [0.0], [0.0], 0.0for s in axiom:if s in angle_map: theta += angle_map[s]elif s =='F': x.append(x[-1] + cote * np.cos(theta)) y.append(y[-1] + cote * np.sin(theta)) ax.plot(x, y, lw=0.8); ax.set_aspect('equal'); ax.axis('off')return fig, axaxiome = lsystem('F', KOCH, 4)fig, ax = trace(axiome, DIRECTIONS)plt.title(f'Koch quadratique (5 segments), 4 iterations - axiome 1 caractere -> {len(axiome)} caracteres')plt.show()print(f"Chaine generee : {len(axiome)} caracteres pour {axiome.count('F')} segments")
Chaine generee : 1249 caracteres pour 625 segments
Interprétation : le programme engendré, pas écrit
Quatre itérations de la règle ont transformé un caractère (F) en une chaîne de 5^4 = 625 segments — la règle contient cinq F, chaque itération multiplie donc les segments par cinq. Personne n’a écrit ces 625 segments : la règle les a engendrés. C’est la définition opérationnelle de l’art algorithmique — et le chaînon que la démoscène (jalon suivant de l’Epic) poussera jusqu’à l’intro 64 ko : l’itération ici est explicite, là elle sera cachée dans une boucle de compression.
4. Automates cellulaires : règle 110 et Game of Life
La troisième famille remplace la grammaire séquentielle par une règle locale uniforme : l’état futur d’une cellule ne dépend que de son voisinage. Deux exemples historiques exécutés ici :
la règle 110 élémentaire (Wolfram) — le seul automate 1D élémentaire Turing-complet (Cook 2004) : la complexité naît d’une table de 8 bits ;
le Game of Life B3/S23 (Conway 1970) — le même geste en 2D, dont le dépôt porte déjà le port Lean complet (Conway.Life.*, zoo A4 de Lean-16b).
def regle110(grille, n_steps=60):# Evolue une grille 1D binaire sous la regle 110 (table a 8 entrees). table = {tuple(map(int, f'{i:03b}')): (110>> i) &1for i inrange(8)} histoire = [grille]for _ inrange(n_steps): g = histoire[-1] suivant = [table[(g[(i-1) %len(g)], g[i], g[(i+1) %len(g)])] for i inrange(len(g))] histoire.append(suivant)return np.array(histoire)n =121graine = np.zeros(n, dtype=int); graine[n //2] =1# une seule cellulehistoire = regle110(list(graine))fig, ax = plt.subplots(figsize=(7, 5))ax.imshow(histoire, cmap='binary', interpolation='nearest')ax.set_title('Regle 110 depuis une cellule unique - table de 8 bits')ax.axis('off')plt.show()
def step_life(grid):# Une generation du Game of Life B3/S23 (bords toriques). voisins =sum(np.roll(np.roll(grid, dy, 0), dx, 1)for dy in (-1, 0, 1) for dx in (-1, 0, 1)if (dy, dx) != (0, 0))return ((voisins ==3) | ((grid ==1) & (voisins ==2))).astype(int)n =48grid = np.zeros((n, n), dtype=int)glider = [(1, 2), (2, 3), (3, 1), (3, 2), (3, 3)] # le planeur canoniquefor r, c in glider: grid[r, c] =1frames = [grid.copy()]for _ inrange(24): grid = step_life(grid) frames.append(grid.copy())fig, axes = plt.subplots(1, 4, figsize=(12, 3.2))for ax, f, t inzip(axes, frames[::8], [0, 8, 16, 24]): ax.imshow(f, cmap='binary'); ax.set_title(f't = {t}'); ax.axis('off')fig.suptitle('Le planeur du Game of Life - cinq cellules decrites, vingt-quatre generations')plt.show()
Interprétation : la règle locale, la complexité globale
Trois signatures, un même constat :
la règle 110 engendre une structure en triangles imbriqués depuis une cellule et une table tenant en un octet — Turing-complette (Cook 2004) ;
le planeur se déplace en diagonale : cinq cellules initiales, un déplacement périodique de période 4 — l’information voyage ;
aucun des deux n’a de paramètre appris : la règle est tout le modèle.
C’est la frontière exacte où ce rayon s’arrête : à partir de 05-1, la règle cessera d’être écrite par l’humain pour être apprise — mais la signature description-courte → image-riche restera la même à travers tout le rayon.
5. Mesurer la compression : description contre rendu
La leçon exécutable du jalon : l’art génératif symbolique est un argument de compression rendu visible. On le mesure honnêtement :
la description est la règle seule (axiome + règle, hors code d’affichage et d’interprétation) ;
le rendu est la sortie visuelle — mesurée par le nombre de segments tracés, puis par le poids de l’image PNG rendue.
Mesuré à l’instant sur les figures ci-dessus : description de 12 caractères pour un rendu Koch de ~12 ko (x1030), table de 9 bits pour un rendu règle 110 de ~2,7 ko (x2365) — deux ordres de grandeur obtenus sans aucun apprentissage. Ce chiffre est la charnière du rayon :
jalon 2 (démoscène) : la même mesure poussée à l’extrême — une intro 64 ko produisant des minutes d’animation, ratio de l’ordre du million ;
jalon 5 (DiscoDiffusion) : le prompt remplace l’axiome, le réseau remplace la règle — le ratio ne changera pas d’ordre de grandeur, mais la variété des images atteignables, si.
6. Exercices
Trois exercices, dans l’esprit du corpus : le squelette s’exécute, la réalisation vous appartient.
# Exercice 1 - une autre grammaire : le flocon variant# Ecrire la regle du flocon de Koch sous sa variante 'snowflake' (axiome 'F--F--F',# angle 60 degres) et tracer 3 iterations. Indices :# # Indice 1 - l'axiome est un triangle, la regle reste 'F+F-F-F+F' ;# # Etape 1 - definir l'axiome ; # Etape 2 - appliquer lsystem ; # Etape 3 - tracer avec trace().print("Exercice 1 a completer")
Exercice 1 a completer
# Exercice 2 - la periode du blinker sous regle 110# Partir d'une graine de TROIS cellules consecutives et mesurer combien de# generations avant retour a l'etat initial (s'il existe). Indices :# # Indice 1 - reutiliser regle110 avec une graine differente ;# # Etape 1 - construire la graine ; # Etape 2 - chercher la periode par comparaison a t=0.print("Exercice 2 a completer")
Exercice 2 a completer
# Exercice 3 - votre propre rosace# Choisir (n_petales, cote) pour engendrer la rosace la plus dense possible# avec le moins de caracteres de code modifies. Mesurer le ratio comme en 5.# # Indice 1 - seuls deux nombres changent ; # Etape 1 - modifier les parametres ; # Etape 2 - mesurer.print("Exercice 3 a completer")
Exercice 3 a completer
7. Conclusion
Ce jalon a exécuté les trois familles symboliques — tortue, grammaire de réécriture, automate cellulaire — et mesuré leur bien commun : une description minuscule engendre un rendu riche. La suite du rayon raconte comment l’apprentissage a repris cette signature : la démoscène en poussera le ratio à l’extrême (jalon 2), le low-complexity art le théorisera (Schmidhuber, jalon 3 — jointure avec l’Epic #16775), les GANs puis la CLIP-guided diffusion en changeront le moteur.
Rattachements : Epic #16774 (jalon 1) · Epic #16775 (jointure low-complexity art) · le zoo A4 de Lean-16b porte déjà la moitié formelle de l’automate cellulaire.