import random
import cmath, math
import matplotlib.pyplot as plt
Considérons la suite réelle définie par et pour tout ,
Le comportement de cette suite est facile à étudier. Notons
Si , la suite est croissante et tend vers . Si , la suite est décroissante et tend vers .
phi = (1 + math.sqrt(5)) / 2
hatphi = (1 - math.sqrt(5)) / 2
print(phi)
print(hatphi)
1.618033988749895 -0.6180339887498949
Passons à des choses plus intéressantes. Que se passe-t-il si nous prenons ?
La définition de la suite fait intervenir des racines carrées. Si nous voulons définir cette suite dans il va nous falloir être précis.
Supposons donné un espace probabilisé . Soit une suite de variables aléatoires réelles indépendantes qui suivent une loi uniforme sur (c'est ce l'on appelle des variables de Rademacher). La fonction epsilon
ci-dessous possède un « paramètre caché » . Au ème appel, cette fonction renvoie .
def epsilon():
return 2 * random.randint(0, 1) - 1
[epsilon() for k in range(10)]
[1, 1, -1, -1, -1, 1, -1, 1, 1, 1]
Pour tout , écrivons
où et . Si , est l'unique argument de qui appartient à . Posons
Le nombre complexe est une racine carrée de . C'est en fait la valeur renvoyée par la fonction sqrt
du module Python cmath
.
Pour tout , considérons la suite de premier terme définie, pour tout , par
Plus précisément, nous venons de définir la variable aléatoire
qui à tout associe la suite .
La fonction termes_suite
prend en paramètres un nombre complexe et un entier naturel . Elle renvoie la liste .
Cette fonction est un peu plus générale. Elle prend également en paramètre un nombre complexe et renvoie les termes de la suite définie par . Dans tout le notebook, nous ne discuterons que du cas mais bien d'autres valeurs de donnent des résultats tout aussi intéressants. Vous pourrez trouver lesquelles en vous documentant sur les ensembles de Julia.
def termes_suite(z0, n, c=-1):
zs = [z0]
z = z0
for k in range(n):
z = epsilon() * cmath.sqrt(z - c)
zs.append(z)
return zs
print(termes_suite(0, 10))
[0, (1+0j), (-1.4142135623730951+0j), (-0-0.6435942529055827j), (-1.0462330321211237+0.3075769131475257j), (0.36386755980555213+0.422649539453163j), (-1.1814655449611482-0.1788666378193287j), (-0.19148683000196587+0.46704684029051086j), (-0.9333352988257161-0.25020313754238693j), (0.40348279018439887-0.31005428686071046j), (-1.191806582179002+0.13007743517149925j)]
Quelle est la « limite » de la suite ? Faisons un dessin.
La fonction plot_complexes
affiche les images des nombres complexes de la liste zs
.
def plot_complexes(zs, **opt):
xs = [z.real for z in zs]
ys = [z.imag for z in zs]
plt.plot(xs, ys, **opt)
plt.xlim(-2, 2)
plt.ylim(-1, 1)
plt.grid()
Dessinons maintenant les 100000 premiers termes de la suite en prenant (par exemple) . Chaque évaluation de la cellule ci-dessous renvoie (a priori) un dessin différent.
plt.rcParams['figure.figsize'] = (8, 4)
zs = termes_suite(0, 100000)
plot_complexes(zs[100:], color='k', ls='None', marker='o', ms=0.1)
Bigre ! Et en fait, re-bigre : vous pouvez évaluer la cellule ci-dessus autant de fois que vous le voulez, en prenant les valeurs de que vous voulez, vous obtiendrez toujours (sauf malchance inouie) le même dessin. La suite semble attirée par un ensemble très compliqué que nous noterons .
L'ensemble (nommé ainsi en l'honneur du mathématicien Gaston Julia) est un attracteur étrange.
En fait, vérifie la propriété suivante. Pour tout , une sorte de définition de limite est vérifiée :
où
est la distance de à . Mieux, est la plus petite partie fermée de (je n'expliciterai pas le concept d'ensemble fermé) vérifiant cette propriété.
IFS signifie « Iterated Function System ». Comme leur nom l'indique, les IFS sont des systèmes de fonctions itérées. Dans le cas qui nous occupe, considérons la fonction définie par . Définissons ensuite la fonction définie, pour toute partie de , par
Remarquons que pour tout , est symétrique par rapport à , c'est à dire que pour tout , . Notons l'ensemble des parties de symétriques par rapport à .
Démonstration. Montrons que est injective. Soient . Supposons . Soit . On a , il existe donc tel que . En élevant au carré, il vient facilement et donc . De même, . Ainsi, .
Montrons maintenant que est surjective. Soit . Soit . Montrons que .
Soit . On a donc ou . Soit tel que . Comme , il existe tel que . On a donc
Or, donc . Ainsi, .
Inversement, soit . En posant , on a et , donc . Ainsi, .
Finalement, .
Quel est le lien entre et ? Eh bien est un point fixe de .
Démonstration. Étant donné que nous n'avons pas défini proprement l'ensemble , montrer ce résultat serait assez délicat ...
Retenons en tout cas que est un point fixe de (c'est même l'unique compact non vide qui soit un point fixe de ). Ceci suggère de faire l'expérience suivante. Étant donné , considérons la suite définie par la relation de récurrence . Se pourrait-il que, d'une certaine façon, la suite converge vers ? Il s'avère que si est compact et non vide, c'est effectivement le cas.
Nous allons tenter l'expérience. En partant d'une image représentant n'importe quoi (une licorne, par exemple) et en itérant la fonction sur l'image , nous devrions voir la licorne se déformer et se transformer progressivement en .
La fonction matrice
renvoie une matrice dont tous les coefficients sont égaux à .
def matrice(N, c):
A = N * [None]
for i in range(N): A[i] = N * [c]
return A
Soit une matrice de taille , dont les coefficients sont censés représenter des points du carré . Pour tous , nous aurons besoin de savoir à quel nombre complexe correspond le couple . Inversement, si , nous devrons déterminer le couple d'entiers de qui lui correspond.
Les fonctions pixel_vers_complexe
et complexe_vers_pixel
font le travail.
def pixel_vers_complexe(i, j, N):
y = -2 + 4 * i / N
x = -2 + 4 * j / N
return x + y * 1j
pixel_vers_complexe(100, 200, 400)
-1j
def round(x, N):
i = int(x)
if i <= 0: i = 0
elif i >= N: i = N - 1
return i
def complexe_vers_pixel(z, N):
x = z.real
y = z.imag
j = round(N * (x + 2) / 4, N)
i = round(N * (y + 2) / 4, N)
return (i, j)
complexe_vers_pixel(-1j, 400)
(100, 200)
La fonction transformee
prend en paramètre une matrice carrée . Les coefficients de sont des triplets d'entiers entre 0 et 255, représentant des couleurs. La fonction construit une matrice de même taille que initialement remplie de triplets (la couleur blanche). Pour chaque nombre complexe correspondant à un coefficient de qui n'est pas la couleur blanche, la fonction calcule (où, dans notre discussion, ). Elle ajuste le coefficient correspondant dans la matrice . Pour faire vite, transformee(A)
calcule .
def transformee(A, c=-1):
N = len(A)
B = matrice(N, (255, 255, 255))
for i in range(N):
for j in range(N):
if A[i][j] != (255, 255, 255):
z = pixel_vers_complexe(i, j, N)
w = cmath.sqrt(z - c)
u, v = complexe_vers_pixel(w, N)
B[u][v] = A[i][j]
u, v = complexe_vers_pixel(-w, N)
B[u][v] = A[i][j]
return B
Enfin, la fonction plot_matrix
affiche l'image représentée par .
def plot_matrix(A):
plt.imshow(A, origin='lower', interpolation='antialiased')
plt.axis('off')
L'image sur laquelle nous allons travailler représente une licorne. La fonction read_licorne
lit les pixels de l'image dans le fichier licorne.raw
.
def read_licorne():
A = matrice(512, (255, 255, 255))
f = open('licorne.raw', 'rb')
for i in range(512):
for j in range(512):
r, g, b = f.read(3)
A[511 - i][j] = (r, g, b)
f.close()
return A
plt.rcParams['figure.figsize'] = (6, 6)
A = read_licorne()
plot_matrix(A)
plt.savefig('img00.pdf', bbox_inches='tight')
La fonction iter
lit l'image licorne et applique fois la fonction .
def iter(n, c=-1):
A = read_licorne()
for k in range(n):
A = transformee(A, c)
return A
Itérons une fois.
k = 1
plot_matrix(iter(k))
plt.savefig('img' + ('%02d' % k) + '.pdf', bbox_inches='tight')
Nous obtenons deux licornes avec un gros museau en train de s'embrasser.
Itérons maintenant deux fois.
k = 2
plot_matrix(iter(k))
plt.savefig('img' + ('%02d' % k) + '.pdf', bbox_inches='tight')
On reconnaît 4 licornes entrelacées. Et si on itère 3 fois ?
k = 3
plot_matrix(iter(k))
plt.savefig('img' + ('%02d' % k) + '.pdf', bbox_inches='tight')
Il y a 8 licornes, mais on a beaucoup de mal à les voir. Faisons 20 itérations.
k = 20
plot_matrix(iter(k))
plt.savefig('img' + ('%02d' % k) + '.pdf', bbox_inches='tight')
Voyez-vous les 1048576 licornes ? Pas moi. Par contre, la figure affichée me rappelle quelque chose :-).
Reprenons notre suite . Pour tout ,
En élevant au carré, il vient
Ceci suggère de considérer de nouvelles suites récurrentes, définie par leur premier terme et par la relation de récurrence
Remarquez le subtil échange . Posons alors
Y a-t-il un lien entre et ? On se croirait dans Men in Black.
Démonstration admise. Prouvons juste un petit résultat.
Démonstration. Rappelons que est le nombre d'or. Soit . Supposons . Posons où . On a alors
Par une récurrence facile,,
Ainsi, tend vers lorsque tend vers l'infini, et donc .
Remarque. Plus généralement, s'il existe tel que , alors .
Nous devrions pouvoir trouver par la technique suivante : pour chaque appartenant au disque de centre 0 et de rayon , on calcule , , etc. Si l'un des est de module strictement supérieur à , alors . Sinon, on affiche avec une couleur qui dépend de ... ce qui nous fait plaisir.
La fonction iterer
effectue le travail. Elle fonctionne
un peu différemment de ce que je viens de dire. Je n'explique pas ici la
valeur renvoyée. Cette valeur est reliée à l'interprétation de comme un corps électriquement chargé. Ce corps crée un champ électrique. La fonction iterer
renvoie une approximation du potentiel associé à ce champ.
La théorie des potentiels associés aux attracteurs étranges a été développée (entre autres) par les mathématiciens Adrien Douady et John Hubbard.
def iterer(z, niter):
k = 0
while k < niter and abs(z) <= 1e6:
z = z ** 2 - 1
k = k + 1
if k == niter: return 0
else:
return math.log(abs(z), 2) / 2 ** k
La fonction de tracé prend en paramètres :
def bornes(centre, zoom, ratio):
h = 2 / zoom
w = ratio * h
xmin = centre.real - w / 2
xmax = centre.real + w / 2
ymin = centre.imag - h / 2
ymax = centre.imag + h / 2
return (xmin, xmax, ymin, ymax)
def tracer(centre, zoom, ratio, niter, ny):
xmin, xmax, ymin, ymax = bornes(centre, zoom, ratio)
nx = math.floor(ny * ratio)
M = ny * [None]
for i in range(ny):
M[i] = nx * [(0, 0, 0)]
for i in range(ny):
for j in range(nx):
x = xmin + j * (xmax - xmin) / nx
y = ymin + i * (ymax - ymin) / ny
z = x + 1j * y
G = iterer(z, niter)
if G != 0:
c = math.floor(math.log(G)) % 2
if c == 0: M[i][j] = (255, 255, 255)
else: M[i][j] = (0, 0, 0)
plt.imshow(M, interpolation='antialiased', origin='lower', extent = (xmin, xmax, ymin, ymax))
plt.grid()
Commençons par un tracé de tout entier. Centre 0, zoom 1, ratio image 2/1, 512 itérations maximum par pixel, et une hauteur d'image de 300 pixels.
plt.rcParams['figure.figsize'] = (10, 5)
tracer(0, 1, 2, 512, 300)
Remarquons que le nombre d'or est le point situé à l'extrémité droite de . En effet, si nous prenons alors la suite est constante égale à , donc bornée. Ainsi, . En revanche, si , alors nous avons vu que .
Faisons un zoom sur ce point.
tracer(phi, 1000, 2, 512, 300)
Et le copain du nombre d'or ? Il est à l'extrémité gauche du gros trou noir au milieu de l'image globale. Faisons un zoom .
plt.rcParams['figure.figsize'] = (8, 8)
tracer(hatphi, 1000, 1, 512, 400)
Dernier zoom : sur l'imaginaire pur situé à l'extémité haute de .
Démonstration. Soit . On a
et
puis, pour tout , . Ainsi, . Soit maintenant où . On a et
Ainsi, , donc .
z0 = 1j / math.sqrt(phi)
print(z0)
0.7861513777574233j
plt.rcParams['figure.figsize'] = (8, 8)
tracer(z0, 1000, 1, 512, 400)
Vous ne verrez plus jamais la suite de la même façon.