Annexe 3 — Tutoriel Python : Méthodes numériques interactives

Objectifs

Ce tutoriel vous permet de :

  • Implémenter les méthodes numériques du chapitre 2 en Python
  • Visualiser la convergence de manière interactive avec Plotly
  • Comparer le comportement des différentes méthodes
  • Expérimenter avec vos propres fonctions

Configuration de l'environnement

Option 1 : Google Colab (recommandé)

La façon la plus simple d'exécuter ce tutoriel est d'utiliser Google Colab, un environnement Jupyter gratuit dans le navigateur :

  1. Rendez-vous sur Google Colab
  2. Créez un nouveau notebook : Fichier → Nouveau notebook
  3. Les bibliothèques numpy et plotly sont déjà préinstallées !
  4. Copiez-collez le code des sections suivantes dans les cellules
💡

Avantages de Colab

  • Aucune installation requise
  • Fonctionne sur n'importe quel ordinateur avec un navigateur
  • Plotly fonctionne nativement (version 4.x+)
  • Sauvegarde automatique sur Google Drive

Option 2 : Installation locale (Jupyter Notebook)

Si vous préférez travailler localement :

Prérequis : Python 3.8+ installé sur votre machine

terminalbash
# Installation des bibliothèques (une seule fois)
pip install numpy plotly jupyter

# Ou avec conda :
conda install numpy plotly jupyter

# Lancer Jupyter Notebook
jupyter notebook

Bibliothèques utilisées

BibliothèqueVersionDescription
numpy≥ 1.20Calculs numériques (tableaux, fonctions mathématiques)
plotly≥ 4.0Graphiques interactifs (zoom, pan, animations)
⚠️

Version de Plotly

Assurez-vous d'avoir Plotly 4.x ou plus récent. Les versions antérieures nécessitaient une configuration spéciale pour Colab. Depuis la version 4, fig.show() fonctionne automatiquement dans tous les environnements (Colab, Jupyter, Kaggle, etc.).

Imports et configuration

Copiez ce bloc au début de votre notebook :

imports.pypython
# ============================================
# IMPORTS - Exécutez cette cellule en premier
# ============================================

import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots

# Configuration pour de meilleurs graphiques
import plotly.io as pio
pio.templates.default = "plotly_white"

print("✓ Bibliothèques chargées avec succès!")
print(f"  NumPy version: {np.__version__}")
import plotly
print(f"  Plotly version: {plotly.__version__}")

1. Méthode de bissection

Principe

La méthode de bissection divise successivement l'intervalle en deux, en gardant le sous-intervalle contenant la racine (changement de signe).

Implémentation

bissection.pypython
def bissection(f, a, b, tol=1e-10, max_iter=100):
  """
  Méthode de bissection pour trouver une racine de f dans [a, b].

  Paramètres:
  -----------
  f : fonction dont on cherche la racine
  a, b : bornes de l'intervalle (f(a) et f(b) doivent être de signes opposés)
  tol : tolérance sur la largeur de l'intervalle
  max_iter : nombre maximum d'itérations

  Retourne:
  ---------
  racine : approximation de la racine
  historique : liste des triplets (a, b, milieu) à chaque itération
  """
  # Vérification de la condition initiale
  if f(a) * f(b) > 0:
      raise ValueError("f(a) et f(b) doivent être de signes opposés")

  historique = []

  for i in range(max_iter):
      # Point milieu
      c = (a + b) / 2
      historique.append({'a': a, 'b': b, 'c': c, 'f_c': f(c)})

      # Critère d'arrêt
      if abs(b - a) < tol or abs(f(c)) < tol:
          break

      # Mise à jour de l'intervalle
      if f(a) * f(c) <= 0:
          b = c
      else:
          a = c

  return c, historique

Exemple d'utilisation

bissection_exemple.pypython
# Fonction exemple : f(x) = x³ - x - 2
def f(x):
  return x**3 - x - 2

# Recherche de la racine dans [1, 2]
racine, hist = bissection(f, 1, 2, tol=1e-8)
print(f"Racine trouvée : {racine:.10f}")
print(f"Nombre d'itérations : {len(hist)}")
print(f"Vérification : f({racine:.10f}) = {f(racine):.2e}")

Visualisation interactive

bissection_viz.pypython
def visualiser_bissection(f, a, b, n_iter=10):
  """Visualise les itérations de la méthode de bissection avec un slider."""

  # Calcul des itérations
  _, hist = bissection(f, a, b, tol=1e-15, max_iter=n_iter)

  # Grille pour tracer la fonction
  x = np.linspace(a - 0.5, b + 0.5, 500)
  y = np.array([f(xi) for xi in x])
  y_min, y_max = y.min() * 1.2, y.max() * 1.2

  # Création de la figure
  fig = go.Figure()

  # Tracé de la fonction (toujours visible)
  fig.add_trace(go.Scatter(x=x, y=y, mode='lines', name='f(x)',
                            line=dict(color='blue', width=2)))

  # Axe horizontal y=0
  fig.add_trace(go.Scatter(x=[a-0.5, b+0.5], y=[0, 0], mode='lines',
                            name='y=0', line=dict(color='black', width=1)))

  # Pour chaque itération, on crée des traces (initialement invisibles sauf la première)
  for i, h in enumerate(hist):
      visible = (i == 0)  # Seule la première itération est visible au départ

      # Ligne verticale gauche (borne a)
      fig.add_trace(go.Scatter(
          x=[h['a'], h['a']], y=[y_min, y_max],
          mode='lines', name=f"a={h['a']:.4f}",
          line=dict(color='orange', width=2, dash='dash'),
          visible=visible, showlegend=visible
      ))

      # Ligne verticale droite (borne b)
      fig.add_trace(go.Scatter(
          x=[h['b'], h['b']], y=[y_min, y_max],
          mode='lines', name=f"b={h['b']:.4f}",
          line=dict(color='orange', width=2, dash='dash'),
          visible=visible, showlegend=visible
      ))

      # Point milieu sur la courbe
      fig.add_trace(go.Scatter(
          x=[h['c']], y=[h['f_c']],
          mode='markers+text', name=f"c={h['c']:.6f}",
          marker=dict(color='green', size=14, symbol='star'),
          text=[f"c_{i+1}"], textposition='top center',
          visible=visible, showlegend=visible
      ))

      # Bornes sur l'axe x
      fig.add_trace(go.Scatter(
          x=[h['a'], h['b']], y=[0, 0],
          mode='markers', name='Intervalle',
          marker=dict(color='red', size=10, symbol='diamond'),
          visible=visible, showlegend=False
      ))

  # Création des steps pour le slider
  # Chaque itération a 4 traces (a, b, milieu, bornes)
  n_base_traces = 2  # f(x) et y=0
  n_traces_per_iter = 4

  steps = []
  for i in range(len(hist)):
      # Créer le masque de visibilité
      visibility = [True, True]  # f(x) et y=0 toujours visibles
      for j in range(len(hist)):
          if j == i:
              visibility.extend([True, True, True, True])
          else:
              visibility.extend([False, False, False, False])

      step = dict(
          method='update',
          args=[{'visible': visibility},
                {'title': f"Bissection - Itération {i+1}/{len(hist)} | "
                         f"Intervalle [{hist[i]['a']:.6f}, {hist[i]['b']:.6f}] | "
                         f"f(c) = {hist[i]['f_c']:.2e}"}],
          label=str(i+1)
      )
      steps.append(step)

  sliders = [dict(
      active=0,
      currentvalue={"prefix": "Itération: ", "font": {"size": 14}},
      pad={"t": 50},
      steps=steps
  )]

  fig.update_layout(
      sliders=sliders,
      title=f"Bissection - Itération 1/{len(hist)} | "
            f"Intervalle [{hist[0]['a']:.6f}, {hist[0]['b']:.6f}] | "
            f"f(c) = {hist[0]['f_c']:.2e}",
      xaxis_title="x",
      yaxis_title="f(x)",
      yaxis=dict(range=[y_min, y_max]),
      height=500
  )

  fig.show()

# Exécution
visualiser_bissection(f, 1, 2, n_iter=15)

2. Méthode de la sécante

Principe

La méthode de la sécante utilise une droite passant par deux points pour approximer la racine.

Implémentation

secante.pypython
def secante(f, x0, x1, tol=1e-10, max_iter=100):
  """
  Méthode de la sécante pour trouver une racine de f.

  Paramètres:
  -----------
  f : fonction dont on cherche la racine
  x0, x1 : deux approximations initiales
  tol : tolérance
  max_iter : nombre maximum d'itérations

  Retourne:
  ---------
  racine : approximation de la racine
  historique : liste des approximations successives
  """
  historique = [{'x': x0, 'f_x': f(x0)}, {'x': x1, 'f_x': f(x1)}]

  for i in range(max_iter):
      f_x0, f_x1 = f(x0), f(x1)

      # Protection contre la division par zéro
      if abs(f_x1 - f_x0) < 1e-15:
          break

      # Formule de la sécante
      x2 = x1 - f_x1 * (x1 - x0) / (f_x1 - f_x0)
      historique.append({'x': x2, 'f_x': f(x2), 'x_prev': x0, 'x_curr': x1})

      # Critère d'arrêt
      if abs(x2 - x1) < tol or abs(f(x2)) < tol:
          break

      # Mise à jour
      x0, x1 = x1, x2

  return x2, historique

Visualisation

secante_viz.pypython
def visualiser_secante(f, x0, x1, n_iter=8):
  """Visualise les itérations de la méthode de la sécante avec un slider."""

  racine, hist = secante(f, x0, x1, tol=1e-15, max_iter=n_iter)

  # Déterminer les bornes du graphique
  all_x = [h['x'] for h in hist]
  x_min, x_max = min(all_x) - 0.5, max(all_x) + 0.5

  x = np.linspace(x_min, x_max, 500)
  y = np.array([f(xi) for xi in x])
  y_min, y_max = min(y.min(), -1) * 1.2, max(y.max(), 1) * 1.2

  fig = go.Figure()

  # Traces de base (toujours visibles)
  fig.add_trace(go.Scatter(x=x, y=y, mode='lines', name='f(x)',
                            line=dict(color='blue', width=2)))
  fig.add_trace(go.Scatter(x=[x_min, x_max], y=[0, 0], mode='lines',
                            name='y=0', line=dict(color='black', width=1)))

  # Pré-calculer les sécantes (une par itération à partir de j=2)
  secantes_data = []
  for j in range(2, len(hist)):
      h = hist[j]
      if 'x_prev' in h:
          x_p, x_c = h['x_prev'], h['x_curr']
          f_p, f_c = f(x_p), f(x_c)
          if abs(x_c - x_p) > 1e-15:
              m = (f_c - f_p) / (x_c - x_p)
              x_line = np.array([x_min, x_max])
              y_line = f_c + m * (x_line - x_c)
              secantes_data.append((j, x_line, y_line))

  # Ajouter toutes les sécantes (initialement invisibles)
  for j, x_line, y_line in secantes_data:
      fig.add_trace(go.Scatter(
          x=x_line, y=y_line, mode='lines',
          name='Sécante',
          line=dict(color='red', width=2),
          opacity=0.7,
          visible=False
      ))

  # Ajouter les points pour chaque step_idx avec opacité décroissante
  # On crée n_points traces pour chaque step (avec opacités différentes)
  n_steps = len(hist)
  for step_idx in range(n_steps):
      for j in range(step_idx + 1):
          # Opacité : le point actuel a 1.0, les précédents diminuent de 15% par itération
          age = step_idx - j  # 0 pour le plus récent
          opacity = max(0.15, 1.0 - age * 0.15)

          h = hist[j]
          fig.add_trace(go.Scatter(
              x=[h['x']], y=[h['f_x']],
              mode='markers+text',
              name=f"x_{j}",
              marker=dict(color='green', size=12, opacity=opacity),
              text=[f'x_{j}'], textposition='top center',
              textfont=dict(color=f'rgba(0,128,0,{opacity})'),
              visible=False,
              showlegend=(j == step_idx)
          ))

  n_base = 2
  n_secantes = len(secantes_data)

  # Compter les traces de points par step
  # step 0: 1 point, step 1: 2 points, ..., step k: k+1 points
  # Total points traces = sum(1 to n_steps) = n_steps*(n_steps+1)/2

  # Créer les steps du slider
  steps = []
  point_trace_start = n_base + n_secantes

  for step_idx in range(n_steps):
      visibility = [True, True]  # f(x) et y=0

      # Sécantes : seulement celle de l'itération actuelle
      for sec_idx, (j, _, _) in enumerate(secantes_data):
          visibility.append(j == step_idx)

      # Points : calculer l'index des traces pour ce step
      # Les traces sont organisées : step0(1pt), step1(2pts), step2(3pts), ...
      for s in range(n_steps):
          n_pts_this_step = s + 1
          for p in range(n_pts_this_step):
              visibility.append(s == step_idx)

      step = dict(
          method='update',
          args=[{'visible': visibility}],
          label=str(step_idx)
      )
      steps.append(step)

  sliders = [dict(
      active=0,
      currentvalue={"prefix": "Itération: ", "font": {"size": 14}},
      pad={"t": 50},
      steps=steps
  )]

  fig.update_layout(
      sliders=sliders,
      title=f"Méthode de la sécante - Racine ≈ {racine:.8f}",
      xaxis_title="x",
      yaxis_title="f(x)",
      yaxis=dict(range=[y_min, y_max]),
      height=500
  )

  fig.show()

# Exécution
visualiser_secante(f, 1.0, 2.0, n_iter=6)

3. Méthode de Newton-Raphson

Principe

La méthode de Newton utilise la tangente à la courbe pour approximer la racine.

Implémentation

newton.pypython
def newton(f, df, x0, tol=1e-10, max_iter=100):
  """
  Méthode de Newton-Raphson pour trouver une racine de f.

  Paramètres:
  -----------
  f : fonction dont on cherche la racine
  df : dérivée de f
  x0 : approximation initiale
  tol : tolérance
  max_iter : nombre maximum d'itérations

  Retourne:
  ---------
  racine : approximation de la racine
  historique : liste des approximations successives
  """
  x = x0
  historique = [{'x': x, 'f_x': f(x), 'df_x': df(x)}]

  for i in range(max_iter):
      f_x = f(x)
      df_x = df(x)

      # Protection contre la division par zéro
      if abs(df_x) < 1e-15:
          print(f"Attention : dérivée proche de zéro à x = {x}")
          break

      # Formule de Newton
      x_new = x - f_x / df_x
      historique.append({'x': x_new, 'f_x': f(x_new), 'df_x': df(x_new),
                        'x_prev': x})

      # Critère d'arrêt
      if abs(x_new - x) < tol or abs(f(x_new)) < tol:
          break

      x = x_new

  return x, historique

Exemple avec dérivée

newton_exemple.pypython
# Fonction : f(x) = x³ - x - 2
def f(x):
  return x**3 - x - 2

# Dérivée : f'(x) = 3x² - 1
def df(x):
  return 3*x**2 - 1

# Recherche de la racine
racine, hist = newton(f, df, x0=1.5, tol=1e-12)
print(f"Racine trouvée : {racine:.15f}")
print(f"Nombre d'itérations : {len(hist)-1}")
print(f"Vérification : f({racine}) = {f(racine):.2e}")

# Afficher la convergence
print("\nConvergence :")
for i, h in enumerate(hist):
  print(f"  x_{i} = {h['x']:.15f}, f(x_{i}) = {h['f_x']:.2e}")

Visualisation des tangentes

newton_viz.pypython
def visualiser_newton(f, df, x0, n_iter=6):
  """Visualise les itérations de la méthode de Newton avec un slider."""

  racine, hist = newton(f, df, x0, tol=1e-15, max_iter=n_iter)

  # Bornes du graphique
  all_x = [h['x'] for h in hist]
  x_min, x_max = min(min(all_x) - 1, -1), max(max(all_x) + 1, 3)

  x = np.linspace(x_min, x_max, 500)
  y = np.array([f(xi) for xi in x])
  y_min, y_max = min(y.min(), -1) * 1.3, max(y.max(), 1) * 1.3

  fig = go.Figure()

  # Traces de base (toujours visibles)
  fig.add_trace(go.Scatter(x=x, y=y, mode='lines', name='f(x)',
                            line=dict(color='blue', width=2)))
  fig.add_trace(go.Scatter(x=[x_min, x_max], y=[0, 0], mode='lines',
                            name='y=0', line=dict(color='black', width=1)))

  # Pré-calculer les tangentes
  tangentes_data = []
  for j in range(1, len(hist)):
      hp = hist[j]
      if 'x_prev' in hp:
          xp = hp['x_prev']
          fp = f(xp)
          dfp = df(xp)
          x_tang = np.array([x_min, x_max])
          y_tang = fp + dfp * (x_tang - xp)
          tangentes_data.append((j, x_tang, y_tang, xp, fp))

  # Ajouter toutes les tangentes (initialement invisibles) - seulement la courante sera affichée
  for j, x_tang, y_tang, xp, fp in tangentes_data:
      fig.add_trace(go.Scatter(
          x=x_tang, y=y_tang, mode='lines',
          name='Tangente',
          line=dict(color='red', width=2),
          opacity=0.7,
          visible=False
      ))

  n_base = 2
  n_tangentes = len(tangentes_data)
  n_steps = len(hist)

  # Ajouter les points pour chaque step avec opacité décroissante
  for step_idx in range(n_steps):
      for j in range(step_idx + 1):
          age = step_idx - j
          opacity = max(0.15, 1.0 - age * 0.15)

          h = hist[j]
          # Point sur l'axe x
          fig.add_trace(go.Scatter(
              x=[h['x']], y=[0],
              mode='markers+text',
              name=f"x_{j}",
              marker=dict(color='purple', size=10, symbol='diamond', opacity=opacity),
              text=[f'x_{j}'], textposition='top center',
              textfont=dict(color=f'rgba(128,0,128,{opacity})'),
              visible=False,
              showlegend=(j == step_idx)
          ))

      # Point actuel sur la courbe (étoile verte, toujours opaque)
      h = hist[step_idx]
      fig.add_trace(go.Scatter(
          x=[h['x']], y=[h['f_x']],
          mode='markers',
          name='Point actuel',
          marker=dict(color='green', size=14, symbol='star'),
          visible=False,
          showlegend=False
      ))

  # Créer les steps du slider
  steps = []

  for step_idx in range(n_steps):
      visibility = [True, True]  # f(x) et y=0

      # Tangentes : seulement celle de l'itération actuelle
      for tang_idx, (j, _, _, _, _) in enumerate(tangentes_data):
          visibility.append(j == step_idx)

      # Points : chaque step a ses propres traces
      for s in range(n_steps):
          n_pts_this_step = s + 1  # points sur l'axe x
          for p in range(n_pts_this_step):
              visibility.append(s == step_idx)
          # Point sur la courbe
          visibility.append(s == step_idx)

      step = dict(
          method='update',
          args=[{'visible': visibility}],
          label=str(step_idx)
      )
      steps.append(step)

  sliders = [dict(
      active=0,
      currentvalue={"prefix": "Itération: ", "font": {"size": 14}},
      pad={"t": 50},
      steps=steps
  )]

  fig.update_layout(
      sliders=sliders,
      title=f"Méthode de Newton - x₀={x0} → Racine ≈ {racine:.10f}",
      xaxis_title="x",
      yaxis_title="f(x)",
      yaxis=dict(range=[y_min, y_max]),
      height=500
  )

  fig.show()

# Exécution
visualiser_newton(f, df, x0=2.0, n_iter=6)

4. Méthode du point fixe

Principe

On transforme en et on itère.

La convergence est garantie si au voisinage de la racine.

Implémentation

point_fixe.pypython
def point_fixe(g, x0, tol=1e-10, max_iter=100):
  """
  Méthode du point fixe : x_{n+1} = g(x_n).

  Paramètres:
  -----------
  g : fonction d'itération (on cherche x tel que x = g(x))
  x0 : approximation initiale
  tol : tolérance
  max_iter : nombre maximum d'itérations

  Retourne:
  ---------
  point_fixe : approximation du point fixe
  historique : liste des approximations successives
  """
  x = x0
  historique = [{'x': x, 'g_x': g(x)}]

  for i in range(max_iter):
      try:
          x_new = g(x)
      except:
          print(f"Erreur de calcul à l'itération {i}")
          break

      historique.append({'x': x_new, 'g_x': g(x_new) if abs(x_new) < 1e10 else float('inf')})

      # Critère d'arrêt
      if abs(x_new - x) < tol:
          break

      # Détection de divergence
      if abs(x_new) > 1e10:
          print(f"Divergence détectée à l'itération {i}")
          break

      x = x_new

  return x, historique

Visualisation : diagramme en toile d'araignée

point_fixe_viz.pypython
def visualiser_point_fixe(g, x0, x_min=-2, x_max=4, n_iter=15):
  """
  Visualise la méthode du point fixe avec un diagramme en toile d'araignée.
  """

  _, hist = point_fixe(g, x0, tol=1e-15, max_iter=n_iter)

  # Grilles
  x = np.linspace(x_min, x_max, 500)
  y_g = [g(xi) for xi in x]
  y_id = x  # y = x

  fig = go.Figure()

  # g(x) et y = x
  fig.add_trace(go.Scatter(x=x, y=y_g, mode='lines', name='g(x)',
                            line=dict(color='blue', width=2)))
  fig.add_trace(go.Scatter(x=x, y=y_id, mode='lines', name='y = x',
                            line=dict(color='black', width=1, dash='dash')))

  # Toile d'araignée
  cobweb_x = [hist[0]['x']]
  cobweb_y = [0]

  for i, h in enumerate(hist[:-1]):
      xi = h['x']
      gi = h['g_x']
      # Vertical : (xi, ?) → (xi, g(xi))
      cobweb_x.extend([xi, xi])
      cobweb_y.extend([cobweb_y[-1], gi])
      # Horizontal : (xi, g(xi)) → (g(xi), g(xi))
      cobweb_x.extend([xi, gi])
      cobweb_y.extend([gi, gi])

  fig.add_trace(go.Scatter(x=cobweb_x, y=cobweb_y, mode='lines',
                            name='Itérations', line=dict(color='red', width=1)))

  # Points d'itération
  for i, h in enumerate(hist):
      fig.add_trace(go.Scatter(x=[h['x']], y=[h['g_x']], mode='markers',
                               name=f'x_{i}', marker=dict(size=8),
                               showlegend=(i < 5)))

  # Point fixe théorique (si convergé)
  if len(hist) > 1:
      pf = hist[-1]['x']
      fig.add_trace(go.Scatter(x=[pf], y=[pf], mode='markers',
                               name='Point fixe',
                               marker=dict(color='green', size=15, symbol='star')))

  fig.update_layout(
      title=f"Méthode du point fixe - x₀ = {x0}",
      xaxis_title="x",
      yaxis_title="y",
      xaxis=dict(range=[x_min, x_max]),
      yaxis=dict(range=[x_min, x_max]),
      width=600,
      height=600
  )

  fig.show()

# Exemple : résoudre x³ - x - 2 = 0 ⟺ x = (x + 2)^(1/3)
def g(x):
  return np.cbrt(x + 2)  # racine cubique

visualiser_point_fixe(g, x0=1.0, x_min=0, x_max=3, n_iter=10)

5. Méthode Regula Falsi (fausse position)

Principe

Combine les avantages de la bissection (intervalle borné) et de la sécante (interpolation linéaire).

Implémentation

regula_falsi.pypython
def regula_falsi(f, a, b, tol=1e-10, max_iter=100):
  """
  Méthode Regula Falsi (fausse position).

  Paramètres:
  -----------
  f : fonction dont on cherche la racine
  a, b : bornes de l'intervalle (f(a) et f(b) doivent être de signes opposés)
  tol : tolérance
  max_iter : nombre maximum d'itérations

  Retourne:
  ---------
  racine : approximation de la racine
  historique : liste des approximations successives
  """
  if f(a) * f(b) > 0:
      raise ValueError("f(a) et f(b) doivent être de signes opposés")

  historique = []

  for i in range(max_iter):
      fa, fb = f(a), f(b)

      # Interpolation linéaire
      c = b - fb * (b - a) / (fb - fa)
      fc = f(c)

      historique.append({'a': a, 'b': b, 'c': c, 'f_c': fc})

      # Critère d'arrêt
      if abs(fc) < tol or abs(b - a) < tol:
          break

      # Mise à jour de l'intervalle
      if fa * fc <= 0:
          b = c
      else:
          a = c

  return c, historique

6. Comparaison des méthodes

Comparaison de la vitesse de convergence

comparaison.pypython
def comparer_methodes(f, df, a, b, x0, tol=1e-12):
  """Compare les différentes méthodes sur un même problème."""

  # Bissection
  _, hist_bis = bissection(f, a, b, tol=tol)

  # Sécante
  _, hist_sec = secante(f, a, b, tol=tol)

  # Newton
  _, hist_new = newton(f, df, x0, tol=tol)

  # Regula Falsi
  _, hist_rf = regula_falsi(f, a, b, tol=tol)

  # Graphique de convergence
  fig = go.Figure()

  # Erreur en fonction des itérations
  methods = [
      ('Bissection', hist_bis, 'blue'),
      ('Sécante', hist_sec, 'orange'),
      ('Newton', hist_new, 'green'),
      ('Regula Falsi', hist_rf, 'red')
  ]

  # Trouver la vraie racine (Newton avec haute précision)
  racine, _ = newton(f, df, x0, tol=1e-15, max_iter=100)

  for name, hist, color in methods:
      if 'c' in hist[0]:  # Bissection ou Regula Falsi
          erreurs = [abs(h['c'] - racine) for h in hist]
      else:  # Sécante ou Newton
          erreurs = [abs(h['x'] - racine) for h in hist]

      # Éviter log(0)
      erreurs = [max(e, 1e-16) for e in erreurs]

      fig.add_trace(go.Scatter(
          x=list(range(len(erreurs))),
          y=erreurs,
          mode='lines+markers',
          name=f'{name} ({len(hist)} iter)',
          line=dict(color=color)
      ))

  fig.update_layout(
      title="Comparaison des méthodes - Convergence",
      xaxis_title="Itération",
      yaxis_title="Erreur |x_n - racine|",
      yaxis_type="log",
      yaxis=dict(exponentformat='e'),
      legend=dict(x=0.7, y=0.95)
  )

  fig.show()

  # Tableau récapitulatif
  print("\n" + "="*60)
  print("RÉCAPITULATIF")
  print("="*60)
  print(f"Racine exacte : {racine:.15f}")
  print("-"*60)
  print(f"{'Méthode':<15} {'Itérations':<12} {'Erreur finale':<20}")
  print("-"*60)
  for name, hist, _ in methods:
      n_iter = len(hist)
      if 'c' in hist[-1]:
          final_x = hist[-1]['c']
      else:
          final_x = hist[-1]['x']
      erreur = abs(final_x - racine)
      print(f"{name:<15} {n_iter:<12} {erreur:.2e}")

# Exemple : f(x) = x³ - x - 2
comparer_methodes(f, df, a=1, b=2, x0=1.5)

7. Cas problématiques

Newton : sensibilité au point de départ

newton_problemes.pypython
# Fonction avec plusieurs racines : f(x) = sin(x)
def f_sin(x):
  return np.sin(x)

def df_sin(x):
  return np.cos(x)

def explorer_newton_sensibilite():
  """Explore comment le point de départ affecte la convergence de Newton."""

  fig = make_subplots(rows=1, cols=2,
                      subplot_titles=["Différents points de départ",
                                     "Racine trouvée vs x₀"])

  x = np.linspace(-10, 10, 1000)
  y = np.sin(x)

  # Graphique 1 : Trajectoires
  fig.add_trace(go.Scatter(x=x, y=y, mode='lines', name='sin(x)',
                            line=dict(color='blue')), row=1, col=1)
  fig.add_hline(y=0, line=dict(color='black', width=1), row=1, col=1)

  # Différents points de départ
  x0_list = [0.5, 1.4, 2.0, 3.0, 4.5]
  colors = ['red', 'green', 'orange', 'purple', 'brown']

  for x0, color in zip(x0_list, colors):
      racine, hist = newton(f_sin, df_sin, x0, tol=1e-10, max_iter=20)
      x_vals = [h['x'] for h in hist]
      y_vals = [f_sin(xi) for xi in x_vals]
      fig.add_trace(go.Scatter(x=x_vals, y=y_vals, mode='lines+markers',
                                name=f'x₀={x0} → {racine:.2f}',
                                line=dict(color=color)), row=1, col=1)

  # Graphique 2 : Bassin d'attraction
  x0_range = np.linspace(-10, 10, 200)
  racines_trouvees = []

  for x0 in x0_range:
      try:
          racine, _ = newton(f_sin, df_sin, x0, tol=1e-8, max_iter=50)
          # Arrondir à la racine la plus proche (multiple de π)
          racine_norm = round(racine / np.pi) * np.pi
          racines_trouvees.append(racine_norm)
      except:
          racines_trouvees.append(np.nan)

  fig.add_trace(go.Scatter(x=x0_range, y=racines_trouvees, mode='markers',
                            name='Racine trouvée', marker=dict(size=3)),
                row=1, col=2)

  fig.update_layout(height=400, title="Newton sur sin(x) : sensibilité au point de départ")
  fig.update_xaxes(title_text="x", row=1, col=1)
  fig.update_xaxes(title_text="x₀", row=1, col=2)
  fig.update_yaxes(title_text="y", row=1, col=1)
  fig.update_yaxes(title_text="Racine trouvée", row=1, col=2)

  fig.show()

explorer_newton_sensibilite()

Point fixe : convergence vs divergence

point_fixe_convergence.pypython
def comparer_fonctions_iteration():
  """
  Compare deux formulations de point fixe pour le même problème.
  x³ - x - 2 = 0 peut s'écrire :
  - g₁(x) = (x + 2)^(1/3)  (converge car |g₁'| < 1 près de la racine)
  - g₂(x) = x³ - 2         (diverge car |g₂'| > 1 près de la racine)
  """

  def g1(x):
      return np.cbrt(x + 2)

  def g2(x):
      return x**3 - 2

  fig = make_subplots(rows=1, cols=2,
                      subplot_titles=["g₁(x) = ∛(x+2) - CONVERGE",
                                     "g₂(x) = x³ - 2 - DIVERGE"])

  x = np.linspace(0, 3, 200)

  # g₁ : converge
  fig.add_trace(go.Scatter(x=x, y=[g1(xi) for xi in x], name='g₁(x)',
                            line=dict(color='blue')), row=1, col=1)
  fig.add_trace(go.Scatter(x=x, y=x, name='y=x',
                            line=dict(color='black', dash='dash')), row=1, col=1)

  _, hist1 = point_fixe(g1, 1.0, max_iter=10)
  cobweb_x1, cobweb_y1 = [hist1[0]['x']], [0]
  for h in hist1[:-1]:
      cobweb_x1.extend([h['x'], h['x'], h['g_x']])
      cobweb_y1.extend([cobweb_y1[-1], h['g_x'], h['g_x']])
  fig.add_trace(go.Scatter(x=cobweb_x1, y=cobweb_y1, name='Itérations',
                            line=dict(color='red')), row=1, col=1)

  # g₂ : diverge
  x2 = np.linspace(0.5, 2, 200)
  fig.add_trace(go.Scatter(x=x2, y=[min(g2(xi), 10) for xi in x2], name='g₂(x)',
                            line=dict(color='blue')), row=1, col=2)
  fig.add_trace(go.Scatter(x=x2, y=x2, name='y=x',
                            line=dict(color='black', dash='dash')), row=1, col=2)

  _, hist2 = point_fixe(g2, 1.2, max_iter=5)
  cobweb_x2, cobweb_y2 = [hist2[0]['x']], [0]
  for h in hist2[:-1]:
      xi, gi = h['x'], min(h['g_x'], 5)
      cobweb_x2.extend([xi, xi, gi])
      cobweb_y2.extend([cobweb_y2[-1], gi, gi])
  fig.add_trace(go.Scatter(x=cobweb_x2, y=cobweb_y2, name='Itérations',
                            line=dict(color='red')), row=1, col=2)

  fig.update_layout(height=400,
                    title="Point fixe : importance du choix de g(x)")
  fig.update_yaxes(range=[0, 4], row=1, col=1)
  fig.update_yaxes(range=[0, 5], row=1, col=2)

  fig.show()

  print("Dérivées au point fixe r ≈ 1.52:")
  r = 1.5214
  print(f"  |g₁'(r)| = |1/(3∛(r+2)²)| ≈ {abs(1/(3*np.cbrt(r+2)**2)):.3f} < 1 ✓")
  print(f"  |g₂'(r)| = |3r²| ≈ {abs(3*r**2):.3f} > 1 ✗")

comparer_fonctions_iteration()

8. Exercices pratiques

Exercice 1 : Trouver les racines de différentes fonctions

exercice1.pypython
# Testez les méthodes sur ces fonctions :

# 1. f(x) = e^x - 3x (deux racines)
def f1(x):
  return np.exp(x) - 3*x

def df1(x):
  return np.exp(x) - 3

# 2. f(x) = cos(x) - x (une racine près de 0.74)
def f2(x):
  return np.cos(x) - x

def df2(x):
  return -np.sin(x) - 1

# 3. f(x) = x² - 2 (racine = √2)
def f3(x):
  return x**2 - 2

def df3(x):
  return 2*x

# Essayez différentes méthodes et comparez !
# Exemple :
print("Racine de x² - 2 :")
racine, hist = newton(f3, df3, 1.0)
print(f"Newton : {racine:.15f}")
print(f"√2 réel : {np.sqrt(2):.15f}")

Exercice 2 : Implémenter Newton modifié pour racines multiples

exercice2.pypython
def newton_modifie(f, df, d2f, x0, tol=1e-10, max_iter=100):
  """
  Newton modifié pour racines multiples.

  u(x) = f(x)/f'(x)
  x_{n+1} = x_n - u(x_n)/u'(x_n)

  où u'(x) = 1 - f(x)f''(x)/f'(x)²
  """
  x = x0
  historique = [{'x': x}]

  for i in range(max_iter):
      fx = f(x)
      dfx = df(x)
      d2fx = d2f(x)

      if abs(dfx) < 1e-15:
          break

      u = fx / dfx
      u_prime = 1 - fx * d2fx / dfx**2

      if abs(u_prime) < 1e-15:
          break

      x_new = x - u / u_prime
      historique.append({'x': x_new})

      if abs(x_new - x) < tol:
          break

      x = x_new

  return x, historique

# Test sur f(x) = (x-1)³ (racine triple en x=1)
def f_triple(x):
  return (x - 1)**3

def df_triple(x):
  return 3 * (x - 1)**2

def d2f_triple(x):
  return 6 * (x - 1)

print("Racine triple de (x-1)³ :")
print("Newton classique :", end=" ")
r1, h1 = newton(f_triple, df_triple, 2.0)
print(f"{r1:.10f} en {len(h1)} itérations")

print("Newton modifié   :", end=" ")
r2, h2 = newton_modifie(f_triple, df_triple, d2f_triple, 2.0)
print(f"{r2:.10f} en {len(h2)} itérations")

Résumé des méthodes

MéthodeOrdreAvantagesInconvénients
Bissection1 (linéaire)Toujours converge, simpleLent, nécessite intervalle
Regula Falsi~1.6Plus rapide que bissectionPeut stagner
Sécante~1.618Pas besoin de dérivéePeut diverger
Newton2 (quadratique)Très rapideNécessite f', sensible à x₀
Point fixeDépend de gFlexibleChoix de g crucial
💡

Conseil pratique

En pratique, on combine souvent les méthodes : quelques itérations de bissection pour s'approcher de la racine (phase de localisation), puis Newton pour converger rapidement (phase de raffinement).