Fichier d’origine (300 × 350 pixels, taille du fichier : 213 kio, type MIME : image/gif, en boucle, 226 trames, 14 s)

    Ce fichier et sa description proviennent de Wikimedia Commons.

    Description

     Cette image matricielle GIF a été créée avec Python.
    Description
    Français : Itération de l'application 2/3-contractante f(x)=(x+sin⁡x)/3 depuis deux points initiaux. Les deux orbites convergent vers l'unique point fixe 0
    Date
    Source Travail personnel
    Auteur ou autrice Global Donald

    Conditions d’utilisation

    Moi, en tant que détenteur des droits d’auteur sur cette œuvre, je la publie sous la licence suivante :
    Creative Commons CC-Zero Ce fichier est dans le domaine public selon les termes de la licence Creative Commons CC0 1.0 Universel.
    La personne qui a associé une œuvre avec cet acte l’a placée dans le domaine public en renonçant mondialement à tous ses droits sur cette œuvre en vertu des lois relatives au droit d’auteur, ainsi qu’à tous les droits juridiques connexes et voisins qu’elle possédait sur l’œuvre, sans autre limite que celles imposées par la loi. Vous pouvez copier, modifier, distribuer et utiliser cette œuvre, y compris à des fins commerciales, sans qu’il soit nécessaire d’en demander la permission.

    Source code

    Python script used to generate this animation
    import numpy as np
    from PIL import Image, ImageDraw, ImageFont
    
    DOSSIER = os.path.dirname(os.path.abspath(__file__))
    SORTIE = os.path.join(DOSSIER, "Application_contractante_animation.gif")
    
    W, H = 300, 350
    SS = 3
    FPS = 20
    DUREE = 14.0
    NFRAMES = int(DUREE * FPS)
    INTRO = 1.5
    DUREE_PAS = 1.55
    NITER = 6
    FIN_PAS = INTRO + NITER * DUREE_PAS
    A0, B0 = 2.7, -2.2
    K = 2.0 / 3.0
    
    FOND = (251, 250, 247, 255)
    BLANC = (255, 255, 255, 255)
    ENCRE = (31, 35, 42, 255)
    GRIS = (104, 111, 120, 255)
    GRIS_CLAIR = (218, 219, 214, 255)
    GRILLE = (233, 233, 228, 255)
    TEAL = (7, 122, 117, 255)
    TEAL_SOMBRE = (5, 91, 88, 255)
    ROUGE = (217, 79, 48, 255)
    ROUGE_PALE = (246, 190, 171, 255)
    Bleu = (39, 111, 191, 255)
    Bleu_PALE = (183, 211, 243, 255)
    OR = (222, 145, 32, 255)
    OR_SOMBRE = (151, 91, 13, 255)
    
    POL = "/usr/share/fonts/truetype/dejavu/"
    FTITRE = ImageFont.truetype(POL + "DejaVuSans-Bold.ttf", 14 * SS)
    FSOUS = ImageFont.truetype(POL + "DejaVuSans.ttf", 10.5 * SS)
    FPETIT = ImageFont.truetype(POL + "DejaVuSans.ttf", 8.0 * SS)
    FPETITG = ImageFont.truetype(POL + "DejaVuSans-Bold.ttf", 8.2 * SS)
    FFORM = ImageFont.truetype(POL + "DejaVuSerif.ttf", 10.0 * SS)
    FMINI = ImageFont.truetype(POL + "DejaVuSans.ttf", 6.8 * SS)
    
    PLOT = (34.0, 56.0, 266.0, 288.0)  # x0, y0, x1, y1 écran
    ECHELLE = (PLOT[2] - PLOT[0]) / 6.0
    OX = (PLOT[0] + PLOT[2]) / 2.0
    OY = (PLOT[1] + PLOT[3]) / 2.0
    
    
    def f(x):
        return (x + math.sin(x)) / 3.0
    
    
    def orbite(x0):
        vals = [float(x0)]
        for _ in range(NITER):
            vals.append(f(vals[-1]))
        return vals
    
    
    ORB_A = orbite(A0)
    ORB_B = orbite(B0)
    DIST = [abs(a - b) for a, b in zip(ORB_A, ORB_B)]
    
    
    def P(x, y):
        return (x * SS, y * SS)
    
    
    def B(x0, y0, x1, y1):
        return (x0 * SS, y0 * SS, x1 * SS, y1 * SS)
    
    
    def XY(x, y):
        return (OX + ECHELLE * x, OY - ECHELLE * y)
    
    
    def borne(x, a=0.0, b=1.0):
        return max(a, min(b, x))
    
    
    def doux(a, b, x):
        q = borne((x - a) / (b - a))
        return q * q * (3.0 - 2.0 * q)
    
    
    def lerp(a, b, q):
        return a + (b - a) * q
    
    
    def lerp_p(p, q, t):
        return (lerp(p[0], q[0], t), lerp(p[1], q[1], t))
    
    
    def trait(d, pts, couleur, largeur=2.0):
        if len(pts) >= 2:
            d.line([P(x, y) for x, y in pts], fill=couleur,
                   width=max(1, int(round(largeur * SS))), joint="curve")
    
    
    def point(d, centre, rayon, couleur, bord=BLANC, largeur=1.2):
        x, y = centre
        d.ellipse(B(x - rayon, y - rayon, x + rayon, y + rayon),
                  fill=couleur, outline=bord,
                  width=max(1, int(round(largeur * SS))))
    
    
    def etat_animation(t):
        """Indice du pas courant, fraction dans le pas et nombre affiché."""
        if t < INTRO:
            return 0, 0.0, 0
        if t >= FIN_PAS:
            return NITER, 1.0, NITER
        idx = int((t - INTRO) // DUREE_PAS)
        u = ((t - INTRO) - idx * DUREE_PAS) / DUREE_PAS
        n_aff = idx + (1 if u >= 0.94 else 0)
        return idx, float(u), n_aff
    
    
    def chemin_orbite(vals, idx, u):
        """
        Retourne (segments terminés, segment courant partiel, point mobile,
        abscisse scalaire courante).
        Les segments sont dans le plan (x,y) mathématique.
        """
        segments = []
        # Point de départ du pas j.
        def depart(j):
            return (vals[j], 0.0) if j == 0 else (vals[j], vals[j])
        for j in range(min(idx, NITER)):
            s = depart(j)
            v = (vals[j], vals[j + 1])
            h = (vals[j + 1], vals[j + 1])
            segments.extend([(s, v), (v, h)])
        if idx >= NITER:
            p = (vals[-1], vals[-1])
            return segments, None, p, vals[-1]
        s = depart(idx)
        v = (vals[idx], vals[idx + 1])
        h = (vals[idx + 1], vals[idx + 1])
        qv = doux(0.05, 0.45, u)
        qh = doux(0.52, 0.92, u)
        courant = []
        if qv > 0:
            courant.append((s, lerp_p(s, v, qv)))
        if qh > 0:
            courant.append((v, lerp_p(v, h, qh)))
        if u < 0.52:
            mobile = lerp_p(s, v, qv)
            scalaire = vals[idx]
        else:
            mobile = lerp_p(v, h, qh)
            scalaire = lerp(vals[idx], vals[idx + 1], qh)
        return segments, courant, mobile, scalaire
    
    
    def dessiner_fond(img, t):
        d = ImageDraw.Draw(img, "RGBA")
        # Ombre et panneau du graphique.
        d.rounded_rectangle(B(PLOT[0] + 2, PLOT[1] + 3, PLOT[2] + 2, PLOT[3] + 3),
                            radius=8 * SS, fill=(226, 224, 217, 255))
        d.rounded_rectangle(B(*PLOT), radius=8 * SS, fill=BLANC,
                            outline=GRIS_CLAIR, width=1 * SS)
        # Grille.
        for v in (-2, -1, 1, 2):
            x, _ = XY(v, 0)
            trait(d, [(x, PLOT[1] + 5), (x, PLOT[3] - 5)], GRILLE, 0.8)
            _, y = XY(0, v)
            trait(d, [(PLOT[0] + 5, y), (PLOT[2] - 5, y)], GRILLE, 0.8)
        # Axes.
        trait(d, [(PLOT[0] + 4, OY), (PLOT[2] - 4, OY)], (178, 183, 190, 255), 1.0)
        trait(d, [(OX, PLOT[1] + 4), (OX, PLOT[3] - 4)], (178, 183, 190, 255), 1.0)
        for v in (-3, 0, 3):
            x, _ = XY(v, 0)
            d.text(P(x, PLOT[3] - 14), str(v), font=FMINI, fill=GRIS, anchor="ma")
            _, y = XY(0, v)
            if v != 0:
                d.text(P(PLOT[0] + 6, y), str(v), font=FMINI, fill=GRIS, anchor="lm")
        # Diagonale y=x, en tirets.
        xx = np.linspace(-3, 3, 25)
        for i in range(0, len(xx) - 1, 2):
            trait(d, [XY(xx[i], xx[i]), XY(xx[i + 1], xx[i + 1])],
                  (158, 164, 171, 255), 1.2)
        d.text(P(*XY(2.47, 2.68)), "y = x", font=FPETIT, fill=GRIS, anchor="ma")
        # Courbe de f.
        xs = np.linspace(-3, 3, 241)
        trait(d, [XY(float(x), f(float(x))) for x in xs], TEAL, 2.4)
        d.text(P(43, 66), "f(x) = (x + sin x)/3", font=FFORM,
               fill=TEAL_SOMBRE, anchor="la")
        d.text(P(43, 80), "k = 2/3", font=FPETITG, fill=TEAL_SOMBRE, anchor="la")
    
    
    def dessiner_orbite(d, vals, idx, u, couleur, pale):
        segments, courant, mobile, scalaire = chemin_orbite(vals, idx, u)
        for a, b in segments:
            trait(d, [XY(*a), XY(*b)], couleur, 1.8)
        if courant:
            for a, b in courant:
                trait(d, [XY(*a), XY(*b)], pale, 4.2)
                trait(d, [XY(*a), XY(*b)], couleur, 2.0)
        # Itérés déjà atteints sur la diagonale.
        nb = min(idx + (1 if u >= 0.94 else 0), NITER) if idx < NITER else NITER
        for j in range(1, nb + 1):
            point(d, XY(vals[j], vals[j]), 2.6, couleur)
        point(d, XY(*mobile), 4.6, couleur, largeur=1.4)
        return scalaire
    
    
    def sous_titre(t, idx):
        if t < INTRO:
            return "Deux départs, une même application"
        if t < FIN_PAS:
            return "À chaque pas, l’écart est multiplié par au plus 2/3"
        return "Point fixe unique : x* = 0"
    
    
    def faire_image(t):
        img = Image.new("RGBA", (W * SS, H * SS), FOND)
        dessiner_fond(img, t)
        d = ImageDraw.Draw(img, "RGBA")
        idx, u, n_aff = etat_animation(t)
    
        # Orbites.
        a_cur = dessiner_orbite(d, ORB_A, idx, u, ROUGE, ROUGE_PALE)
        b_cur = dessiner_orbite(d, ORB_B, idx, u, Bleu, Bleu_PALE)
    
        # Points initiaux sur l'axe des abscisses et étiquettes au début.
        point(d, XY(A0, 0), 3.4, ROUGE)
        point(d, XY(B0, 0), 3.4, Bleu)
        if t < 3.4:
            d.text(P(*XY(A0, 0.18)), "a₀", font=FPETITG, fill=ROUGE, anchor="ma")
            d.text(P(*XY(B0, 0.18)), "b₀", font=FPETITG, fill=Bleu, anchor="ma")
    
        # Point fixe, avec un léger anneau pulsant dans le final.
        if t >= FIN_PAS:
            phase = (t - FIN_PAS) % 1.2
            r = 7.0 + 5.0 * (phase / 1.2)
            coul = tuple(int(round(lerp(c, 255, phase / 1.2))) for c in OR[:3]) + (255,)
            x0, y0 = XY(0, 0)
            d.ellipse(B(x0 - r, y0 - r, x0 + r, y0 + r), outline=coul,
                      width=max(1, int(round(1.2 * SS))))
        point(d, XY(0, 0), 4.2, OR, largeur=1.3)
        d.text(P(OX + 8, OY + 7), "x* = 0", font=FPETITG,
               fill=OR_SOMBRE, anchor="la")
    
        # Titre, sous-titre et compteur.
        d.text(P(16, 11), "APPLICATION CONTRACTANTE", font=FTITRE,
               fill=ENCRE, anchor="la")
        d.text(P(284, 13), f"n = {n_aff}", font=FPETITG, fill=GRIS, anchor="ra")
        d.text(P(150, 33), sous_titre(t, idx), font=FSOUS,
               fill=ENCRE, anchor="ma")
    
        # Règle des distances, alignée sur l'échelle du graphique.
        yregle = 318.0
        d.text(P(16, 297), "distance entre les orbites", font=FPETIT,
               fill=GRIS, anchor="la")
        dist = abs(a_cur - b_cur)
        d.text(P(284, 297), f"d = {dist:.2f}".replace(".", ","),
               font=FPETITG, fill=ENCRE, anchor="ra")
        trait(d, [(PLOT[0], yregle), (PLOT[2], yregle)], (190, 194, 198, 255), 1.0)
        for v in (-3, 0, 3):
            x = XY(v, 0)[0]
            trait(d, [(x, yregle - 3), (x, yregle + 3)], (160, 165, 171, 255), 1.0)
        xa = XY(a_cur, 0)[0]
        xb = XY(b_cur, 0)[0]
        ybra = yregle - 9
        trait(d, [(xb, ybra), (xa, ybra)], ENCRE, 1.7)
        trait(d, [(xb, ybra - 3), (xb, ybra + 3)], ENCRE, 1.4)
        trait(d, [(xa, ybra - 3), (xa, ybra + 3)], ENCRE, 1.4)
        point(d, (xa, yregle), 3.8, ROUGE)
        point(d, (xb, yregle), 3.8, Bleu)
        if n_aff >= 1:
            ratio = DIST[n_aff] / DIST[n_aff - 1]
            txt = f"dernier pas : × {ratio:.2f}".replace(".", ",")
            d.text(P(150, 329), txt, font=FPETIT, fill=GRIS, anchor="ma")
        else:
            d.text(P(150, 329), "d₀ = 4,90", font=FPETIT, fill=GRIS, anchor="ma")
    
        return img.resize((W, H), Image.Resampling.LANCZOS).convert("RGB")
    
    
    def encoder(frames):
        echant = [fr.resize((150, 175)) for fr in frames[::10][:28]]
        mos = Image.new("RGB", (7 * 150, 4 * 175))
        for i, fr in enumerate(echant):
            mos.paste(fr, ((i % 7) * 150, (i // 7) * 175))
        pal = mos.quantize(colors=160, method=Image.Quantize.MEDIANCUT)
        pframes = [fr.quantize(palette=pal, dither=Image.Dither.NONE) for fr in frames]
        pframes[0].save(SORTIE, save_all=True, append_images=pframes[1:],
                        duration=[50] * len(pframes), loop=0, optimize=True)
    
    
    def aperçus():
        temps = [0.0, 1.0, 2.25, 4.0, 6.5, 9.5, 11.5, 13.4]
        noms = ["départ", "intro", "pas 1", "pas 2", "milieu", "pas 6", "final", "boucle"]
        planche = Image.new("RGB", (4 * W, 2 * (H + 22)), (235, 235, 235))
        d = ImageDraw.Draw(planche)
        ft = ImageFont.truetype(POL + "DejaVuSans.ttf", 11)
        for i, (t, nom) in enumerate(zip(temps, noms)):
            fr = faire_image(t)
            x, y = (i % 4) * W, (i // 4) * (H + 22)
            d.text((x + 6, y + 4), f"t={t:.2f} — {nom}", font=ft, fill=(0, 0, 0))
            planche.paste(fr, (x, y + 22))
            if nom in ("pas 1", "final"):
                fr.save(os.path.join(DOSSIER, f"design_{nom.replace(' ', '_')}.png"))
        planche.save(os.path.join(DOSSIER, "design_planche.png"))
    
    
    def main():
        if "--previews" in sys.argv:
            aperçus()
            print("aperçus écrits")
            print("orbites A", [round(x, 6) for x in ORB_A])
            print("orbites B", [round(x, 6) for x in ORB_B])
            print("distances", [round(x, 6) for x in DIST])
            print("ratios", [round(DIST[i] / DIST[i - 1], 6) for i in range(1, len(DIST))])
            return
        frames = []
        for i in range(NFRAMES):
            frames.append(faire_image(i / FPS))
            if i % 25 == 0 or i == NFRAMES - 1:
                print(f"rendu {i+1}/{NFRAMES}", flush=True)
        # Fondu enchaîné final vers le premier état, sans première image vide.
        nfondu = int(0.5 * FPS)
        for j in range(nfondu):
            idx = NFRAMES - nfondu + j
            q = doux(0.0, 1.0, (j + 1) / nfondu)
            frames[idx] = Image.blend(frames[idx], frames[0], q)
        encoder(frames)
        with Image.open(SORTIE) as im:
            print(f"GIF : {SORTIE}")
            print(f"{im.width}x{im.height}, {im.n_frames} images stockées, "
                  f"{os.path.getsize(SORTIE):,} octets, "
                  f"produit={im.width*im.height*im.n_frames:,}")
    
    
    if __name__ == "__main__":
        main()
    

    Légendes

    Itération de l'application <math>2/3</math>-contractante <math>f(x) = (x + \sin x)/3</math> depuis deux points initiaux. Les deux orbites convergent vers l'unique point fixe <math>0</math>

    Éléments décrits dans ce fichier

    dépeint

    218 041 octet

    350 pixel

    300 pixel

    image/gif

    0acea03d45d3009e28483bd5cb7dd31b153ef664

    Historique du fichier

    Cliquer sur une date et heure pour voir le fichier tel qu'il était à ce moment-là.

    Date et heureVignetteDimensionsUtilisateurCommentaire
    actuel27 octobre 2026 à 15:23Vignette pour la version du 27 octobre 2026 à 15:23300 × 350 (213 kio)Global DonaldUploaded own work with UploadWizard

    La page suivante utilise ce fichier :