sandbox/easystab/easyPYstab/Lorenz_Convection.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-Created on Wed Jan 7 20:28:46 2026 (adapted from a previous program in Matlab)
Program designed to illustrate the chapter 5 of the course “Introduction to hydrodynamical instabilities” M2 DET, Toulouse
@author: David Fabre
This program is protected by Creative Commons Licence CC-BY-NC
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
plt.close('all')
# Définition of the dynamical function for Lorenz system
def lorenz(XYZ, *, r=28, P=10, beta=8/3.):
X,Y,Z = XYZ
X_dot = P * (Y - X)
Y_dot = r * X - Y - X * Z
Z_dot = X * Y - beta * Z
return np.array([X_dot, Y_dot, Z_dot])
# Function for selection of parameters by user wirth default value
def myinput(question,default):
answer = input(f"{question} [default : {default}] : " )
if not answer:
return default
else:
return answer
# Selection or parameters by user
print("\n Integration of the Lorenz system (demonstration program \n")
print(" Select the kind of computation you want to do : ")
print(" 0 : Basic plot of the Lorenz attractor")
print(" 1 : animation showing the trajectory and reconstruction of convection flow")
print(" 2 : animation showing two trajectories initially close")
kind = int(myinput(" >>> Your choice ? ",0));
# Physical parameters
r = float(myinput("\n Enter the value of the control parameter r :",28))
P = 10;beta = 2.667;
print(" ( NB other parameters are set to P = "+str(P)+" ; beta = "+str(beta)+" )")
# Conditions initiales (on peut les modifier)
print(" Select Initial condition :")
Xinit = float(myinput(" X_init = ",0.5*np.sqrt(r)))
Yinit = float(myinput(" Y_init = ",-0.5*np.sqrt(r)))
Zinit = float(myinput(" Z_init = ",r/10))
init = Xinit,Yinit,Zinit
# Shema Runge-Kutta 4 used for time integration
def rk4_step(f, state, dt, **kwargs):
# f : fonction dérivée (lorenz)
k1 = f(state, **kwargs)
k2 = f(state + 0.5*dt*k1, **kwargs)
k3 = f(state + 0.5*dt*k2, **kwargs)
k4 = f(state + dt*k3, **kwargs)
return state + (dt/6.0)*(k1 + 2*k2 + 2*k3 + k4)
# Paramètres de simulation
dt = 0.01 # pas de temps
num_steps = 10000 # nombre de pas
# Tableau pour stocker les valeurs
data = np.empty((num_steps + 2, 3))
data = np.zeros((num_steps, 3))
data[0] = init
# Intégration par RK4
for i in range(num_steps-1):
data[i + 1] = rk4_step(lorenz, data[i], dt,r=r,P=P,beta=beta)
if (kind==0):
# -------------------------
# Version du programme qui trace juste l'attracteur
# -------------------------
# Création du graphique 3D
fig = plt.figure()
ax = fig.add_subplot(projection='3d')
# Tracé de la trajectoire
ax.plot(data[:,0],data[:,1],data[:,2], lw=0.5) # xyzs.T transpose (3, N)
ax.set_xlabel("X Axis")
ax.set_ylabel("Y Axis")
ax.set_zlabel("Z Axis")
ax.set_title("Attracteur de Lorenz pour r = "+str(r))
plt.show()
if(kind==2):
# -------------------------
# Version du programme qui trace deux trajectoires
# -------------------------
print("\n For plot of two trajectories : distance between initial conditions")
epsilon = float(myinput(" epsilon = ",1e-3))
# Integrate with RK4, second trajectory
data2 = np.zeros((num_steps, 3))
data2[0] = data[0] + [epsilon,epsilon,epsilon]
for i in range(num_steps - 1):
data2[i+1] = rk4_step(lorenz, data2[i], dt,r=r,P=P,beta=beta)
# Prepare figure
fig = plt.figure()
ax = fig.add_subplot(projection='3d')
line, = ax.plot([], [], [], lw=0.8,color='red')
line2, = ax.plot([], [], [], lw=0.8,color='blue')
ax.set_xlim((-20, 20))
ax.set_ylim((-30, 30))
ax.set_zlim((0, 50))
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.set_zlabel("Z")
# Animation function
def update_2traj(frame):
line.set_data(data[:frame, 0], data[:frame, 1])
line.set_3d_properties(data[:frame, 2])
line2.set_data(data2[:frame, 0], data2[:frame, 1])
line2.set_3d_properties(data2[:frame, 2])
return line,line2
# Create animation
ani = FuncAnimation(fig, update_2traj, frames=num_steps, interval=5, blit=True)
plt.show()
if(kind==1):
# -------------------------
# Version du programme pour visualiser le champ de temp/vitesse
# -------------------------
# préparation maillage 2D pour contour + quiver
n = 20 # résolution du quiver (peut être plus petit pour performance)
x_line = np.linspace(0, 2, n)
y_line = np.linspace(0, 1, n)
x_grid, y_grid = np.meshgrid(x_line, y_line)
def PlotConvection(XX,x_grid,y_grid):
# Fonction qui sert a representer le champ de temperature et de vitesse
X,Y,Z = XX
ampX = .02;ampY = 0.02;ampZ = .005; pi = np.pi;
UU = -X*ampX*np.cos(y_grid*pi)*np.sin(x_grid*pi)
VV = X*ampX*np.sin(y_grid*pi)*np.cos(x_grid*pi)
TT = ((1-y_grid)*np.ones(x_grid.shape)) \
- ampZ*Z*(np.sin(2*pi*y_grid)*np.ones(x_grid.shape)) \
+ ampY*Y*(np.sin(y_grid*pi)*np.cos(x_grid*pi))
return TT,UU,VV
# création figure + axes
fig = plt.figure(figsize=(12,6))
ax3d = fig.add_subplot(1,2,1, projection='3d')
ax2d = fig.add_subplot(1,2,2)
# axe 3D
line3d, = ax3d.plot([], [], [], color='blue', lw=0.8)
ax3d.set_xlim(-20, 20); ax3d.set_ylim(-30, 30); ax3d.set_zlim(0,50)
ax3d.set_title("Attracteur de Lorenz 3D")
ax3d.set_xlabel("X"); ax3d.set_ylabel("Y"); ax3d.set_zlabel("Z")
# axe 2D
TT,UU,VV= PlotConvection(init,x_grid,y_grid)
Shape = np.ones_like(x_grid)
contour_plot = ax2d.contourf(x_grid, y_grid, TT, levels=20, cmap='viridis')
quiver_plot = ax2d.quiver(x_grid, y_grid, UU,VV,color="white")
ax2d.set_title("Iso-contours T + quiver [U,V]")
ax2d.set_xlabel("X"); ax2d.set_ylabel("Y")
ax2d.axis('equal')
cbar = fig.colorbar(contour_plot, ax=ax2d, shrink=0.8)
# fonction d’update
def update(frame):
# mise à jour 3D
line3d.set_data(data[:frame, 0], data[:frame, 1])
line3d.set_3d_properties(data[:frame, 2])
# Construction des champs à plotter
TT,UU,VV= PlotConvection(data[frame, :],x_grid,y_grid)
# suppression de l'ancien contourf
if hasattr(ax2d, 'contour_plot'):
for coll in ax2d.contour_plot.collections:
coll.remove()
# tracer le nouveau contourf
ax2d.contour_plot = ax2d.contourf(x_grid, y_grid, TT, levels=20, cmap='viridis')
# suppression de l'ancien quiver
if hasattr(ax2d, 'quiver_plot'):
ax2d.quiver_plot.remove()
# tracer le nouveau quiver
ax2d.quiver_plot = ax2d.quiver(x_grid, y_grid, UU, VV, color='white', scale=np.sqrt(r)/2)
return line3d, ax2d.contour_plot, ax2d.quiver_plot
# animation
ani = FuncAnimation(
fig,
update,
frames=range(0, num_steps, 10), # mise à jour tous les 10 pas
interval=40,
blit=False
)
plt.tight_layout()
plt.show()
