Code Python 2

Code FFT Glissante

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import wave
import os
from pygments import pigmentize
from matplotlib.animation import PillowWriter

# ----------------------------
# PARAMÈTRE : chemin audio
# ----------------------------
# filename = "audio.wav"
filename = "no_audio.wav"

# ----------------------------
# Chargement ou génération du signal
# ----------------------------
if os.path.exists(filename):

    wav = wave.open(filename, 'rb')
    fs = wav.getframerate()
    n_samples = wav.getnframes()

    audio = wav.readframes(n_samples)
    signal = np.frombuffer(audio, dtype=np.int16).astype(float)

    if wav.getnchannels() == 2:
        signal = signal[::2]

    signal /= np.max(np.abs(signal))
    wav.close()

else:
    fs = 4400
    duration = 3
    t = np.arange(0, duration, 1/fs)
    freqs_notes = [262, 2300, 392, 1230]
    signal = np.zeros_like(t)
    segment = len(t) // len(freqs_notes)

    for i, f in enumerate(freqs_notes):
        start = i * segment
        end = (i + 1) * segment
        signal[start:end] = np.sin(2*np.pi*f*t[start:end])

t = np.arange(len(signal)) / fs

# ----------------------------
# PARAMÈTRES FFT
# ----------------------------
window_size = 1024
hop_size = 50
window = np.hanning(window_size)
freqs = np.fft.rfftfreq(window_size, 1/fs)

# ----------------------------
# BANQUE DE 22 FILTRES RECTANGULAIRES
# ----------------------------
f_min = 20
#f_max = 20000
f_max = fs
n_filters = 22

width_min = 20
width_max = 500

centers = np.linspace(f_min, f_max, n_filters)
widths = np.linspace(width_min, width_max, n_filters)

def build_filterbank(freqs):
    filters = np.zeros((n_filters, len(freqs)))
    for i in range(n_filters):
        f0 = centers[i]
        w = widths[i] / 2
        mask = (freqs >= f0 - w) & (freqs <= f0 + w)
        filters[i, mask] = 1.0
    return filters

filterbank = build_filterbank(freqs)

# ----------------------------
# FIGURE : 4 FENÊTRES
# ----------------------------
fig, (ax_global, ax_time, ax_fft, ax_filt) = plt.subplots(4, 1, figsize=(12, 12), dpi = 60)

# -------------------------------------------------
# 1) SIGNAL GLOBAL + FENÊTRE GLISSANTE
# -------------------------------------------------
ax_global.set_title("Signal global avec fenêtre glissante")
ax_global.plot(t, signal, color='steelblue', lw=1)

ax_global.set_xlim(0, t[-1])
ax_global.set_ylim(-1.1, 1.1)

# rectangle représentant la fenêtre
window_patch = ax_global.axvspan(
    0,
    window_size/fs,
    color='red',
    alpha=0.3
)

# -------------------------------------------------
# 2) SIGNAL LOCAL
# -------------------------------------------------
ax_time.set_title("Signal local analysé")
ax_time.set_xlim(0, window_size/fs)
ax_time.set_ylim(-1.1, 1.1)

line_time, = ax_time.plot([], [], lw=1.5)

# -------------------------------------------------
# 3) FFT GLISSANTE
# -------------------------------------------------
ax_fft.set_title("FFT glissante")
ax_fft.set_xlim(0, np.max(freqs))
ax_fft.set_ylim(0, 1)

line_fft, = ax_fft.plot([], [], lw=1.5, color='orange')

# -------------------------------------------------
# 4) ÉNERGIE FILTRÉE
# -------------------------------------------------
ax_filt.set_title("Énergie dans les 22 bandes fréquentielles")
ax_filt.set_xlim(-0.5, n_filters - 0.5)
ax_filt.set_ylim(0, 1)

bars = ax_filt.bar(np.arange(n_filters), np.zeros(n_filters))

plt.tight_layout()

# ----------------------------
# ANIMATION
# ----------------------------
def update(frame):

    start = frame * hop_size
    end = start + window_size

    if end >= len(signal):
        return line_time, line_fft, bars

    # segment courant
    segment = signal[start:end]

    # fenêtrage
    windowed = segment * window

    # FFT
    spectrum = np.abs(np.fft.rfft(windowed))
    spectrum /= np.max(spectrum) + 1e-12

    # -------------------------------------------------
    # 1) MISE À JOUR FENÊTRE GLISSANTE
    # -------------------------------------------------
    t_start = start / fs
    t_end = end / fs

    window_patch.set_xy([
        [t_start, -1.1],
        [t_start,  1.1],
        [t_end,    1.1],
        [t_end,   -1.1],
        [t_start, -1.1]
    ])

    # -------------------------------------------------
    # 2) SIGNAL LOCAL
    # -------------------------------------------------
    time_axis = np.arange(window_size) / fs

    line_time.set_data(time_axis, segment)

    # -------------------------------------------------
    # 3) FFT
    # -------------------------------------------------
    line_fft.set_data(freqs, spectrum)

    # -------------------------------------------------
    # 4) ÉNERGIES FILTRÉES
    # -------------------------------------------------
    energies = filterbank @ spectrum
    energies /= np.max(energies) + 1e-12

    for i, b in enumerate(bars):
        b.set_height(energies[i])

    return line_time, line_fft, bars

# animation
frames = (len(signal) - window_size) // hop_size

ani = animation.FuncAnimation(
    fig,
    update,
    frames=frames,
    interval=30,
    blit=False
)

# CODE POUR SAUVEGARDER DES GIF PAS TROP VOLUMINEUX
# METTRE LE dpi = 60 DANS LES FIGURES
# from matplotlib.animation import PillowWriter

# ani = FuncAnimation(fig, update, frames=800, interval=20)

ani.save("FFT_GLISSANTE.gif",
           writer=PillowWriter(fps=20)) #, save_count=80)

plt.show()

# pygmentize -f pdf -o FFT_Glissante_v3.pdf FFT_Glissante_v3.py

Code Aliasing Shannon

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.animation import PillowWriter

# =========================
# PARAMÈTRES
# =========================
f_signal = 50
T = 0.2

fe_ok = 500
fe_bad = 70

t_cont = np.linspace(0, T, 5000)

t_ok = np.arange(0, T, 1/fe_ok)
t_bad = np.arange(0, T, 1/fe_bad)

frames = 120

# =========================
# FIGURE
# =========================
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 9), dpi = 60)

# =========================
# 1. TEMPS + SHANNON / NYQUIST
# =========================
ax1.plot(t_cont, np.sin(2*np.pi*f_signal*t_cont), 'k', alpha=0.3)

line_ok, = ax1.plot([], [], 'bo-', label="Sans aliasing")
line_bad, = ax1.plot([], [], 'ro-', label="Avec aliasing")

nyquist_line = ax1.axhline(0, color='green', linestyle='--', label="Nyquist (fe/2)")

ax1.set_xlim(0, T)
ax1.set_ylim(-1.2, 1.2)
ax1.set_title("Visualisation de l'aliasing (fsignal = 50 Hz, fech1 = 500 Hz, fech2 = 70 Hz")
ax1.legend()
ax1.grid()

# =========================
# 2. SPECTRE + REPLIEMENT
# =========================
def spectrum(x, fe):
    N = len(x)
    X = np.fft.fft(x)
    f = np.fft.fftfreq(N, 1/fe)
    return f[:N//2], np.abs(X[:N//2]) / N

# spectres fixes
x_ok = np.sin(2*np.pi*f_signal*t_ok)
x_bad = np.sin(2*np.pi*f_signal*t_bad)

f1, X1 = spectrum(x_ok, fe_ok)
f2, X2 = spectrum(x_bad, fe_bad)

stem_ok = ax2.stem(f1, X1, linefmt='b-', markerfmt='bo', basefmt=" ", label="OK")
stem_bad = ax2.stem(f2, X2, linefmt='r-', markerfmt='ro', basefmt=" ", label="Aliasing")

f_max_line = ax2.axvline(f_signal, color='black', linestyle='--', label="f_max")

alias_line = ax2.axvline(0, color='red', linestyle='--', label="f_alias")

ax2.set_xlim(0, 200)
ax2.set_ylim(0, 0.6)
ax2.set_title("Repliement spectral")
ax2.grid()
ax2.legend()

# =========================
# UPDATE
# =========================
def update(i):
    # -------------------------
    # temps
    # -------------------------
    idx_ok = max(1, int(len(t_ok)*i/frames))
    idx_bad = max(1, int(len(t_bad)*i/frames))

    line_ok.set_data(t_ok[:idx_ok], np.sin(2*np.pi*f_signal*t_ok[:idx_ok]))
    line_bad.set_data(t_bad[:idx_bad], np.sin(2*np.pi*f_signal*t_bad[:idx_bad]))

    # -------------------------
    # SHANNON DYNAMIQUE
    # on fait varier fe_bad progressivement
    # -------------------------
    fe_dynamic = np.linspace(40, 200, frames)[i]
    nyquist = fe_dynamic / 2
    shannon_limit = 2 * f_signal

    # ligne Nyquist
    nyquist_line.set_ydata([0, 0])  # stable visuellement
    nyquist_line.set_label(f"Nyquist = {nyquist:.1f} Hz")

    # -------------------------
    # SPECTRE + ALIAS
    # -------------------------
    fe_current = np.linspace(40, 200, frames)[i]

    if f_signal > fe_current / 2:
        f_alias = abs(f_signal - fe_current)
    else:
        f_alias = f_signal

    alias_line.set_xdata([f_alias, f_alias])

    return line_ok, line_bad, nyquist_line, alias_line

ani = FuncAnimation(fig, update, frames=frames, interval=50, blit=False)

# CODE POUR SAUVEGARDER DES GIF PAS TROP VOLUMINEUX
# METTRE LE dpi = 60 DANS LES FIGURES
# from matplotlib.animation import PillowWriter
# ani = FuncAnimation(fig, update, frames=80, interval=20)

# ani.save("Shannon.gif",
#         writer=PillowWriter(fps=20)) #, save_count=80)

plt.tight_layout()
plt.show()

Code SHFT

import numpy as np
import matplotlib.pyplot as plt
import wave
import os

# ----------------------------
# PARAMÈTRE : chemin audio
# ----------------------------
filename = "audio.wav"   # <-- change ici

# ----------------------------
# Essai de chargement du fichier audio
# ----------------------------
if os.path.exists(filename):

    print("Fichier audio trouvé : utilisation du fichier")

    wav = wave.open(filename, 'rb')

    fs = wav.getframerate()
    n_samples = wav.getnframes()

    audio = wav.readframes(n_samples)
    signal = np.frombuffer(audio, dtype=np.int16)

    # Normalisation
    signal = signal / np.max(np.abs(signal))

    # Conversion stéréo → mono
    if wav.getnchannels() == 2:
        signal = signal[::2]

    wav.close()

    t = np.arange(len(signal)) / fs

else:

    print("Fichier non trouvé → génération d'un signal musical")

    fs = 1000
    duration = 5
    t = np.arange(0, duration, 1/fs)

    freqs_notes = [262, 330, 392, 523]  # Do - Mi - Sol - Do
    signal = np.zeros_like(t)

    segment = len(t) // len(freqs_notes)

    for i, f in enumerate(freqs_notes):
        start = i * segment
        end = (i + 1) * segment
        signal[start:end] = np.sin(2*np.pi*f*t[start:end])

# ----------------------------
# STFT PARAMÈTRES
# ----------------------------
window_size = 512
hop_size = 128

window = np.hanning(window_size)

spectrogram = []
times = []

# ----------------------------
# STFT glissante
# ----------------------------
for start in range(0, len(signal) - window_size, hop_size):

    segment = signal[start:start + window_size]
    windowed = segment * window

    fft = np.fft.rfft(windowed)
    spectrogram.append(np.abs(fft))

    times.append(start / fs)

spectrogram = np.array(spectrogram).T
freqs = np.fft.rfftfreq(window_size, 1/fs)

# ----------------------------
# AFFICHAGE
# ----------------------------

# 1) Signal temporel
plt.figure(figsize=(12, 4))
plt.plot(t, signal)
plt.xlabel("Temps (s)")
plt.ylabel("Amplitude")
plt.title("Signal temporel")
plt.grid(True)

# 2) Spectrogramme
plt.figure(figsize=(12, 6))
plt.pcolormesh(times, freqs, spectrogram, shading='auto')
plt.xlabel("Temps (s)")
plt.ylabel("Fréquence (Hz)")
plt.title("Spectrogramme (STFT)")
plt.colorbar(label="Amplitude")
plt.ylim(0, fs/2)

plt.show()

Vibration membrane cochlée

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.animation import PillowWriter

# ==================================================
# PARAMETRES
# ==================================================

c = 1.0
k = 8.0
omega = c * k

L = 10.0

sigma = 1.2

x01 = -5.0

delay = 8.0
x02 = x01 - c * delay

r = 1.0

x = np.linspace(-15, 15, 1200)

# ==================================================
# PAQUET INCIDENT
# ==================================================

def packet(x, t, x0):

    envelope = np.exp(
        -((x - x0 - c*t)**2)/(2*sigma**2)
    )

    carrier = np.cos(k*x - omega*t)

    return envelope * carrier

# ==================================================
# PAQUET REFLECHI
# ==================================================

def reflected_packet(x, t, x0):

    tc = (L - x0)/c

    if t < tc:
        return np.zeros_like(x)

    envelope = np.exp(
        -((x - (2*L - x0 - c*t))**2)/(2*sigma**2)
    )

    carrier = np.cos(-k*x - omega*t)

    return r * envelope * carrier

# ==================================================
# MEMBRANE BASILAIRE SIMPLIFIEE
# ==================================================

x_bf = 4          # position résonante
sigma_bf = 0.8      # largeur de la zone active

gain_bf = np.exp(
    -((x - x_bf)**2)/(2*sigma_bf**2)
)

# ==================================================
# FIGURE
# ==================================================

fig, (ax1, ax2) = plt.subplots(
    2, 1,
    figsize=(10, 8)
)

# ------------------------
# Panneau supérieur
# ------------------------

line_total, = ax1.plot(
    [], [],
    color='navy',
    lw=2
)

ax1.axvline(
    L,
    color='red',
    linestyle=':',
    label='Interface'
)

ax1.set_xlim(-15, 15)
ax1.set_ylim(-2.5, 2.5)

ax1.set_ylabel("Amplitude")
ax1.set_title("Propagation du paquet d'onde")

ax1.legend()

# ------------------------
# Panneau inférieur
# ------------------------

line_bm, = ax2.plot(
    [],
    [],
    color='darkgreen',
    lw=2
)

ax2.axvline(
    x_bf,
    color='orange',
    linestyle='--',
    label='Position caractéristique'
)

ax2.set_xlim(-15, 15)
ax2.set_ylim(-2.5, 2.5)

ax2.set_xlabel("Position cochléaire")
ax2.set_ylabel("Déplacement")

ax2.set_title(
    "Réponse localisée de type membrane basilaire"
)

ax2.legend()

# ==================================================
# ANIMATION
# ==================================================

def update(frame):

    t = frame * 0.08

    psi1 = packet(x, t, x01)
    psi2 = packet(x, t, x02)

    refl = (
        reflected_packet(x, t, x01)
        + reflected_packet(x, t, x02)
    )

    total = psi1 + psi2 + refl

    # ------------------------------------------------
    # Réponse cochléaire simplifiée
    # ------------------------------------------------

    membrane = gain_bf * total

    line_total.set_data(x, total)
    line_bm.set_data(x, membrane)

    ax1.set_title(
        f"Propagation du paquet - t = {t:.2f}"
    )

    return line_total, line_bm

ani = FuncAnimation(
    fig,
    update,
    frames=500,
    interval=30,
    blit=True
)


# CODE POUR SAUVEGARDER DES GIF PAS TROP VOLUMINEUX
# METTRE LE dpi = 60 DANS LES FIGURES
# from matplotlib.animation import PillowWriter

# ani = FuncAnimation(fig, update, frames=800, interval=20)

# ani.save("Cochlée_Paquet_Ondes.gif",
#           writer=PillowWriter(fps=20)) #, save_count=80)

plt.tight_layout()
plt.show()

# ani.save('Vibration_Mecanique_Membrane_Cochlee_4.gif', writer='pillow', fps=30)

Etrier Membrane cochlée

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

# --- Paramètres ---
L_membrane = 0.5
L_water = 20.0
c_water = 148.2

A = 0.5
freq = 2

Nx = 200
Nt = 500

dx = L_water / Nx
dt = 0.02

# --- grille eau ---
x_water = np.linspace(L_membrane, L_membrane + L_water, Nx)

# --- état bille ---
ball_x = 0.2
ball_speed = 0.0
ball_v = 0.02

# --- clavier ---
keys = {"left": False, "right": False}

def on_key_press(event):
    if event.key == "left":
        keys["left"] = True
    if event.key == "right":
        keys["right"] = True

def on_key_release(event):
    if event.key == "left":
        keys["left"] = False
    if event.key == "right":
        keys["right"] = False

# --- onde eau (simple propagation) ---
def water_wave(x, t):
    return A * np.sin(2 * np.pi * freq * (t - (x - L_membrane) / c_water))

# --- figure ---
fig, ax = plt.subplots(figsize=(10, 6))

line_water, = ax.plot([], [], lw=2, color='green', label="Eau")

# membrane verticale (ligne pointillée)
membrane_line = ax.axvline(L_membrane, color='k', linestyle='--')

# membrane "bleue" (petit segment déformé horizontalement)
membrane_top, = ax.plot([], [], lw=4, color='blue')

# bille
ball, = ax.plot([], [], 'ro', markersize=10)

ax.set_xlim(0, L_membrane + L_water)
ax.set_ylim(-1.5, 1.5)
ax.set_title("Membrane + onde + bille contrôlée")
ax.legend()

fig.canvas.mpl_connect("key_press_event", on_key_press)
fig.canvas.mpl_connect("key_release_event", on_key_release)

# --- init ---
def init():
    line_water.set_data([], [])
    membrane_top.set_data([], [])
    ball.set_data([], [])
    return line_water, membrane_top, ball

# --- update ---
def update(frame):
    global ball_x, ball_speed

    t = frame * dt

    # -----------------------
    # onde eau
    # -----------------------
    y_water = water_wave(x_water, t)
    line_water.set_data(x_water, y_water)

    # -----------------------
    # membrane (ligne verticale + petite déformation horizontale)
    # -----------------------
    y_mem = np.linspace(-1, 1, 50)
    x_mem = L_membrane + 0.05 * np.sin(2*np.pi*freq*t)  # vibration horizontale

    membrane_top.set_data(
        [x_mem]*len(y_mem),
        y_mem
    )

    # -----------------------
    # bille (mouvement horizontal)
    # -----------------------
    if keys["left"]:
        ball_x -= ball_v
    if keys["right"]:
        ball_x += ball_v

    ball_x = np.clip(ball_x, 0, L_membrane + L_water)

    ball.set_data([ball_x], [0])

    return line_water, membrane_top, ball

ani = FuncAnimation(fig, update, frames=Nt, init_func=init, blit=True, interval=20)

plt.show()

Potentiel d’Action

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.widgets import Slider
from matplotlib.animation import PillowWriter

# -------------------------
# Paramètres
# -------------------------
L = 30
Nx = 500
x = np.linspace(0, L, Nx)

v = 10.0
tau_spike = 0.2
dt = 0.05

lambda_rate = 3.0
tau_ref = 0.08  # réfractaire

# -------------------------
# état
# -------------------------
spikes = []
last_spike_time = -1e9

def poisson_event(lam, dt):
    return np.random.rand() < lam * dt

def spike_shape(x, t, t0):
    return np.exp(-((x - v*(t - t0))**2) / (2*tau_spike**2))

# -------------------------
# figure
# -------------------------
fig, ax = plt.subplots(dpi = 60)
plt.subplots_adjust(bottom=0.25)

line, = ax.plot(x, np.zeros_like(x))

ax.set_ylim(0, 1.5)
ax.set_xlim(0, L)
ax.set_title("Neurone avec période réfractaire")
ax.set_xlabel("temps(ms)")
ax.set_ylabel("Potentiel membranaire (mV)")

# -------------------------
# SLIDER λ
# -------------------------
ax_lambda = plt.axes([0.25, 0.1, 0.5, 0.03])
slider_lambda = Slider(ax_lambda, "λ (fréquence)", 0.1, 20.0, valinit=lambda_rate)

def update_lambda(val):
    global lambda_rate
    lambda_rate = slider_lambda.val

slider_lambda.on_changed(update_lambda)

# -------------------------
# update animation
# -------------------------
def update(frame):
    global spikes, last_spike_time

    t = frame * dt

    # -------------------------
    # réfractaire
    # -------------------------
    can_fire = (t - last_spike_time) > tau_ref

    if can_fire and poisson_event(lambda_rate, dt):
        spikes.append(t)
        last_spike_time = t

    # -------------------------
    # champ
    # -------------------------
    y = np.zeros_like(x)

    new_spikes = []
    for t0 in spikes:
        if t - t0 < L / v:
            y += spike_shape(x, t, t0)
            new_spikes.append(t0)

    spikes = new_spikes

    line.set_ydata(y)
    return line,

ani = FuncAnimation(fig, update, frames=2000, interval=20, blit=True)



# CODE POUR SAUVEGARDER DES GIF PAS TROP VOLUMINEUX
# METTRE LE dpi = 60 DANS LES FIGURES
# from matplotlib.animation import PillowWriter
# ani = FuncAnimation(fig, update, frames=500, interval=20)

# ani.save("Potentiel_d_Action.gif",
#          writer=PillowWriter(fps=20)) #, save_count=80)

plt.show()

Potentiel d Action sans periode réfractaire

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.widgets import Slider

# -------------------------
# Paramètres physiques
# -------------------------
L = 10.0              # longueur axone
Nx = 500
x = np.linspace(0, L, Nx)

v = 2.0               # vitesse propagation spike
tau_spike = 0.2      # largeur du potentiel d'action

dt = 0.05

# -------------------------
# Etat des spikes
# -------------------------
spikes = []  # chaque spike = (t0)

# -------------------------
# lambda initial (Poisson)
# -------------------------
lambda_rate = 2.0

# -------------------------
# fonction Poisson
# -------------------------
def poisson_event(lam, dt):
    return np.random.rand() < lam * dt

# -------------------------
# potentiel d'action
# -------------------------
def spike_shape(x, t, t0):
    return np.exp(-((x - v*(t - t0))**2) / (2*tau_spike**2))

# -------------------------
# figure
# -------------------------
fig, ax = plt.subplots()
plt.subplots_adjust(bottom=0.25)

line, = ax.plot(x, np.zeros_like(x), lw=2)

ax.set_ylim(0, 1.5)
ax.set_xlim(0, L)
ax.set_title("Train de potentiels d'action (processus de Poisson)")

# -------------------------
# Slider λ
# -------------------------
ax_lambda = plt.axes([0.25, 0.1, 0.5, 0.03])
slider_lambda = Slider(ax_lambda, 'λ (fréquence spikes)', 0.1, 20.0, valinit=lambda_rate)

def update_lambda(val):
    global lambda_rate
    lambda_rate = slider_lambda.val

slider_lambda.on_changed(update_lambda)

# -------------------------
# update animation
# -------------------------
def update(frame):
    global spikes

    t = frame * dt

    # -------------------------
    # génération Poisson
    # -------------------------
    if poisson_event(lambda_rate, dt):
        spikes.append(t)

    # -------------------------
    # champ total
    # -------------------------
    y = np.zeros_like(x)

    new_spikes = []
    for t0 in spikes:
        # ne garder que les spikes visibles
        if t - t0 < L / v:
            y += spike_shape(x, t, t0)
            new_spikes.append(t0)

    spikes = new_spikes

    line.set_ydata(y)

    return line,

ani = FuncAnimation(fig, update, frames=2000, interval=20, blit=True)

plt.show()

Comparaison Euler RK4

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from scipy.signal import find_peaks
from scipy import stats
from matplotlib.animation import PillowWriter

# ==========================================================
# PARAMETRES
# ==========================================================
g = 9.81
L = 1.0

dt = 0.01
T = 25
N = int(T / dt)

t = np.linspace(0, T, N)

# ==========================================================
# CONDITIONS INITIALES
# ==========================================================
theta_rk4 = np.zeros(N)
theta_rk2 = np.zeros(N)
theta_euler = np.zeros(N)

omega_rk4 = np.zeros(N)
omega_rk2 = np.zeros(N)
omega_euler = np.zeros(N)

theta_rk4[0] = np.pi/4
theta_rk2[0] = np.pi/4
theta_euler[0] = np.pi/4

# ==========================================================
# SYSTEME
# ==========================================================
def f(theta, omega):
    return omega, -(g/L)*np.sin(theta)

# ==========================================================
# RK4
# ==========================================================
for i in range(N-1):

    th, om = theta_rk4[i], omega_rk4[i]

    k1_t, k1_w = f(th, om)
    k2_t, k2_w = f(th+0.5*dt*k1_t, om+0.5*dt*k1_w)
    k3_t, k3_w = f(th+0.5*dt*k2_t, om+0.5*dt*k2_w)
    k4_t, k4_w = f(th+dt*k3_t, om+dt*k3_w)

    theta_rk4[i+1] = th + dt/6*(k1_t+2*k2_t+2*k3_t+k4_t)
    omega_rk4[i+1] = om + dt/6*(k1_w+2*k2_w+2*k3_w+k4_w)

# ==========================================================
# RK2
# ==========================================================
for i in range(N-1):

    th, om = theta_rk2[i], omega_rk2[i]

    k1_t, k1_w = f(th, om)
    k2_t, k2_w = f(th+0.5*dt*k1_t, om+0.5*dt*k1_w)

    theta_rk2[i+1] = th + dt*k2_t
    omega_rk2[i+1] = om + dt*k2_w

# ==========================================================
# EULER
# ==========================================================
for i in range(N-1):

    dth, dom = f(theta_euler[i], omega_euler[i])

    theta_euler[i+1] = theta_euler[i] + dt*dth
    omega_euler[i+1] = omega_euler[i] + dt*dom

# ==========================================================
# PEAKS (post-process)
# ==========================================================
idx_rk2, _ = find_peaks(theta_rk2, distance=100)
idx_eu, _ = find_peaks(theta_euler, distance=100)

peaks_rk2 = theta_rk2[idx_rk2]
peaks_eu = theta_euler[idx_eu]

times_rk2 = t[idx_rk2]
times_eu = t[idx_eu]

# ==========================================================
# Z SCORE
# ==========================================================
def z(x):
    if len(x) < 2:
        return np.zeros(len(x))
    return (x - np.mean(x)) / np.std(x)

z_rk2 = z(peaks_rk2)
z_eu = z(peaks_eu)

# ==========================================================
# FIGURE
# ==========================================================
fig = plt.figure(figsize=(10,6), dpi = 60)
gs = fig.add_gridspec(2,2)

ax_p = fig.add_subplot(gs[:,0])
ax_t = fig.add_subplot(gs[:,1])
# ax_z = fig.add_subplot(gs[1,1])

# pendule
line_rk4, = ax_p.plot([], [], lw=2, label="RK4")
line_rk2, = ax_p.plot([], [], "--", label="RK2")
line_eu, = ax_p.plot([], [], ":", label="Euler")

mass_rk4, = ax_p.plot([], [], "o")
mass_rk2, = ax_p.plot([], [], "o")
mass_eu, = ax_p.plot([], [], "o")

ax_p.set_xlim(-1.2,1.2)
ax_p.set_ylim(-1.2,1.2)
ax_p.set_aspect("equal")
ax_p.grid()
ax_p.legend()
ax_p.set_title("Pendule")

# theta
line_t_rk4, = ax_t.plot([], [], label="RK4")
line_t_rk2, = ax_t.plot([], [], label="RK2")
line_t_eu, = ax_t.plot([], [], label="Euler")

ax_t.set_xlim(0,T)
ax_t.set_ylim(-2,2)
ax_t.grid()
ax_t.legend()
ax_t.set_title("Angle")

# z
# line_z_rk2, = ax_z.plot([], [], "o-", label="RK2")
# line_z_eu, = ax_z.plot([], [], "o-", label="Euler")

# ax_z.grid()
# ax_z.legend()
# ax_z.set_title("Z-score")

# ==========================================================
# VIDEO WRITER
# ==========================================================
Writer = animation.FFMpegWriter
writer = Writer(fps=30, metadata=dict(artist='ChatGPT'), bitrate=1800)

# ==========================================================
# UPDATE
# ==========================================================
def update(i):

    # pendule coords
    xrk4 = L*np.sin(theta_rk4[i])
    yrk4 = -L*np.cos(theta_rk4[i])

    xrk2 = L*np.sin(theta_rk2[i])
    yrk2 = -L*np.cos(theta_rk2[i])

    xeu = L*np.sin(theta_euler[i])
    yeu = -L*np.cos(theta_euler[i])

    line_rk4.set_data([0,xrk4],[0,yrk4])
    line_rk2.set_data([0,xrk2],[0,yrk2])
    line_eu.set_data([0,xeu],[0,yeu])

    mass_rk4.set_data([xrk4],[yrk4])
    mass_rk2.set_data([xrk2],[yrk2])
    mass_eu.set_data([xeu],[yeu])

    # theta curves
    line_t_rk4.set_data(t[:i], theta_rk4[:i])
    line_t_rk2.set_data(t[:i], theta_rk2[:i])
    line_t_eu.set_data(t[:i], theta_euler[:i])

##    # z-score progressive
##    nrk2 = np.sum(times_rk2 <= t[i])
##    neu = np.sum(times_eu <= t[i])
##
##    if nrk2 > 1:
##        line_z_rk2.set_data(range(nrk2), z_rk2[:nrk2])
##
##    if neu > 1:
##        line_z_eu.set_data(range(neu), z_eu[:neu])
##
##    return (line_rk4,line_rk2,line_eu,
##            mass_rk4,mass_rk2,mass_eu,
##            line_t_rk4,line_t_rk2,line_t_eu,
##            line_z_rk2,line_z_eu)

# ==========================================================
# SAVE ANIMATION
# ==========================================================
ani = animation.FuncAnimation(fig, update, frames=N, interval=20)
# CODE POUR SAUVEGARDER DES GIF PAS TROP VOLUMINEUX
# METTRE LE dpi = 60 DANS LES FIGURES
# from matplotlib.animation import PillowWriter

# ani = FuncAnimation(fig, update, frames=800, interval=20)

# ani.save("Pendule_Euler_RK4.gif",
#            writer=PillowWriter(fps=20)) #, save_count=80)



# ani.save("pendule_erreur.mp4", writer=writer)

plt.show()

Comparaison Euler RK4 (avec z-score)

import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
from scipy.signal import find_peaks


# ==========================================================
# PARAMETRES PHYSIQUES
# ==========================================================
g = 9.81
L = 1.0

# ==========================================================
# PARAMETRES NUMERIQUES
# ==========================================================
dt = 0.01
T = 35
N = int(T / dt)

# ==========================================================
# CONDITIONS INITIALES
# ==========================================================
theta0 = np.pi / 4
omega0 = 0.0

# ==========================================================
# TABLEAUX
# ==========================================================
t = np.zeros(N)

theta_rk4 = np.zeros(N)
omega_rk4 = np.zeros(N)

theta_rk2 = np.zeros(N)
omega_rk2 = np.zeros(N)

theta_euler = np.zeros(N)
omega_euler = np.zeros(N)

# Initialisation
theta_rk4[0] = theta0
omega_rk4[0] = omega0

theta_rk2[0] = theta0
omega_rk2[0] = omega0

theta_euler[0] = theta0
omega_euler[0] = omega0

# ==========================================================
# SYSTEME DIFFERENTIEL
# ==========================================================
def f(theta, omega):
    dtheta = omega
    domega = -(g/L) * np.sin(theta)
    return dtheta, domega

# ==========================================================
# SIMULATION RK4 (REFERENCE)
# ==========================================================
for n in range(N - 1):

    theta = theta_rk4[n]
    omega = omega_rk4[n]

    k1_t, k1_w = f(theta, omega)
    k2_t, k2_w = f(theta + 0.5 * dt * k1_t,
                   omega + 0.5 * dt * k1_w)

    k3_t, k3_w = f(theta + 0.5 * dt * k2_t,
                   omega + 0.5 * dt * k2_w)

    k4_t, k4_w = f(theta + dt * k3_t,
                   omega + dt * k3_w)

    theta_rk4[n+1] = theta + (dt/6)*(k1_t + 2*k2_t + 2*k3_t + k4_t)
    omega_rk4[n+1] = omega + (dt/6)*(k1_w + 2*k2_w + 2*k3_w + k4_w)

    t[n+1] = t[n] + dt

# ==========================================================
# SIMULATION RK2
# ==========================================================
for n in range(N - 1):

    theta = theta_rk2[n]
    omega = omega_rk2[n]

    k1_t, k1_w = f(theta, omega)

    k2_t, k2_w = f(theta + 0.5 * dt * k1_t,
                   omega + 0.5 * dt * k1_w)

    theta_rk2[n+1] = theta + dt * k2_t
    omega_rk2[n+1] = omega + dt * k2_w

# ==========================================================
# SIMULATION EULER
# ==========================================================
for n in range(N - 1):

    theta = theta_euler[n]
    omega = omega_euler[n]

    dtheta, domega = f(theta, omega)

    theta_euler[n+1] = theta + dt * dtheta
    omega_euler[n+1] = omega + dt * domega

# ==========================================================
# DETECTION DES MAXIMA
# ==========================================================
##def find_peaks(theta, t,
##               min_peak_height=0.1,
##               min_distance=100):
##
##    peaks_theta = []
##    peaks_time = []
##    peaks_index = []
##
##    for i in range(1, len(theta)-1):
##
##        if (theta[i] > theta[i-1]
##            and theta[i] > theta[i+1]
##            and theta[i] > min_peak_height
##            and (len(peaks_index) == 0
##                 or i - peaks_index[-1] > min_distance)):
##
##            peaks_theta.append(theta[i])
##            peaks_time.append(t[i])
##            peaks_index.append(i)
##
##    return peaks_theta, peaks_time
##
##
### Maxima RK2
##peaks_rk2, times_rk2 = find_peaks(theta_rk2, t)
##
### Maxima Euler
##peaks_euler, times_euler = find_peaks(theta_euler, t)


peaks_idx, _ = find_peaks(theta_euler,
                          height=0.1,
                          distance=100)

# ==========================================================
# DETECTION DES MAXIMA AVEC SCIPY
# ==========================================================

# RK2
idx_rk2, _ = find_peaks(theta_rk2,
                        height=0.1,
                        distance=100)

peaks_rk2 = theta_rk2[idx_rk2]
times_rk2 = t[idx_rk2]

# Euler
idx_euler, _ = find_peaks(theta_euler,
                          height=0.1,
                          distance=100)

peaks_euler = theta_euler[idx_euler]
times_euler = t[idx_euler]


# ==========================================================
# CALCUL DES Z-SCORES
# ==========================================================
zscore_rk2 = stats.zscore(peaks_rk2)
zscore_euler = stats.zscore(peaks_euler)

# ==========================================================
# FIGURE : 2 SUBPLOTS
# ==========================================================
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(7, 6))

# ==========================================================
# SUBPLOT 1 : SIMULATIONS + BARRES θmax
# ==========================================================
ax1.plot(t, theta_rk4,
         label='RK4 (référence)',
         linewidth=2)

ax1.plot(t, theta_rk2,
         label='RK2',
         linestyle='--',
         linewidth=2)

ax1.plot(t, theta_euler,
         label='Euler',
         linestyle=':',
         linewidth=2)

# Barres verticales RK2
for time_peak, peak in zip(times_rk2, peaks_rk2):
    ax1.vlines(time_peak,
               0,
               peak,
               color='blue',
               alpha=0.4)

# Barres verticales Euler
for time_peak, peak in zip(times_euler, peaks_euler):
    ax1.vlines(time_peak,
               0,
               peak,
               color='orange',
               alpha=0.4)

# Points maxima
ax1.plot(times_rk2, peaks_rk2,
         marker = 'o', markersize=5)

ax1.plot(times_euler, peaks_euler,
         marker = 'o', color='orange',
         markersize=5)

ax1.set_xlabel('Temps (s)')
ax1.set_ylabel('θ (rad)')
ax1.set_title('Simulation temporelle et θ_max')
ax1.legend()
ax1.grid(True)

# ==========================================================
# SUBPLOT 2 : Z-SCORES
# ==========================================================
ax2.plot(zscore_rk2,
         'o-',
         label='RK2',
         color='blue',
         markersize=5)

ax2.plot(zscore_euler,
         'o-',
         label='Euler',
         color='orange',
         markersize=5)

ax2.axhline(0,
            color='gray',
            linestyle='--',
            linewidth=1)

ax2.set_xlabel('Nombre de périodes')
ax2.set_ylabel('Z-score')
ax2.set_title('Z-score des θ_max')
ax2.legend()
ax2.grid(True)

plt.tight_layout()
plt.rcParams.update({'font.size': 12, 'axes.titlesize': 7, 'legend.fontsize': 6})
plt.savefig("Comparaison_Euler_RK2.svg", format="svg", bbox_inches='tight')
plt.savefig("Comparaison_Euler_RK2.eps", format="eps", bbox_inches='tight')
plt.show()

Regression Lineaire avec et sans incertitude

import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import minimize

# ============================
# A. DÉFINITION DU MODÈLE
# ============================

def modele_lineaire(x, params):
    """Modèle linéaire : y = a x + b"""
    a, b = params
    return a * x + b

# ============================
# B. GÉNÉRATION DES DONNÉES
# ============================

rng = np.random.default_rng(0)

# vrais paramètres
a_vrai = 2.0
b_vrai = 5.0

x_obs = np.linspace(0, 10, 30)
y_theo = modele_lineaire(x_obs, [a_vrai, b_vrai])

# incertitudes expérimentales (hétérogènes pour montrer l'effet)
sigma = 0.5 + 0.3 * x_obs   # incertitudes croissantes

# données bruitées
y_obs = y_theo + rng.normal(0, sigma)

# ============================
# C. ESTIMATION DES PARAMÈTRES
# ============================

# --- 1) Régression linéaire simple (OLS) ---
def chi2_OLS(params):
    y_mod = modele_lineaire(x_obs, params)
    return np.sum((y_obs - y_mod)**2)

params_init = [1.0, 1.0]
res_OLS = minimize(chi2_OLS, params_init)
params_OLS = res_OLS.x
a_OLS, b_OLS = params_OLS

# --- 2) Régression linéaire pondérée (WLS / χ²) ---
def chi2_WLS(params):
    y_mod = modele_lineaire(x_obs, params)
    return np.sum(((y_obs - y_mod) / sigma)**2)

res_WLS = minimize(chi2_WLS, params_init)
params_WLS = res_WLS.x
a_WLS, b_WLS = params_WLS

print("=== Paramètres vrais ===")
print(f"a = {a_vrai:.3f}, b = {b_vrai:.3f}\n")

print("=== Régression OLS (non pondérée) ===")
print(f"a = {a_OLS:.3f}, b = {b_OLS:.3f}")

print("\n=== Régression WLS (pondérée par 1/sigma^2) ===")
print(f"a = {a_WLS:.3f}, b = {b_WLS:.3f}")

# ============================
# D. VISUALISATION
# ============================

x_fine = np.linspace(0, 10, 300)

plt.figure(figsize=(8,6))

# Données expérimentales
plt.errorbar(x_obs, y_obs, yerr=sigma, fmt='o', label="Données expérimentales")

# Modèle vrai
plt.plot(x_fine, modele_lineaire(x_fine, [a_vrai, b_vrai]),
         'k--', label="Modèle vrai")

# Fit OLS
plt.plot(x_fine, modele_lineaire(x_fine, params_OLS),
         'b-', label="Régression OLS (non pondérée)")

# Fit WLS
plt.plot(x_fine, modele_lineaire(x_fine, params_WLS),
         'r-', label="Régression WLS (pondérée)")

plt.xlabel("x")
plt.ylabel("y")
plt.title("Régression linéaire : OLS vs WLS (avec incertitudes)")
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.show()

Modele decroissance exponentielle

import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import minimize

# ============================
# A. DÉFINITION DU MODÈLE
# ============================

def modele_exponentiel(t, params):
    """
    Modèle physique : décroissance exponentielle
    N(t) = N0 * exp(-t / tau)
    params = [N0, tau]
    """
    N0, tau = params
    return N0 * np.exp(-t / tau)

# ============================
# B. GÉNÉRATION / CHARGEMENT DES DONNÉES
# ============================

# Ici on simule des données expérimentales pour l'exemple
rng = np.random.default_rng(0)

# "vrais" paramètres physiques
N0_vrai = 100.0
tau_vrai = 2.5

t_obs = np.linspace(0, 10, 30)  # temps mesurés
N_theo = modele_exponentiel(t_obs, [N0_vrai, tau_vrai])

# incertitudes expérimentales (par ex. bruit de comptage)
sigma = 5.0 * np.ones_like(t_obs)

# données observées = théorie + bruit gaussien
N_obs = N_theo + rng.normal(0, sigma)

# ============================
# C. ESTIMATION DES PARAMÈTRES
# ============================

def chi2(params):
    N_mod = modele_exponentiel(t_obs, params)
    return np.sum(((N_obs - N_mod) / sigma)**2)

# point de départ pour l'optimisation
params_init = [80.0, 1.0]

result = minimize(chi2, params_init)
params_fit = result.x
N0_fit, tau_fit = params_fit

print("Paramètres vrais :   N0 = {:.2f}, tau = {:.2f}".format(N0_vrai, tau_vrai))
print("Paramètres ajustés : N0 = {:.2f}, tau = {:.2f}".format(N0_fit, tau_fit))

# ============================
# D. VALIDATION / DIAGNOSTICS
# ============================

N_mod_fit = modele_exponentiel(t_obs, params_fit)
residus = N_obs - N_mod_fit
z_scores = residus / sigma

chi2_val = chi2(params_fit)
ddl = len(t_obs) - len(params_fit)  # degrés de liberté
chi2_reduit = chi2_val / ddl

print("χ² = {:.2f}, ddl = {}, χ² réduit = {:.2f}".format(chi2_val, ddl, chi2_reduit))

# ============================
# E. VISUALISATION
# ============================

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(7,8), sharex=True)

# Données + modèle
ax1.errorbar(t_obs, N_obs, yerr=sigma, fmt='o', label="Données expérimentales")
t_fine = np.linspace(0, 10, 300)
# ax1.plot(t_fine, modele_exponentiel(t_fine, [N0_vrai, tau_vrai]),'k--', label="Modèle vrai")
ax1.plot(t_fine, modele_exponentiel(t_fine, params_fit), 'r-', label="Modèle ajusté")
ax1.set_ylabel("N(t)")
ax1.set_title("Décroissance exponentielle : données vs modèle")
ax1.legend()
ax1.grid(True)

# Résidus normalisés (z-scores)
ax2.axhline(0, color='k', linewidth=1)
# ax2.errorbar(t_obs, z_scores, yerr=np.ones_like(z_scores), fmt='o', label="z-scores")
ax2.errorbar(t_obs, z_scores, fmt='o', label="z-scores")
ax2.set_xlabel("Temps t")
ax2.set_ylabel("z-score")
ax2.set_title("Résidus normalisés (z-scores)")
ax2.grid(True)

plt.tight_layout()
plt.show()

Battements 435Hz – 440 Hz

import numpy as np
import matplotlib.pyplot as plt
from scipy import signal

# Paramètres du signal
frequence1 = 435  # Hz
frequence2 = 440  # Hz
amplitude = 1
duree = 10    # secondes (20 ms)
fe = 44100       # fréquence d'échantillonnage (Hz)
tau = 0.3

# Axe temporel
t = np.linspace(0, duree, int(fe * duree), endpoint=False)

# Génération du signal sinusoïdal

audio_435Hz = amplitude * np.sin(2 * np.pi * frequence1 * t)
audio_440Hz = amplitude * np.sin(2 * np.pi * frequence2 * t)
audio_Battements = audio_435Hz + audio_440Hz
audio_Battements_Decay = audio_Battements * np.exp(-t/tau)


# FFT

N = len(audio_Battements)

fft_values = np.fft.fft(audio_Battements)
fft_freqs = np.fft.fftfreq(N, 1 / fe)
# On garde uniquement les fréquences positives
mask = fft_freqs >= 0
fft_freqs = fft_freqs[mask]
fft_values = fft_values[mask]
# Amplitude (normalisée)
amplitude_spectre = np.abs(fft_values) / N


# Génération du spectrogramme

frequences, temps, Sxx = signal.spectrogram(
    audio_Battements,
    fe,
    nperseg=4096
)



# Affichage
fig, axes = plt.subplots(2, 1, figsize=(15, 10))


##plt.title("Signal sinusoïdal de 435 Hz")
##plt.xlabel("Temps (s)")
##plt.ylabel("Amplitude")
##plt.grid(True)
##
##plt.tight_layout()
##plt.show()

# LA FONCTION title, ... S'APPLIQUE A L'OBJET plt. SI L'oN VEUT UTILISER LES OBJETS GENERES PAR LE SUBPLOT (ax[], ...) IL FAUT UTILISER LES FONCTIONS set_title(), ...


axes[0].plot(t[:25000], audio_Battements[:25000], color='green', Label = 'Sans attenuation')
axes[0].plot(t[:25000], audio_Battements_Decay[:25000], color='blue', Label = 'Avec attenuation')
axes[0].set_title("Battements (435 Hz - 440 Hz)")
axes[0].set_xlabel("Temps (s)")
axes[0].set_ylabel("Amplitude")
axes[0].legend(loc="upper left")
axes[0].grid(True)


# axes[1].plot(fft_freqs, amplitude_spectre, color='red')
axes[1].loglog(fft_freqs,amplitude_spectre, color='red')
axes[1].set_title("Spectre de fréquence (FFT)")
axes[1].set_xlabel("Fréquence (Hz)")
axes[1].set_ylabel("Amplitude")
# axes[1].set_xlim(0, 1000)  # zoom utile
axes[1].grid(True)

plt.rcParams.update({'font.size': 12, 'axes.titlesize': 14, 'legend.fontsize': 10})
plt.savefig("Battements_435Hz_440Hz.svg", format="svg", bbox_inches='tight')
plt.savefig("Battements_435Hz_440Hz.eps", format="eps", bbox_inches='tight')
plt.tight_layout()







# Spectrogramme
fig, ax2 = plt.subplots()
ax2.set_title("Spectrogramme")
ax2.set_xlabel("Temps (s)")
ax2.set_ylabel("Fréquence (Hz)")
ax2.set_ylim(0, 2000)


spectre = ax2.pcolormesh( temps, frequences, 10 * np.log10(Sxx), shading='gouraud', cmap='viridis' )
# Barre de couleur
fig.colorbar(
    spectre,
    ax=ax2,
    label="Puissance (dB)"
)
# Ajustement automatique
plt.tight_layout()
# Affichage
plt.show()

Tube de Kundt

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.animation import PillowWriter

# --- Paramètres ---
L = 10.0       # Longueur de la corde (m)
A = 1.0        # Amplitude (m)
f = 400        # Fréquence (Hz)
v = 340        # Vitesse de l'onde (m/s)
modes = [1, 2, 3, 4]  # Modes harmoniques à afficher

# --- Création de la figure ---
fig, axes = plt.subplots(len(modes), 1, figsize=(12, 8), sharex=True, dpi = 60)
if len(modes) == 1:
    axes = [axes]

# --- Grille spatiale et temporelle ---
x = np.linspace(0, L, 1000)
t = np.linspace(0, 2, 100)

# --- Liste pour stocker les animations ---
anis = []

# --- Animation pour chaque mode ---
for i, n in enumerate(modes):
    ax = axes[i]
    k = n * np.pi / L
    omega = 2 * np.pi * f * n

    # Tracer les nœuds et ventres
    nodes = np.linspace(0, L, n+1)
    for node in nodes:
        ax.axvline(x=node, color='red', linestyle='--', alpha=0.5, label='Nœud' if i==0 else "")
    antinodes = np.linspace(L/(2*n), L, n)
    for antinode in antinodes:
        ax.axvline(x=antinode, color='green', linestyle=':', alpha=0.5, label='Ventre' if i==0 else "")

    # Initialisation
    line, = ax.plot([], [], lw=2, color='blue')
    ax.set_ylim(-1.5*A, 1.5*A)
    ax.set_xlim(0, L)
    ax.set_ylabel(f'Mode {n}')
    ax.legend(loc='upper right')
    ax.grid(True)

    # Fonction d'initialisation
    def init():
        line.set_data([], [])
        return line,

    # Fonction d'animation (capture de n, k, omega, x, line)
    def update(frame, n=n, k=k, omega=omega, x=x, line=line):
        y = A * np.sin(k * x) * np.cos(omega * frame)
        line.set_data(x, y)
        return line,

    # Animation (blit=False pour éviter les conflits)
    ani = FuncAnimation(
        fig, update, frames=t,
        init_func=init, blit=False, interval=50, repeat=True
    )
    # anis.append(ani)  # Stocker l'animation

    # CODE POUR SAUVEGARDER DES GIF PAS TROP VOLUMINEUX
    # METTRE LE dpi = 60 DANS LES FIGURES
    # from matplotlib.animation import PillowWriter

    # ani = FuncAnimation(fig, update, frames=80, interval=20)

    # ani.save("Tube_Kundt.gif",
    #          writer=PillowWriter(fps=20)) #, save_count=80)

    anis.append(ani)  # Stocker l'animation


# --- Affichage ---
plt.suptitle("Ondes stationnaires : Nœuds (rouge) et Ventres (vert)", y=1.02)
plt.xlabel('Position (m)')
plt.tight_layout()
plt.show()

Explications Ondes Progressives Battements

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import tkinter as tk
from tkinter import ttk

# Paramètres par défaut
L = 50.0  # Longueur du tuyau
N = 1000
x = np.linspace(0, L, N)

# Fonction pour générer une onde plane progressive
def onde_plane_progressive(x, A, k, omega, c, phi, x0, t):
    return A * np.cos(k * (x - x0) - omega * t + phi)

def onde_plane_regressive(x, A, k, omega, c, phi, x0, t):
    return A * np.cos(-k * (x - L + x0) - omega * t - phi)

# Classe pour l'application Tkinter
class OndeApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Simulation d'ondes planes dans un tuyau")

        # Variables de contrôle
        self.A1 = tk.DoubleVar(value=1.0)
        self.A2 = tk.DoubleVar(value=1.0)
        self.k1 = tk.DoubleVar(value=2*np.pi*100/L)
        self.k2 = tk.DoubleVar(value=2*np.pi*100/L)
        self.omega1 = tk.DoubleVar(value=1.0)
        self.omega2 = tk.DoubleVar(value=1.0)
        self.c1 = tk.DoubleVar(value=1.0)
        self.c2 = tk.DoubleVar(value=1.0)
        self.phi1 = tk.DoubleVar(value=0.0)
        self.phi2 = tk.DoubleVar(value=0.0)
        self.x01 = tk.DoubleVar(value=0.0)
        self.x02 = tk.DoubleVar(value=0.0)
        self.L = tk.DoubleVar(value=L)

        # Création de l'interface
        self.create_widgets()
        self.create_plot()

    def create_widgets(self):
        # Frame pour les contrôles
        control_frame = ttk.LabelFrame(self.root, text="Paramètres des ondes")
        control_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=False, padx=5, pady=5)

        # Paramètres Onde 1
        ttk.Label(control_frame, text="Onde 1").grid(row=0, column=0, columnspan=2, sticky=tk.W, pady=2)
        ttk.Label(control_frame, text="Amplitude:").grid(row=1, column=0, sticky=tk.W)
        ttk.Scale(control_frame, from_=0.1, to=5.0, variable=self.A1, orient=tk.HORIZONTAL).grid(row=1, column=1, sticky=tk.EW)
        ttk.Label(control_frame, text="Vecteur d'onde (k):").grid(row=2, column=0, sticky=tk.W)
        ttk.Scale(control_frame, from_=0.1, to=20.0, variable=self.k1, orient=tk.HORIZONTAL).grid(row=2, column=1, sticky=tk.EW)
        ttk.Label(control_frame, text="Pulsation (ω):").grid(row=3, column=0, sticky=tk.W)
        ttk.Scale(control_frame, from_=0.1, to=10.0, variable=self.omega1, orient=tk.HORIZONTAL).grid(row=3, column=1, sticky=tk.EW)
        ttk.Label(control_frame, text="Célérité:").grid(row=4, column=0, sticky=tk.W)
        ttk.Scale(control_frame, from_=0.1, to=5.0, variable=self.c1, orient=tk.HORIZONTAL).grid(row=4, column=1, sticky=tk.EW)
        ttk.Label(control_frame, text="Déphasage:").grid(row=5, column=0, sticky=tk.W)
        ttk.Scale(control_frame, from_=0, to=2*np.pi, variable=self.phi1, orient=tk.HORIZONTAL).grid(row=5, column=1, sticky=tk.EW)
        ttk.Label(control_frame, text="Position départ:").grid(row=6, column=0, sticky=tk.W)
        ttk.Scale(control_frame, from_=0, to=self.L.get(), variable=self.x01, orient=tk.HORIZONTAL).grid(row=6, column=1, sticky=tk.EW)

        # Paramètres Onde 2
        ttk.Label(control_frame, text="Onde 2").grid(row=7, column=0, columnspan=2, sticky=tk.W, pady=2)
        ttk.Label(control_frame, text="Amplitude:").grid(row=8, column=0, sticky=tk.W)
        ttk.Scale(control_frame, from_=0.1, to=5.0, variable=self.A2, orient=tk.HORIZONTAL).grid(row=8, column=1, sticky=tk.EW)
        ttk.Label(control_frame, text="Vecteur d'onde (k):").grid(row=9, column=0, sticky=tk.W)
        ttk.Scale(control_frame, from_=0.1, to=20.0, variable=self.k2, orient=tk.HORIZONTAL).grid(row=9, column=1, sticky=tk.EW)
        ttk.Label(control_frame, text="Pulsation (ω):").grid(row=10, column=0, sticky=tk.W)
        ttk.Scale(control_frame, from_=0.1, to=10.0, variable=self.omega2, orient=tk.HORIZONTAL).grid(row=10, column=1, sticky=tk.EW)
        ttk.Label(control_frame, text="Célérité:").grid(row=11, column=0, sticky=tk.W)
        ttk.Scale(control_frame, from_=0.1, to=5.0, variable=self.c2, orient=tk.HORIZONTAL).grid(row=11, column=1, sticky=tk.EW)
        ttk.Label(control_frame, text="Déphasage:").grid(row=12, column=0, sticky=tk.W)
        ttk.Scale(control_frame, from_=0, to=2*np.pi, variable=self.phi2, orient=tk.HORIZONTAL).grid(row=12, column=1, sticky=tk.EW)
        ttk.Label(control_frame, text="Position départ:").grid(row=13, column=0, sticky=tk.W)
        ttk.Scale(control_frame, from_=0, to=self.L.get(), variable=self.x02, orient=tk.HORIZONTAL).grid(row=13, column=1, sticky=tk.EW)

        # Paramètres du tuyau
        ttk.Label(control_frame, text="Longueur du tuyau:").grid(row=14, column=0, sticky=tk.W)
        ttk.Scale(control_frame, from_=10, to=100, variable=self.L, orient=tk.HORIZONTAL).grid(row=14, column=1, sticky=tk.EW)

        # Bouton pour redémarrer l'animation
        ttk.Button(control_frame, text="Redémarrer", command=self.restart_animation).grid(row=15, column=0, columnspan=2, pady=5)

    def create_plot(self):
        # Frame pour le graphique
        plot_frame = ttk.Frame(self.root)
        plot_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)

        # Figure Matplotlib
        self.fig, self.ax = plt.subplots(figsize=(15, 8))
        self.ax.set_xlim(0, self.L.get())
        self.ax.set_ylim(-5, 5)
        self.ax.set_xlabel("Position dans le tuyau (m)")
        self.ax.set_ylabel("Amplitude")
        self.ax.set_title("Propagation d'ondes planes dans un tuyau")
        self.ax.grid(True)

        # Canvas Tkinter
        self.canvas = FigureCanvasTkAgg(self.fig, master=plot_frame)
        self.canvas.draw()
        self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)

        # Animation
        self.line1, = self.ax.plot([], [], label='Onde 1', color='blue', alpha=0.7)
        self.line2, = self.ax.plot([], [], label='Onde 2', color='red', alpha=0.7)
        self.line_total, = self.ax.plot([], [], label='Somme des ondes', color='green', linewidth=2)
        self.ax.legend()

        # Dessiner le tuyau
        self.tuyau = plt.Rectangle((0, -5), self.L.get(), 10, linewidth=1, edgecolor='black', facecolor='none', linestyle='--')
        self.ax.add_patch(self.tuyau)

        # Lancer l'animation
        self.ani = FuncAnimation(self.fig, self.update, frames=np.linspace(0, 20, 200), interval=50, blit=False, repeat=True)

    def update(self, t):
        # Mettre à jour les données
        x = np.linspace(0, self.L.get(), 1000)
        y1 = onde_plane_progresive(x, self.A1.get(), self.k1.get(), self.omega1.get(), self.c1.get(), self.phi1.get(), self.x01.get(), t)
        y2 = onde_plane_regressive(x, self.A2.get(), self.k2.get(), self.omega2.get(), self.c2.get(), self.phi2.get(), self.x02.get(), t)
        y_total = y1 + y2

        # Mettre à jour les lignes
        self.line1.set_data(x, y1)
        self.line2.set_data(x, y2)
        self.line_total.set_data(x, y_total)

        # Mettre à jour les limites
        self.ax.set_xlim(0, self.L.get())
        self.ax.set_ylim(-max(self.A1.get(), self.A2.get())*2, max(self.A1.get(), self.A2.get())*2)
        self.tuyau.set_width(self.L.get())
        self.tuyau.set_height(max(self.A1.get(), self.A2.get())*4)
        self.tuyau.set_xy((0, -max(self.A1.get(), self.A2.get())*2))

        self.ax.set_title(f"Propagation d'ondes planes dans un tuyau (t = {t:.2f} s)")
        return self.line1, self.line2, self.line_total, self.tuyau

    def restart_animation(self):
        self.ani.event_source.stop()
        self.ani = FuncAnimation(self.fig, self.update, frames=np.linspace(0, 20, 200), interval=50, blit=False, repeat=True)
        self.canvas.draw()

# Lancer l'application
if __name__ == "__main__":
    root = tk.Tk()
    app = OndeApp(root)
    root.mainloop()

Explication Ondes Progressives Ondes Regressives Battements

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import tkinter as tk
from tkinter import ttk

# Paramètres par défaut
L = 50.0  # Longueur du tuyau
N = 1000
x = np.linspace(0, L, N)

# Fonction pour générer une onde plane progressive
def onde_plane_progressive(x, A, k, omega, c, phi, x0, t):
    return A * np.cos(k * (x - x0) - omega * t + phi)

def onde_plane_regressive(x, A, k, omega, c, phi, x0, t):
    return A * np.cos(-k * (x - L + x0) - omega * t - phi)

# Classe pour l'application Tkinter
class OndeApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Simulation d'ondes planes dans un tuyau")

        # Variables de contrôle
        self.A1 = tk.DoubleVar(value=1.0)
        self.A2 = tk.DoubleVar(value=1.0)
        self.k1 = tk.DoubleVar(value=2*np.pi*100/L)
        self.k2 = tk.DoubleVar(value=2*np.pi*100/L)
        self.omega1 = tk.DoubleVar(value=1.0)
        self.omega2 = tk.DoubleVar(value=1.0)
        self.c1 = tk.DoubleVar(value=1.0)
        self.c2 = tk.DoubleVar(value=1.0)
        self.phi1 = tk.DoubleVar(value=0.0)
        self.phi2 = tk.DoubleVar(value=0.0)
        self.x01 = tk.DoubleVar(value=0.0)
        self.x02 = tk.DoubleVar(value=0.0)
        self.L = tk.DoubleVar(value=L)

        # Création de l'interface
        self.create_widgets()
        self.create_plot()

    def create_widgets(self):
        # Frame pour les contrôles
        control_frame = ttk.LabelFrame(self.root, text="Paramètres des ondes")
        control_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=False, padx=5, pady=5)

        # Paramètres Onde 1
        ttk.Label(control_frame, text="Onde 1").grid(row=0, column=0, columnspan=2, sticky=tk.W, pady=2)
        ttk.Label(control_frame, text="Amplitude:").grid(row=1, column=0, sticky=tk.W)
        ttk.Scale(control_frame, from_=0.1, to=5.0, variable=self.A1, orient=tk.HORIZONTAL).grid(row=1, column=1, sticky=tk.EW)
        ttk.Label(control_frame, text="Vecteur d'onde (k):").grid(row=2, column=0, sticky=tk.W)
        ttk.Scale(control_frame, from_=0.1, to=20.0, variable=self.k1, orient=tk.HORIZONTAL).grid(row=2, column=1, sticky=tk.EW)
        ttk.Label(control_frame, text="Pulsation (ω):").grid(row=3, column=0, sticky=tk.W)
        ttk.Scale(control_frame, from_=0.1, to=10.0, variable=self.omega1, orient=tk.HORIZONTAL).grid(row=3, column=1, sticky=tk.EW)
        ttk.Label(control_frame, text="Célérité:").grid(row=4, column=0, sticky=tk.W)
        ttk.Scale(control_frame, from_=0.1, to=5.0, variable=self.c1, orient=tk.HORIZONTAL).grid(row=4, column=1, sticky=tk.EW)
        ttk.Label(control_frame, text="Déphasage:").grid(row=5, column=0, sticky=tk.W)
        ttk.Scale(control_frame, from_=0, to=2*np.pi, variable=self.phi1, orient=tk.HORIZONTAL).grid(row=5, column=1, sticky=tk.EW)
        ttk.Label(control_frame, text="Position départ:").grid(row=6, column=0, sticky=tk.W)
        ttk.Scale(control_frame, from_=0, to=self.L.get(), variable=self.x01, orient=tk.HORIZONTAL).grid(row=6, column=1, sticky=tk.EW)

        # Paramètres Onde 2
        ttk.Label(control_frame, text="Onde 2").grid(row=7, column=0, columnspan=2, sticky=tk.W, pady=2)
        ttk.Label(control_frame, text="Amplitude:").grid(row=8, column=0, sticky=tk.W)
        ttk.Scale(control_frame, from_=0.1, to=5.0, variable=self.A2, orient=tk.HORIZONTAL).grid(row=8, column=1, sticky=tk.EW)
        ttk.Label(control_frame, text="Vecteur d'onde (k):").grid(row=9, column=0, sticky=tk.W)
        ttk.Scale(control_frame, from_=0.1, to=20.0, variable=self.k2, orient=tk.HORIZONTAL).grid(row=9, column=1, sticky=tk.EW)
        ttk.Label(control_frame, text="Pulsation (ω):").grid(row=10, column=0, sticky=tk.W)
        ttk.Scale(control_frame, from_=0.1, to=10.0, variable=self.omega2, orient=tk.HORIZONTAL).grid(row=10, column=1, sticky=tk.EW)
        ttk.Label(control_frame, text="Célérité:").grid(row=11, column=0, sticky=tk.W)
        ttk.Scale(control_frame, from_=0.1, to=5.0, variable=self.c2, orient=tk.HORIZONTAL).grid(row=11, column=1, sticky=tk.EW)
        ttk.Label(control_frame, text="Déphasage:").grid(row=12, column=0, sticky=tk.W)
        ttk.Scale(control_frame, from_=0, to=2*np.pi, variable=self.phi2, orient=tk.HORIZONTAL).grid(row=12, column=1, sticky=tk.EW)
        ttk.Label(control_frame, text="Position départ:").grid(row=13, column=0, sticky=tk.W)
        ttk.Scale(control_frame, from_=0, to=self.L.get(), variable=self.x02, orient=tk.HORIZONTAL).grid(row=13, column=1, sticky=tk.EW)

        # Paramètres du tuyau
        ttk.Label(control_frame, text="Longueur du tuyau:").grid(row=14, column=0, sticky=tk.W)
        ttk.Scale(control_frame, from_=10, to=100, variable=self.L, orient=tk.HORIZONTAL).grid(row=14, column=1, sticky=tk.EW)

        # Bouton pour redémarrer l'animation
        ttk.Button(control_frame, text="Redémarrer", command=self.restart_animation).grid(row=15, column=0, columnspan=2, pady=5)

    def create_plot(self):
        # Frame pour le graphique
        plot_frame = ttk.Frame(self.root)
        plot_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)

        # Figure Matplotlib
        self.fig, self.ax = plt.subplots(figsize=(15, 8))
        self.ax.set_xlim(0, self.L.get())
        self.ax.set_ylim(-5, 5)
        self.ax.set_xlabel("Position dans le tuyau (m)")
        self.ax.set_ylabel("Amplitude")
        self.ax.set_title("Propagation d'ondes planes dans un tuyau")
        self.ax.grid(True)

        # Canvas Tkinter
        self.canvas = FigureCanvasTkAgg(self.fig, master=plot_frame)
        self.canvas.draw()
        self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)

        # Animation
        self.line1, = self.ax.plot([], [], label='Onde 1', color='blue', alpha=0.7)
        self.line2, = self.ax.plot([], [], label='Onde 2', color='red', alpha=0.7)
        self.line_total, = self.ax.plot([], [], label='Somme des ondes', color='green', linewidth=2)
        self.ax.legend()

        # Dessiner le tuyau
        self.tuyau = plt.Rectangle((0, -5), self.L.get(), 10, linewidth=1, edgecolor='black', facecolor='none', linestyle='--')
        self.ax.add_patch(self.tuyau)

        # Lancer l'animation
        self.ani = FuncAnimation(self.fig, self.update, frames=np.linspace(0, 20, 200), interval=50, blit=False, repeat=True)

    def update(self, t):
        # Mettre à jour les données
        x = np.linspace(0, self.L.get(), 1000)
        y1 = onde_plane_progressive(x, self.A1.get(), self.k1.get(), self.omega1.get(), self.c1.get(), self.phi1.get(), self.x01.get(), t)
        y2 = onde_plane_regressive(x, self.A2.get(), self.k2.get(), self.omega2.get(), self.c2.get(), self.phi2.get(), self.x02.get(), t)
        y_total = y1 + y2

        # Mettre à jour les lignes
        self.line1.set_data(x, y1)
        self.line2.set_data(x, y2)
        self.line_total.set_data(x, y_total)

        # Mettre à jour les limites
        self.ax.set_xlim(0, self.L.get())
        self.ax.set_ylim(-max(self.A1.get(), self.A2.get())*2, max(self.A1.get(), self.A2.get())*2)
        self.tuyau.set_width(self.L.get())
        self.tuyau.set_height(max(self.A1.get(), self.A2.get())*4)
        self.tuyau.set_xy((0, -max(self.A1.get(), self.A2.get())*2))

        self.ax.set_title(f"Propagation d'ondes planes dans un tuyau (t = {t:.2f} s)")
        return self.line1, self.line2, self.line_total, self.tuyau

    def restart_animation(self):
        self.ani.event_source.stop()
        self.ani = FuncAnimation(self.fig, self.update, frames=np.linspace(0, 20, 200), interval=50, blit=False, repeat=True)
        self.canvas.draw()

# Lancer l'application
if __name__ == "__main__":
    root = tk.Tk()
    app = OndeApp(root)
    root.mainloop()

Explication Ondes Stationnaires

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.widgets import Slider

# =====================================================
# Paramètres initiaux
# =====================================================

A0 = 1.0
f0 = 2.0
c0 = 2.0
L0 = 10.0
R0 = 1.0

# =====================================================
# Figure
# =====================================================

fig, ax = plt.subplots(figsize=(12,6))
plt.subplots_adjust(left=0.1, bottom=0.35)

x = np.linspace(0, 20, 2000)

incident_line, = ax.plot([], [], lw=2,
                         color='blue',
                         label='Onde incidente')

reflected_line, = ax.plot([], [], lw=2,
                          color='red',
                          label='Onde réfléchie')

total_line, = ax.plot([], [], lw=3,
                      color='black',
                      label='Somme')

ax.legend()
ax.grid(True)

ax.set_xlim(0,20)
ax.set_ylim(-3,3)

ax.set_xlabel("Position x")
ax.set_ylabel("Amplitude")

# =====================================================
# Sliders
# =====================================================

ax_f = plt.axes([0.15,0.25,0.7,0.03])
ax_c = plt.axes([0.15,0.20,0.7,0.03])
ax_L = plt.axes([0.15,0.15,0.7,0.03])
ax_A = plt.axes([0.15,0.10,0.7,0.03])
ax_R = plt.axes([0.15,0.05,0.7,0.03])

slider_f = Slider(ax_f,"Fréquence",0.1,10,valinit=f0)
slider_c = Slider(ax_c,"Célérité",0.2,20,valinit=c0)
slider_L = Slider(ax_L,"Longueur L",1,20,valinit=L0)
slider_A = Slider(ax_A,"Amplitude",0.1,3,valinit=A0)
slider_R = Slider(ax_R,"Réflexion R",-1,1,valinit=R0)

# =====================================================
# Animation
# =====================================================

def update(frame):

    t = frame*0.01

    f = slider_f.val
    c = slider_c.val
    L = slider_L.val
    A = slider_A.val
    R = slider_R.val

    omega = 2*np.pi*f
    k = omega/c

    # ---------------------------
    # Onde incidente
    # ---------------------------

    front_incident = c*t

    ui = np.zeros_like(x)

    mask_incident = x <= front_incident

    ui[mask_incident] = (
        A*np.cos(k*x[mask_incident] - omega*t)
    )

    # ---------------------------
    # Onde réfléchie
    # ---------------------------

    ur = np.zeros_like(x)

    if front_incident > L:

        tr = t - L/c

        reflected_front = L - c*tr

        mask_reflected = x >= reflected_front

        ur[mask_reflected] = (
            A*R*np.cos(
                k*(2*L - x[mask_reflected])
                - omega*t
            )
        )

    # ---------------------------
    # Somme
    # ---------------------------

    utot = ui + ur

    incident_line.set_data(x,ui)
    reflected_line.set_data(x,ur)
    total_line.set_data(x,utot)

    ax.set_xlim(0,max(20,L+2))

    ax.set_title(
        f"L={L:.1f}   "
        f"f={f:.2f} Hz   "
        f"c={c:.2f} m/s   "
        f"R={R:.2f}"
    )

    return incident_line, reflected_line, total_line

ani = FuncAnimation(
    fig,
    update,
    interval=30,
    blit=True
)

plt.show()

Illustration Cohérence Laser

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

# ============================================================
# PARAMÈTRES MODIFIABLES
# ============================================================

fs = 20000           # fréquence d'échantillonnage (Hz)
f_laser = 4000       # fréquence du laser (Hz)
mod_freq = 50        # modulation lente (Hz)
noise_level = 0.1    # bruit
N = 2048             # taille FFT

# Axe temporel centré entre -t_max et +t_max
t = np.arange(N) / fs
t_centered = np.fft.fftshift(t - t.mean())

# Axe fréquentiel centré
freqs = np.fft.fftshift(np.fft.fftfreq(N, 1/fs))

# ============================================================
# SIGNAL LUMINEUX (LASER MODULÉ)
# ============================================================

def generate_laser_signal(frame):
    signal = (1 + 0.5*np.sin(2*np.pi*mod_freq*t + 0.01*frame)) \
             * np.sin(2*np.pi*f_laser*t)
    signal += noise_level * np.random.randn(len(t))
    return signal

# ============================================================
# FFT + IFFT CENTRÉES
# ============================================================

def compute_fft(signal):
    spectrum = np.fft.fft(signal)
    return np.fft.fftshift(spectrum)

def compute_ifft(spectrum_shifted):
    # remettre le spectre dans l'ordre normal
    spectrum = np.fft.ifftshift(spectrum_shifted)
    # IFFT
    signal = np.fft.ifft(spectrum)
    # recentrer le signal temporel
    return np.real(np.fft.fftshift(signal))

# ============================================================
# FIGURE AVEC DEUX SOUS-FENÊTRES
# ============================================================

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 7))

# Sous-fenêtre 1 : spectre centré
line_fft, = ax1.plot(freqs, np.zeros(N), color="blue")
ax1.set_title("Analyse spectrale centrée du signal lumineux (FFT)")
ax1.set_xlabel("Fréquence (Hz)")
ax1.set_ylabel("Amplitude")
ax1.set_xlim(-fs/2, fs/2)
ax1.set_ylim(0, 200)

# Sous-fenêtre 2 : signal temporel centré
line_ifft, = ax2.plot(t_centered, np.zeros_like(t_centered), color="red")
ax2.set_title("Signal temporel reconstruit (centré autour de 0)")
ax2.set_xlabel("Temps (s)")
ax2.set_ylabel("Amplitude")
ax2.set_ylim(-2, 2)

# ============================================================
# ANIMATION
# ============================================================

def update(frame):
    signal = generate_laser_signal(frame)
    spectrum = compute_fft(signal)
    reconstructed = compute_ifft(spectrum)

    line_fft.set_ydata(np.abs(spectrum))
    line_ifft.set_ydata(reconstructed)

    return line_fft, line_ifft

ani = FuncAnimation(fig, update, frames=1000, interval=30, blit=True)

plt.tight_layout()
plt.show()

Retroaction Positive Instabilité

import numpy as np
import matplotlib.pyplot as plt
from scipy import signal

# ============================================================
# PARAMETRES
# ============================================================

# ALI
G_ALI = 1e5
f_ALI = 10        # Hz
w_ALI = 2*np.pi*f_ALI

# Filtres
K = 1
f0 = 1000         # Hz
w0 = 2*np.pi*f0

Q = 1/np.sqrt(2)

# ============================================================
# AXE FREQUENTIEL
# ============================================================

f = np.logspace(0, 7, 5000)
w = 2*np.pi*f

# ============================================================
# FONCTION UTILITAIRE
# ============================================================

def bode_from_num_den(num, den, w):
    sys = signal.TransferFunction(num, den)
    w, mag, phase = signal.bode(sys, w)
    return mag, phase

def nyquist_from_num_den(num, den, w):

    s = 1j * w

    num_eval = np.polyval(num, s)
    den_eval = np.polyval(den, s)

    H = num_eval / den_eval

    return np.real(H), np.imag(H)

# ============================================================
# 1) G(p)
# ============================================================

num_G = [G_ALI * w_ALI]
den_G = [1, w_ALI]

mag_G, phase_G = bode_from_num_den(num_G, den_G, w)

# ============================================================
# 2) FILTRES CANONIQUES
# ============================================================

filters = {}

# ------------------------------------------------------------
# Passe-bas 1er ordre
# ------------------------------------------------------------

filters["PB1"] = (
    [K*w0],
    [1, w0]
)

# ------------------------------------------------------------
# Passe-haut 1er ordre
# ------------------------------------------------------------

filters["PH1"] = (
    [K, 0],
    [1, w0]
)

# ------------------------------------------------------------
# Passe-bas 2e ordre
# ------------------------------------------------------------

filters["PB2"] = (
    [K*w0**2],
    [1, w0/Q, w0**2]
)

# ------------------------------------------------------------
# Passe-bande 2e ordre
# ------------------------------------------------------------

filters["PBand2"] = (
    [K*w0/Q, 0],
    [1, w0/Q, w0**2]
)

# ------------------------------------------------------------
# Passe-haut 2e ordre
# ------------------------------------------------------------

filters["PH2"] = (
    [K, 0, 0],
    [1, w0/Q, w0**2]
)

# ============================================================
# 3) T(p)=G(p)/(1+G(p)H(p))
# ============================================================

closed_loops = {}

for name, (num_H, den_H) in filters.items():

    numG = np.array(num_G)
    denG = np.array(den_G)

    numH = np.array(num_H)
    denH = np.array(den_H)

    # GH
    num_open = np.polymul(numG, numH)
    den_open = np.polymul(denG, denH)

    # T(p)=G/(1+GH)
    num_T = np.polymul(numG, denH)
    den_T = np.polyadd(den_open, num_open)

    closed_loops[name] = (num_T, den_T)

# ============================================================
# FIGURE 1 : BODE GAIN
# ============================================================

fig_gain, axes_gain = plt.subplots(3, 1, figsize=(12, 16))

# ------------------------------------------------------------
# G(p)
# ------------------------------------------------------------

axes_gain[0].semilogx(f, mag_G, linewidth=2)

axes_gain[0].set_title("Bode Gain - G(p)")
axes_gain[0].set_ylabel("Gain (dB)")
axes_gain[0].grid(True, which="both")

# ------------------------------------------------------------
# Filtres
# ------------------------------------------------------------

for name, (num, den) in filters.items():

    mag, phase = bode_from_num_den(num, den, w)

    axes_gain[1].semilogx(f, mag, label=name)

axes_gain[1].set_title("Bode Gain - Filtres canoniques")
axes_gain[1].set_ylabel("Gain (dB)")
axes_gain[1].grid(True, which="both")
axes_gain[1].legend()

# ------------------------------------------------------------
# Boucles fermées
# ------------------------------------------------------------

for name, (num, den) in closed_loops.items():

    mag, phase = bode_from_num_den(num, den, w)

    axes_gain[2].semilogx(f, mag, label=name)

axes_gain[2].set_title("Bode Gain - Boucle fermée T(p)")
axes_gain[2].set_xlabel("Fréquence (Hz)")
axes_gain[2].set_ylabel("Gain (dB)")
axes_gain[2].grid(True, which="both")
axes_gain[2].legend()

plt.tight_layout()

# ============================================================
# FIGURE 2 : BODE PHASE
# ============================================================

fig_phase, axes_phase = plt.subplots(3, 1, figsize=(12, 16))

# ------------------------------------------------------------
# G(p)
# ------------------------------------------------------------

axes_phase[0].semilogx(f, phase_G, linewidth=2)

axes_phase[0].set_title("Bode Phase - G(p)")
axes_phase[0].set_ylabel("Phase (deg)")
axes_phase[0].grid(True, which="both")

# ------------------------------------------------------------
# Filtres
# ------------------------------------------------------------

for name, (num, den) in filters.items():

    mag, phase = bode_from_num_den(num, den, w)

    axes_phase[1].semilogx(f, phase, label=name)

axes_phase[1].set_title("Bode Phase - Filtres canoniques")
axes_phase[1].set_ylabel("Phase (deg)")
axes_phase[1].grid(True, which="both")
axes_phase[1].legend()

# ------------------------------------------------------------
# Boucles fermées
# ------------------------------------------------------------

for name, (num, den) in closed_loops.items():

    mag, phase = bode_from_num_den(num, den, w)

    axes_phase[2].semilogx(f, phase, label=name)

axes_phase[2].set_title("Bode Phase - Boucle fermée T(p)")
axes_phase[2].set_xlabel("Fréquence (Hz)")
axes_phase[2].set_ylabel("Phase (deg)")
axes_phase[2].grid(True, which="both")
axes_phase[2].legend()

plt.tight_layout()

# ============================================================
# FIGURE 3 : NYQUIST
# ============================================================

fig_nyquist, axes_nyquist = plt.subplots(3, 1, figsize=(10, 18))

# ------------------------------------------------------------
# G(p)
# ------------------------------------------------------------

re, im = nyquist_from_num_den(num_G, den_G, w)

axes_nyquist[0].plot(re, im, linewidth=2)
axes_nyquist[0].plot(re, -im, linestyle='--')

axes_nyquist[0].plot(-1, 0, 'rx', markersize=10)

axes_nyquist[0].set_title("Nyquist - G(p)")
axes_nyquist[0].set_xlabel("Partie réelle")
axes_nyquist[0].set_ylabel("Partie imaginaire")
axes_nyquist[0].grid(True)
axes_nyquist[0].axis('equal')

# ------------------------------------------------------------
# Filtres canoniques
# ------------------------------------------------------------

for name, (num, den) in filters.items():

    re, im = nyquist_from_num_den(num, den, w)

    axes_nyquist[1].plot(re, im, label=name)
    axes_nyquist[1].plot(re, -im, linestyle='--')

axes_nyquist[1].plot(-1, 0, 'rx', markersize=10)

axes_nyquist[1].set_title("Nyquist - Filtres canoniques")
axes_nyquist[1].set_xlabel("Partie réelle")
axes_nyquist[1].set_ylabel("Partie imaginaire")
axes_nyquist[1].grid(True)
axes_nyquist[1].legend()
axes_nyquist[1].axis('equal')

# ------------------------------------------------------------
# Boucles fermées T(p)
# ------------------------------------------------------------

for name, (num, den) in closed_loops.items():

    re, im = nyquist_from_num_den(num, den, w)

    axes_nyquist[2].plot(re, im, label=name)
    axes_nyquist[2].plot(re, -im, linestyle='--')

axes_nyquist[2].plot(-1, 0, 'rx', markersize=10)

axes_nyquist[2].set_title("Nyquist - Boucle fermée T(p)")
axes_nyquist[2].set_xlabel("Partie réelle")
axes_nyquist[2].set_ylabel("Partie imaginaire")
axes_nyquist[2].grid(True)
axes_nyquist[2].legend()
axes_nyquist[2].axis('equal')

plt.tight_layout()

# ============================================================
# AFFICHAGE
# ============================================================

plt.show()

Portraits de phase v1

# Créé par ennio2, le 29/04/2026 en Python 3.7
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp

## Reduction de l ordre pour utiliser la fonction solve_ivp de SciPy qui sait résoudre uniquement des équations du premier ordre.
## y′′=f(t,y,y′), on pose v=y′ Donc :
## {y′=v, v′=y′′ => {y′=v, v′=Acos(ωt)−av−ω0^2)y}
## Pour des ordres supérieurs comme pour une equation differentielle d’ordre n, on introduire n variables pour obtenir un système de n équations du premier ordre. Exemple :
## y′′′⇒3 équations

## Méthode générale de création d'une fonction deréduction d'ordre
#def reduction_d_ordre(t, Y):
#    # y, v = Y (commande de déballage de liste)
#    Y = [y, v]
#    dydt = v
#    dvdt = A*np.cos(w*t) - a*v - w0**2*y
#    return [dydt, dvdt]


# =========================
# DIFFERENTS TYPES D'EQUATIONS
# =========================

# 1. Oscillateur linéaire non amorti non forcé
def free(t, Y, w0):
    # y, yp = Y (commande de déballage de liste)
    [y, yp] = Y
    ypp = - w0**2 * (y)
    return [yp, ypp]
### 11. Oscillateur RLC à Resistance compensée par une Résistance négative (souvent plutot modélisé par une équation de Van Der Pol comme la résistane négative ne peut pas compenser exactement la résistance du circuit RLC)
##def negative_resistance(t, Y, w0, a):
##    y, yp = Y
##    ypp = -w0**2 * y + a * yp  # a = 0 → compensation par une résistance négative
##    return [yp, ypp]
# 2. Oscillateur non linéaire (Borda) amorti
def damped_oscillator_Borda(t, Y, a, w0):
    # y, yp = Y (commande de déballage de liste)
    [y, yp] = Y
    ypp = -a * yp - w0**2 * np.sin(y)
    return [yp, ypp]
# 3. Oscillateur non linéaire (Duffing simple)
def duffing(t, Y, a, w0, beta):
    # y, yp = Y (commande de déballage de liste)
    [y, yp] = Y
    ypp = -a * yp - w0**2 * y - beta * y**3
    return [yp, ypp]
# 4. Oscillateur libre amorti
def damped_oscillator(t, Y, a, w0):
    # y, yp = Y (commande de déballage de liste)
    [y, yp] = Y
    ypp = -a * yp - w0**2 * y
    return [yp, ypp]
# 5. Oscillateur forcé linéaire
def forced_oscillator(t, Y, a, w0, A, w):
    # y, yp = Y (commande de déballage de liste)
    [y, yp] = Y
    ypp = A * np.cos(w * t) - a * yp - w0**2 * y
    return [yp, ypp]
# 6. Oscillateur auto-entretenu (contre-réaction) (modélisé par l'équation de Van Der Pol)
def Van_Der_Pol_oscillator(t, Y, w0, E0, y00):
    # y, yp = Y (commande de déballage de liste)
    [y, yp] = Y
    ypp = E0 * (1 - (y/y00)**2) * yp - w0**2 * y
    return [yp, ypp]
### 61. Oscillateur de Wien
##def wien_oscillator(t, Y, w0, mu):
##    y, yp = Y
##    ypp = -w0**2 * y + mu * (1 - y**2) * yp
##    return [yp, ypp]
# 7. Forçage non sinusoïdal
def square_forcing(t, Y, a, w0, A):
    # y, yp = Y (commande de déballage de liste)
    [y, yp] = Y
    force = A * np.sign(np.sin(t))
    ypp = force - a * yp - w0**2 * y
    return [yp, ypp]


# =========================
# CONFIGURATION DES CAS ET DEFINITION DES VARIABLES
# =========================

cases = [
    {
        "name": "Oscillateur non amorti linéaire",
        "func": free,
        "params": {"w0": 1.5},
        "y0": [10, 0]
    },
##    {
##        "name": "Oscillateur à résistance négative",
##        "func": negative_resistance,
##        "params": {"w0": 1.5, "a" : 0},
##        "y0": [10, 0]
##    },
    {
        "name": "Oscillateur amorti non Linéaire (Borda)",
        "func": damped_oscillator_Borda,
        "params": {"a": 0, "w0": 2.0},
        "y0": [10, 0]
    },
    {
        "name": "Duffing (non linéaire)",
        "func": duffing,
        "params": {"a": 0, "w0": 1.5, "beta": 0.5},
        "y0": [10, 0]
    },
    {
        "name": "Oscillateur amorti lineaire",
        "func": damped_oscillator,
        "params": {"a": 0.5, "w0": 2.0},
        "y0": [1, 0]
    },
    {
        "name": "Oscillateur amorti forcé",
        "func": forced_oscillator,
        "params": {"a": 0.3, "w0": 2.0, "A": 1.0, "w": 1.5},
        "y0": [1, 0]
    },
    {
        "name": "Oscillateur auto-entretenu (modélisation par l'équation de Van Der Pol)",
        "func": Van_Der_Pol_oscillator,
        "y0": [1, 0],
        "params": {"w0": 2.0, "E0": 1.0, "y00" : 1.0} # y00 doit etre egal à la position en 0 definie par les conditions initiales
    },
##    {
##        "name": "Oscillateur de Wien",
##        "func": wien_oscillator,
##        "y0": [1, 0],
##        "params": {"w0": 2.0, "mu": 1.0}
##    },
    {
        "name": "Forçage carré",
        "func": square_forcing,
        "params": {"a": 0.3, "w0": 2.0, "A": 1.0},
        "y0": [0, 0]
    }
]

# Temps
t_span = (0, 20)
t_eval = np.linspace(*t_span, 1000)


# =========================
# OBTENTION DES SOLUTIONS ET PLOT
# =========================





#création d'un solveur générique
# L'utilisation d'une fonction intermédiare avec lamba ou en définissant proprement une fonction est nécessaire car solve_ivp necessite en paramètres une fonction avec deux paramètres uniquement. Il faut donc adapter la fonction definie à la fonction d'entrée demadée par solve_ivp

### Solveur générique (version avec lambda)
##def solve_ode(system_func, y0, t_span, t_eval, params):
##    sol = solve_ivp(
##        lambda t, Y: system_func(t, Y, **params),
##        t_span,
##        y0,
##        t_eval=t_eval
##    )
##    return sol

## Solveur numérique (version plus lisible)
def solve_ode(system_func, y0, t_span, t_eval, params):
    def wrapped_system(t, Y):
        return system_func(t, Y, **params)

    return solve_ivp(wrapped_system, t_span, y0, t_eval=t_eval)





m = 1 # la masse doit etre définie pour passer aux energies



figs = []

for i, case in enumerate(cases):

    w0 = (case["params"]["w0"])

    sol = solve_ode(case["func"], case["y0"], t_span, t_eval, case["params"])

    # une figure par case
    fig, (ax1, ax2, ax3, ax4, ax5) = plt.subplots(5, 1, figsize=(8, 6))

    # subplot 1 : y(t)
    ax1.plot(sol.t, sol.y[0], label=case["name"])
    ax1.legend()
    ax1.set_xlabel("Temps")
    ax1.set_ylabel("y(t)")
    ax1.set_title("Postion = f(t)")
    ax1.grid()

    # subplot 2 : y vs dy/dt
    dy_dt = np.gradient(sol.y[0], sol.t)
    ax2.plot(sol.y[0], dy_dt, label=case["name"], color='r', linewidth=2.0)
    ax2.legend()
    ax2.set_xlabel("Position")
    ax2.set_ylabel("y(t)")
    ax2.set_title("Portraits de phase")
    ax2.grid()

    # subplot 3 : Ec = f(t)
    ax3.plot(sol.t, 1/2*m*(dy_dt)**2, color='m', label=case["name"])
    ax3.legend()
    ax3.set_xlabel("Temps")
    ax3.set_ylabel("Ec")
    ax3.set_title("Ec = f(t)")
    ax3.grid()

    # subplot 4 : Ep = f(t)
    ax4.plot(sol.t, w0**2/m*(sol.y[0])**2, label=case["name"], color='g', linewidth=2.0)
    ax4.legend()
    ax4.set_xlabel("Temps")
    ax4.set_ylabel("Ep")
    ax4.set_title("Ep = f(t)")
    ax4.grid()


    # subplot 5 : Ep
    ax5.plot(w0**2/m*(sol.y[0])**2, 1/2*m*(dy_dt)**2, label=case["name"], color='g', linewidth=2.0)
    ax5.legend()
    ax5.set_xlabel("Ep")
    ax5.set_ylabel("Ec")
    ax5.set_title("Ec = f(E(p)")
    ax5.grid()


    fig.suptitle(case["name"])
    plt.tight_layout()

    # fig.suptitle(case["name"])
    figs.append(fig)


# L'affichage de toutes les figures d'un coup doit etre hors de la boucle for
plt.show()



Portraits de phase v2

# Créé par ennio2, le 29/04/2026 en Python 3.7
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp

## Reduction de l ordre pour utiliser la fonction solve_ivp de SciPy qui sait résoudre uniquement des équations du premier ordre.
## y′′=f(t,y,y′), on pose v=y′ Donc :
## {y′=v, v′=y′′ => {y′=v, v′=Acos(ωt)−av−ω0^2)y}
## Pour des ordres supérieurs comme pour une equation differentielle d’ordre n, on introduire n variables pour obtenir un système de n équations du premier ordre. Exemple :
## y′′′⇒3 équations

## Méthode générale de création d'une fonction deréduction d'ordre
#def reduction_d_ordre(t, Y):
#    # y, v = Y (commande de déballage de liste)
#    Y = [y, v]
#    dydt = v
#    dvdt = A*np.cos(w*t) - a*v - w0**2*y
#    return [dydt, dvdt]


# =========================
# DIFFERENTS TYPES D'EQUATIONS
# =========================

# 1. Oscillateur linéaire non amorti non forcé
def free(t, Y, w0):
    # y, yp = Y (commande de déballage de liste)
    [y, yp] = Y
    ypp = - w0**2 * (y)
    return [yp, ypp]
### 11. Oscillateur RLC à Resistance compensée par une Résistance négative (souvent plutot modélisé par une équation de Van Der Pol comme la résistane négative ne peut pas compenser exactement la résistance du circuit RLC)
##def negative_resistance(t, Y, w0, a):
##    y, yp = Y
##    ypp = -w0**2 * y + a * yp  # a = 0 → compensation par une résistance négative
##    return [yp, ypp]
# 2. Oscillateur non linéaire (Borda) amorti
def damped_oscillator_Borda(t, Y, a, w0):
    # y, yp = Y (commande de déballage de liste)
    [y, yp] = Y
    ypp = -a * yp - w0**2 * np.sin(y)
    return [yp, ypp]
# 3. Oscillateur non linéaire (Duffing simple)
def duffing(t, Y, a, w0, beta):
    # y, yp = Y (commande de déballage de liste)
    [y, yp] = Y
    ypp = -a * yp - w0**2 * y - beta * y**3
    return [yp, ypp]
# 4. Oscillateur libre amorti
def damped_oscillator(t, Y, a, w0):
    # y, yp = Y (commande de déballage de liste)
    [y, yp] = Y
    ypp = -a * yp - w0**2 * y
    return [yp, ypp]
# 5. Oscillateur forcé linéaire
def forced_oscillator(t, Y, a, w0, A, w):
    # y, yp = Y (commande de déballage de liste)
    [y, yp] = Y
    ypp = A * np.cos(w * t) - a * yp - w0**2 * y
    return [yp, ypp]
# 6. Oscillateur auto-entretenu (contre-réaction) (modélisé par l'équation de Van Der Pol)
def Van_Der_Pol_oscillator(t, Y, w0, E0, y00):
    # y, yp = Y (commande de déballage de liste)
    [y, yp] = Y
    ypp = E0 * (1 - (y/y00)**2) * yp - w0**2 * y
    return [yp, ypp]
### 61. Oscillateur de Wien
##def wien_oscillator(t, Y, w0, mu):
##    y, yp = Y
##    ypp = -w0**2 * y + mu * (1 - y**2) * yp
##    return [yp, ypp]
# 7. Forçage non sinusoïdal
def square_forcing(t, Y, a, w0, A):
    # y, yp = Y (commande de déballage de liste)
    [y, yp] = Y
    force = A * np.sign(np.sin(t))
    ypp = force - a * yp - w0**2 * y
    return [yp, ypp]


# =========================
# CONFIGURATION DES CAS ET DEFINITION DES VARIABLES
# =========================

cases = [
    {
        "name": "Oscillateur non amorti linéaire",
        "func": free,
        "params": {"w0": 1.5},
        "y0": [10, 0]
    },
##    {
##        "name": "Oscillateur à résistance négative",
##        "func": negative_resistance,
##        "params": {"w0": 1.5, "a" : 0},
##        "y0": [10, 0]
##    },
    {
        "name": "Oscillateur amorti non Linéaire (Borda)",
        "func": damped_oscillator_Borda,
        "params": {"a": 0, "w0": 2.0},
        "y0": [10, 0]
    },
    {
        "name": "Duffing (non linéaire)",
        "func": duffing,
        "params": {"a": 0, "w0": 1.5, "beta": 0.5},
        "y0": [10, 0]
    },
    {
        "name": "Oscillateur amorti lineaire",
        "func": damped_oscillator,
        "params": {"a": 0.5, "w0": 2.0},
        "y0": [1, 0]
    },
    {
        "name": "Oscillateur amorti forcé",
        "func": forced_oscillator,
        "params": {"a": 0.3, "w0": 2.0, "A": 1.0, "w": 1.5},
        "y0": [1, 0]
    },
    {
        "name": "Oscillateur auto-entretenu (modélisation par l'équation de Van Der Pol)",
        "func": Van_Der_Pol_oscillator,
        "y0": [1, 0],
        "params": {"w0": 2.0, "E0": 1.0, "y00" : 1.0} # y00 doit etre egal à la position en 0 definie par les conditions initiales
    },
##    {
##        "name": "Oscillateur de Wien",
##        "func": wien_oscillator,
##        "y0": [1, 0],
##        "params": {"w0": 2.0, "mu": 1.0}
##    },
    {
        "name": "Forçage carré",
        "func": square_forcing,
        "params": {"a": 0.3, "w0": 2.0, "A": 1.0},
        "y0": [0, 0]
    }
]

# Temps
t_span = (0, 20)
t_eval = np.linspace(*t_span, 1000)


# =========================
# OBTENTION DES SOLUTIONS ET PLOT
# =========================





#création d'un solveur générique
# L'utilisation d'une fonction intermédiare avec lamba ou en définissant proprement une fonction est nécessaire car solve_ivp necessite en paramètres une fonction avec deux paramètres uniquement. Il faut donc adapter la fonction definie à la fonction d'entrée demadée par solve_ivp

### Solveur générique (version avec lambda)
##def solve_ode(system_func, y0, t_span, t_eval, params):
##    sol = solve_ivp(
##        lambda t, Y: system_func(t, Y, **params),
##        t_span,
##        y0,
##        t_eval=t_eval
##    )
##    return sol

## Solveur numérique (version plus lisible)
def solve_ode(system_func, y0, t_span, t_eval, params):
    def wrapped_system(t, Y):
        return system_func(t, Y, **params)

    return solve_ivp(wrapped_system, t_span, y0, t_eval=t_eval)





m = 1 # la masse doit etre définie pour passer aux energies



figs = []

for i, case in enumerate(cases):

    w0 = (case["params"]["w0"])

    sol = solve_ode(case["func"], case["y0"], t_span, t_eval, case["params"])

    # une figure par case
    fig, (ax1, ax2, ax3, ax4, ax5) = plt.subplots(5, 1, figsize=(8, 6))

    # subplot 1 : y(t)
    ax1.plot(sol.t, sol.y[0], label=case["name"])
    ax1.legend()
    ax1.set_xlabel("Temps")
    ax1.set_ylabel("y(t)")
    ax1.set_title("Postion = f(t)")
    ax1.grid()

    # subplot 2 : y vs dy/dt
    dy_dt = np.gradient(sol.y[0], sol.t)
    ax2.plot(sol.y[0], dy_dt, label=case["name"], color='r', linewidth=2.0)
    ax2.legend()
    ax2.set_xlabel("Position")
    ax2.set_ylabel("y(t)")
    ax2.set_title("Portraits de phase")
    ax2.grid()

    # subplot 3 : Ec = f(t)
    ax3.plot(sol.t, 1/2*m*(dy_dt)**2, color='m', label=case["name"])
    ax3.legend()
    ax3.set_xlabel("Temps")
    ax3.set_ylabel("Ec")
    ax3.set_title("Ec = f(t)")
    ax3.grid()

    # subplot 4 : Ep = f(t)
    ax4.plot(sol.t, w0**2/m*(sol.y[0])**2, label=case["name"], color='g', linewidth=2.0)
    ax4.legend()
    ax4.set_xlabel("Temps")
    ax4.set_ylabel("Ep")
    ax4.set_title("Ep = f(t)")
    ax4.grid()


    # subplot 5 : Ep
    ax5.plot(w0**2/m*(sol.y[0])**2, 1/2*m*(dy_dt)**2, label=case["name"], color='g', linewidth=2.0)
    ax5.legend()
    ax5.set_xlabel("Ep")
    ax5.set_ylabel("Ec")
    ax5.set_title("Ec = f(E(p)")
    ax5.grid()


    fig.suptitle(case["name"])
    plt.tight_layout()

    # fig.suptitle(case["name"])
    figs.append(fig)


# L'affichage de toutes les figures d'un coup doit etre hors de la boucle for
plt.show()