un+1=√1+unun+1=1+un¶

Marc Lorenzi

17 octobre 2024

In [1]:
import random
import cmath, math
import matplotlib.pyplot as plt

1. Attracteurs étranges¶

1.1 Une suite réelle définie par récurrence¶

Considérons la suite réelle uu définie par u0∈[−1,+∞[u0∈[−1,+∞[ et pour tout n∈Nn∈N,

un+1=√1+un.un+1=1+un.

Le comportement de cette suite est facile à étudier. Notons

φ=1+√52.φ=1+52.

Si u0≤φu0≤φ, la suite uu est croissante et tend vers φφ. Si u0≥φu0≥φ, la suite uu est décroissante et tend vers φφ.

In [2]:
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 u0∈Cu0∈C ?

1.2 Passage aux complexes¶

La définition de la suite uu fait intervenir des racines carrées. Si nous voulons définir cette suite dans CC il va nous falloir être précis.

Supposons donné un espace probabilisé (Ω,T,P)(Ω,T,P). Soit (εn)n∈N(εn)n∈N une suite de variables aléatoires réelles indépendantes qui suivent une loi uniforme sur {−1,1}{−1,1} (c'est ce l'on appelle des variables de Rademacher). La fonction epsilon ci-dessous possède un « paramètre caché » ω∈Ωω∈Ω. Au nnème appel, cette fonction renvoie εn(ω)εn(ω).

In [3]:
def epsilon():
    return 2 * random.randint(0, 1) - 1
In [4]:
[epsilon() for k in range(10)]
Out[4]:
[1, 1, -1, -1, -1, 1, -1, 1, 1, 1]

Pour tout z∈Cz∈C, écrivons

z=reiθz=reiθ

où r=|z|∈R+r=|z|∈R+ et θ∈ ]−π,π]θ∈ ]−π,π]. Si z≠0z≠0, θθ est l'unique argument de zz qui appartient à ]−π,π]]−π,π]. Posons

√z=√reiθ/2.z=reiθ/2.

Le nombre complexe √zz est une racine carrée de zz. C'est en fait la valeur renvoyée par la fonction sqrt du module Python cmath.

Pour tout z0∈Cz0∈C, considérons la suite (zn)n∈N(zn)n∈N de premier terme z0z0 définie, pour tout n∈Nn∈N, par

zn+1=εn√1+zn.zn+1=εn1+zn.

Plus précisément, nous venons de définir la variable aléatoire

Z:Ω⟶CNZ:Ω⟶CN

qui à tout ω∈Ωω∈Ω associe la suite Z(ω)=(zn(ω))n∈NZ(ω)=(zn(ω))n∈N.

La fonction termes_suite prend en paramètres un nombre complexe z0z0 et un entier naturel nn. Elle renvoie la liste [z0,…,zn][z0,…,zn].

Cette fonction est un peu plus générale. Elle prend également en paramètre un nombre complexe cc et renvoie les termes de la suite définie par zn+1=√zn−czn+1=zn−c. Dans tout le notebook, nous ne discuterons que du cas c=−1c=−1 mais bien d'autres valeurs de cc donnent des résultats tout aussi intéressants. Vous pourrez trouver lesquelles en vous documentant sur les ensembles de Julia.

In [5]:
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
In [6]:
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)]

1.3 Étrange ?¶

Quelle est la « limite » de la suite (zn)n∈N(zn)n∈N ? Faisons un dessin.

La fonction plot_complexes affiche les images des nombres complexes zz de la liste zs.

In [7]:
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 (zn)n∈N(zn)n∈N en prenant (par exemple) z0=0z0=0. Chaque évaluation de la cellule ci-dessous renvoie (a priori) un dessin différent.

In [8]:
plt.rcParams['figure.figsize'] = (8, 4)
In [9]:
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 z0z0 que vous voulez, vous obtiendrez toujours (sauf malchance inouie) le même dessin. La suite (zn)n∈N(zn)n∈N semble attirée par un ensemble très compliqué que nous noterons JJ.

L'ensemble JJ (nommé ainsi en l'honneur du mathématicien Gaston Julia) est un attracteur étrange.

  • Attracteur, car il attire la suite.
  • Étrange, car il est ... étrange. Pour donner deux exemples d'attracteurs pas du tout étranges,
    • l'attracteur d'une suite convergente est le singleton {ℓ}{ℓ} où ℓℓ est la limite de la suite.
    • l'attracteur de la suite (1/n+(−1)n)n∈N(1/n+(−1)n)n∈N est l'ensemble {−1,1}{−1,1}.

En fait, JJ vérifie la propriété suivante. Pour tout z0∈Jz0∈J, une sorte de définition de limite est vérifiée :

∀ε>0,∃n∈N,∀n≥N,d(zn,J)≤ε∀ε>0,∃n∈N,∀n≥N,d(zn,J)≤ε

où

d(zn,J)=inf{|zn−z|:z∈J}d(zn,J)=inf{|zn−z|:z∈J}

est la distance de znzn à JJ. Mieux, JJ est la plus petite partie fermée de CC (je n'expliciterai pas le concept d'ensemble fermé) vérifiant cette propriété.

2. IFS¶

2.1 C'est quoi ?¶

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 f:C⟶Cf:C⟶C définie par f(z)=√(1+z)f(z)=(1+z). Définissons ensuite la fonction F:P(C)⟶P(C)F:P(C)⟶P(C) définie, pour toute partie AA de CC, par

F(A)=f(A)∪(−f)(A)F(A)=f(A)∪(−f)(A)

Remarquons que pour tout A∈P(C)A∈P(C), F(A)F(A) est symétrique par rapport à OO, c'est à dire que pour tout w∈Cw∈C, w∈A⟺−w∈Aw∈A⟺−w∈A. Notons SS l'ensemble des parties de CC symétriques par rapport à OO.

Proposition. FF est une bijection de P(C)P(C) sur SS.

Démonstration. Montrons que FF est injective. Soient A,A′∈P(C)A,A′∈P(C). Supposons F(A)=F(A′)F(A)=F(A′). Soit z∈Az∈A. On a f(z)∈F(A)=F(A′)f(z)∈F(A)=F(A′), il existe donc z′∈A′z′∈A′ tel que f(z)=±f(z′)f(z)=±f(z′). En élevant au carré, il vient facilement z=z′∈A′z=z′∈A′ et donc A⊆A′A⊆A′. De même, A′⊆AA′⊆A. Ainsi, A=A′A=A′.

Montrons maintenant que FF est surjective. Soit B∈SB∈S. Soit A={w2−1:w∈B}A={w2−1:w∈B}. Montrons que F(A)=BF(A)=B.

Soit u∈F(A)u∈F(A). On a donc u∈f(A)u∈f(A) ou u∈(−f)(A)u∈(−f)(A). Soit z∈Az∈A tel que u=±f(z)u=±f(z). Comme z∈Az∈A, il existe w∈Bw∈B tel que z=w2−1z=w2−1. On a donc

u=±f(z)=±√w2−1+1=±wu=±f(z)=±w2−1+1=±w

Or, B∈SB∈S donc u=±w∈Bu=±w∈B. Ainsi, F(A)⊆BF(A)⊆B.

Inversement, soit w∈Bw∈B. En posant z=w2−1z=w2−1, on a z∈Az∈A et ±f(z)=w±f(z)=w, donc w∈F(A)w∈F(A). Ainsi, B⊆F(A)B⊆F(A).

Finalement, F(A)=BF(A)=B.

Quel est le lien entre FF et JJ ? Eh bien JJ est un point fixe de FF.

Proposition. F(J)=JF(J)=J.

Démonstration. Étant donné que nous n'avons pas défini proprement l'ensemble JJ, montrer ce résultat serait assez délicat ...

Retenons en tout cas que JJ est un point fixe de FF (c'est même l'unique compact non vide qui soit un point fixe de FF). Ceci suggère de faire l'expérience suivante. Étant donné A0⊆CA0⊆C, considérons la suite (An)n∈N(An)n∈N définie par la relation de récurrence An+1=F(An)An+1=F(An). Se pourrait-il que, d'une certaine façon, la suite (An)n∈N(An)n∈N converge vers JJ ? Il s'avère que si AA est compact et non vide, c'est effectivement le cas.

Nous allons tenter l'expérience. En partant d'une image AA représentant n'importe quoi (une licorne, par exemple) et en itérant la fonction FF sur l'image AA, nous devrions voir la licorne se déformer et se transformer progressivement en JJ.

2.2 Quelques fonctions utiles¶

La fonction matrice renvoie une matrice N×NN×N dont tous les coefficients sont égaux à cc.

In [10]:
def matrice(N, c):
    A = N * [None]
    for i in range(N): A[i] = N * [c]
    return A

Soit AA une matrice de taille N×NN×N, dont les coefficients sont censés représenter des points du carré [−2,2]×[−2,2][−2,2]×[−2,2]. Pour tous i,j∈[[0,N−1]]i,j∈[[0,N−1]], nous aurons besoin de savoir à quel nombre complexe correspond le couple (i,j)(i,j). Inversement, si z∈Cz∈C, nous devrons déterminer le couple (i,j)(i,j) d'entiers de [[0,N−1]][[0,N−1]] qui lui correspond.

Les fonctions pixel_vers_complexe et complexe_vers_pixel font le travail.

In [11]:
def pixel_vers_complexe(i, j, N):
    y = -2 + 4 * i / N
    x = -2 + 4 * j / N
    return x + y * 1j
In [12]:
pixel_vers_complexe(100, 200, 400)
Out[12]:
-1j
In [13]:
def round(x, N):
    i = int(x)
    if i <= 0: i = 0
    elif i >= N: i = N - 1
    return i
In [14]:
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)
In [15]:
complexe_vers_pixel(-1j, 400)
Out[15]:
(100, 200)

La fonction transformee prend en paramètre une matrice carrée AA. Les coefficients de AA sont des triplets (r,g,b)(r,g,b) d'entiers entre 0 et 255, représentant des couleurs. La fonction construit une matrice BB de même taille que AA initialement remplie de triplets (255,255,255)(255,255,255) (la couleur blanche). Pour chaque nombre complexe zz correspondant à un coefficient de AA qui n'est pas la couleur blanche, la fonction calcule ±√z−c±z−c (où, dans notre discussion, c=−1c=−1). Elle ajuste le coefficient correspondant dans la matrice BB. Pour faire vite, transformee(A) calcule F(A)F(A).

In [16]:
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 AA.

In [17]:
def plot_matrix(A):
    plt.imshow(A, origin='lower', interpolation='antialiased')
    plt.axis('off')

2.3 Allons-y¶

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.

In [18]:
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
In [19]:
plt.rcParams['figure.figsize'] = (6, 6)
In [20]:
A = read_licorne()
plot_matrix(A)
plt.savefig('img00.pdf', bbox_inches='tight')

La fonction iter lit l'image licorne et applique nn fois la fonction FF.

In [21]:
def iter(n, c=-1):
    A = read_licorne()
    for k in range(n):
        A = transformee(A, c)
    return A

Itérons une fois.

In [22]:
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.

In [23]:
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 ?

In [24]:
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.

In [25]:
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 :-).

3. Renversement des itérations¶

3.1 Suites renversées.¶

Reprenons notre suite (zn)n∈N(zn)n∈N. Pour tout n∈Nn∈N,

zn+1=εn√1+znzn+1=εn1+zn

En élevant au carré, il vient

zn=z2n+1−1zn=zn+12−1

Ceci suggère de considérer de nouvelles suites récurrentes, définie par leur premier terme z0∈Cz0∈C et par la relation de récurrence

zn+1=z2n−1zn+1=zn2−1

Remarquez le subtil échange n⟷n+1n⟷n+1. Posons alors

K={z0∈C:(zn)n∈N est bornée}K={z0∈C:(zn)n∈N est bornée}

Y a-t-il un lien entre JJ et KK ? On se croirait dans Men in Black.

Définition. Soient a∈Ca∈C et r∈R∗+r∈R+∗. Le disque ouvert de centre aa et de rayon rr est

$$D(a,r)=\{z\in\C:|z-a| On définit de même le disque fermé ¯¯¯¯¯D(a,r)D¯(a,r) en remplaçant l'inégalité stricte par une inégalité large.
Définition. Soit A⊆CA⊆C. La frontière de AA est l'ensemble Fr(A)Fr(A) des z∈Cz∈C vérifiant la propriété suivante : pour tout ε>0ε>0,
  • D(z,ε)∩A≠∅D(z,ε)∩A≠∅.
  • D(z,ε)∩(C∖A)≠∅D(z,ε)∩(C∖A)≠∅.
Proposition. J⊆KJ⊆K et Fr(K)=JFr(K)=J.

Démonstration admise. Prouvons juste un petit résultat.

Proposition. KK est borné. Plus précisément, K⊆¯¯¯¯¯D(0,φ)K⊆D¯(0,φ).

Démonstration. Rappelons que φφ est le nombre d'or. Soit z0∈Cz0∈C. Supposons |z0|>φ|z0|>φ. Posons |z0|=φ+h|z0|=φ+h où h>0h>0. On a alors

|z1|=|z20−1|≥|z0|2−1=(φ+h)2−1=φ+2φh+h2≥φ+2φh.|z1|=|z02−1|≥|z0|2−1=(φ+h)2−1=φ+2φh+h2≥φ+2φh.

Par une récurrence facile,,

∀n∈N,|zn|≥φ+(2φ)nh.∀n∈N,|zn|≥φ+(2φ)nh.

Ainsi, |zn||zn| tend vers +∞+∞ lorsque nn tend vers l'infini, et donc z0∉Kz0∉K.

Remarque. Plus généralement, s'il existe n∈Nn∈N tel que |zn|>φ|zn|>φ, alors z0∉Kz0∉K.

3.2 Dessiner KK¶

Nous devrions pouvoir trouver KK par la technique suivante : pour chaque z0z0 appartenant au disque de centre 0 et de rayon φφ, on calcule z1z1, z2z2, etc. Si l'un des zkzk est de module strictement supérieur à φφ, alors z0∉Kz0∉K. Sinon, on affiche z0z0 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 JJ 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.

In [26]:
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 :

  • Le centre de l'image affichée, qui est un nombre complexe.
  • un facteur de zoom.
  • Le ratio largeur / hauteur de l'image.
  • Le nombre maximal d'itérations effectuées en chaque point.
  • La hauteur de l'image en pixels.
In [27]:
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)
In [28]:
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()

3.3 Allons-y¶

Commençons par un tracé de KK tout entier. Centre 0, zoom 1, ratio image 2/1, 512 itérations maximum par pixel, et une hauteur d'image de 300 pixels.

In [29]:
plt.rcParams['figure.figsize'] = (10, 5)
In [30]:
tracer(0, 1, 2, 512, 300)

Remarquons que le nombre d'or est le point situé à l'extrémité droite de JJ. En effet, si nous prenons z0=φz0=φ alors la suite (zn)n∈N(zn)n∈N est constante égale à φφ, donc bornée. Ainsi, φ∈Kφ∈K. En revanche, si |z0|>φ|z0|>φ, alors nous avons vu que z0∉Kz0∉K.

Faisons un zoom ×1000×1000 sur ce point.

In [31]:
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 ×1000×1000.

In [32]:
plt.rcParams['figure.figsize'] = (8, 8)
In [33]:
tracer(hatphi, 1000, 1, 512, 400)

Dernier zoom : ×103×103 sur l'imaginaire pur situé à l'extémité haute de KK.

Proposition. Ce nombre est i√φiφ

Démonstration. Soit z0=i√φz0=iφ. On a

z1=z20−1=−1φ−1=−φ+1φ=−φz1=z02−1=−1φ−1=−φ+1φ=−φ

et

z2=z21−1=π2−1=φz2=z12−1=π2−1=φ

puis, pour tout n≥2n≥2, zn=φzn=φ. Ainsi, z0∈Kz0∈K. Soit maintenant z0=ix√φz0=ixφ où x>1x>1. On a z1∈Rz1∈R et

z1=z20−1=−x2φ−1<−1φ−1=−φ+1φ=−φz1=z02−1=−x2φ−1<−1φ−1=−φ+1φ=−φ

Ainsi, |z1|>φ|z1|>φ, donc z0∉Kz0∉K.

In [34]:
z0 = 1j / math.sqrt(phi)
print(z0)
0.7861513777574233j
In [35]:
plt.rcParams['figure.figsize'] = (8, 8)
In [36]:
tracer(z0, 1000, 1, 512, 400)

3. Conclusion¶

Vous ne verrez plus jamais la suite un+1=√1+unun+1=1+un de la même façon.