ktongue's picture
download
raw
11.6 kB
#!/usr/bin/env python3
"""
Simulation DEM (Discrete Element Method) - Matériaux Granulaires
Implémentation d'une simulation granulaire 2D simplifiée en Python
Auteur: OpenCode Assistant
Date: 2025
"""
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from scipy.spatial.distance import cdist
import time
class DEMSimulation:
"""
Classe principale pour la simulation DEM (Discrete Element Method)
Méthode des éléments discrets pour matériaux granulaires
"""
def __init__(self, num_particles=100, box_size=10.0):
"""
Initialisation de la simulation
Args:
num_particles (int): Nombre de particules
box_size (float): Taille de la boîte de simulation
"""
self.num_particles = num_particles
self.box_size = box_size
# Paramètres physiques
self.dt = 0.001 # Pas de temps (s)
self.total_time = 5.0 # Temps total de simulation (s)
self.gravity = np.array([0.0, -9.81]) # Gravité (m/s²)
self.damping = 0.99 # Amortissement visqueux
self.restitution = 0.8 # Coefficient de restitution
self.friction_coeff = 0.1 # Coefficient de frottement
# Paramètres de contact
self.k_normal = 1000.0 # Raideur normale (N/m)
self.c_normal = 10.0 # Amortissement normal (N·s/m)
# Initialisation des particules
self._initialize_particles()
# Données pour l'analyse
self.kinetic_energy_history = []
self.time_history = []
def _initialize_particles(self):
"""Initialisation des particules avec positions et vitesses aléatoires"""
# États des particules: [x, y, vx, vy]
self.particles = np.zeros((self.num_particles, 4))
# Rayons aléatoires (distribution uniforme)
self.radii = np.random.uniform(0.05, 0.15, self.num_particles)
# Masses proportionnelles à l'aire (ρ = 1 pour simplifier)
self.masses = self.radii**2 * np.pi
# Positions initiales aléatoires (dans la moitié supérieure)
self.particles[:, 0] = np.random.uniform(
self.radii, self.box_size - self.radii, self.num_particles
) # Position X
self.particles[:, 1] = np.random.uniform(
self.box_size * 0.5, self.box_size - self.radii, self.num_particles
) # Position Y (moitié supérieure)
# Vitesses initiales aléatoires (petites)
self.particles[:, 2:] = np.random.uniform(-0.5, 0.5, (self.num_particles, 2))
def compute_forces(self):
"""
Calcul des forces entre particules et avec les parois
Returns:
np.ndarray: Forces appliquées à chaque particule [fx, fy]
"""
forces = np.zeros((self.num_particles, 2))
# Force gravitationnelle
for i in range(self.num_particles):
forces[i] += self.masses[i] * self.gravity
# Calcul des distances inter-particulaires
positions = self.particles[:, :2]
velocities = self.particles[:, 2:]
dist_matrix = cdist(positions, positions)
# Évite les auto-collisions
np.fill_diagonal(dist_matrix, np.inf)
# Forces de contact entre particules
for i in range(self.num_particles):
for j in range(i + 1, self.num_particles):
dist_ij = dist_matrix[i, j]
radius_sum = self.radii[i] + self.radii[j]
if dist_ij < radius_sum:
# Collision détectée
overlap = radius_sum - dist_ij
# Direction normale (de i vers j)
normal_dir = (positions[j] - positions[i]) / dist_ij
# Vitesse relative
relative_vel = velocities[j] - velocities[i]
vn = np.dot(relative_vel, normal_dir) # Composante normale
# Force normale (modèle ressort-amortisseur)
fn = self.k_normal * overlap - self.c_normal * vn
# Force tangentielle (frottement de Coulomb)
tangent_dir = np.array([-normal_dir[1], normal_dir[0]])
vt = np.dot(relative_vel, tangent_dir)
ft = -self.friction_coeff * abs(fn) * np.sign(vt)
# Application des forces (action-réaction)
force_normal = fn * normal_dir
force_tangent = ft * tangent_dir
forces[i] -= force_normal + force_tangent
forces[j] += force_normal + force_tangent
return forces
def apply_boundary_conditions(self):
"""Application des conditions aux limites (parois réfléchissantes)"""
for i in range(self.num_particles):
# Paroi gauche
if self.particles[i, 0] < self.radii[i]:
self.particles[i, 0] = self.radii[i]
self.particles[i, 2] *= -self.restitution
# Paroi droite
elif self.particles[i, 0] > self.box_size - self.radii[i]:
self.particles[i, 0] = self.box_size - self.radii[i]
self.particles[i, 2] *= -self.restitution
# Paroi basse (sol)
if self.particles[i, 1] < self.radii[i]:
self.particles[i, 1] = self.radii[i]
self.particles[i, 3] *= -self.restitution
# Paroi haute (plafond)
elif self.particles[i, 1] > self.box_size - self.radii[i]:
self.particles[i, 1] = self.box_size - self.radii[i]
self.particles[i, 3] *= -self.restitution
def integrate_verlet(self, forces):
"""
Intégration temporelle utilisant le schéma de Verlet
Args:
forces (np.ndarray): Forces appliquées aux particules
"""
# Intégration de Verlet (plus stable que Euler)
accelerations = forces / self.masses[:, None]
# Mise à jour des vitesses (première moitié)
self.particles[:, 2:] += 0.5 * accelerations * self.dt
# Mise à jour des positions
self.particles[:, :2] += self.particles[:, 2:] * self.dt
# Recalcul des forces avec nouvelles positions
new_forces = self.compute_forces()
new_accelerations = new_forces / self.masses[:, None]
# Mise à jour des vitesses (seconde moitié)
self.particles[:, 2:] += 0.5 * new_accelerations * self.dt
# Amortissement visqueux
self.particles[:, 2:] *= self.damping
def compute_kinetic_energy(self):
"""Calcul de l'énergie cinétique totale du système"""
velocities_squared = np.sum(self.particles[:, 2:]**2, axis=1)
return 0.5 * np.sum(self.masses * velocities_squared)
def step(self):
"""Effectue un pas de temps complet de simulation"""
# Calcul des forces
forces = self.compute_forces()
# Intégration
self.integrate_verlet(forces)
# Conditions aux limites
self.apply_boundary_conditions()
def run_simulation(self, save_animation=True, save_data=True):
"""
Exécute la simulation complète
Args:
save_animation (bool): Sauvegarder l'animation
save_data (bool): Sauvegarder les données d'analyse
"""
print(f"Démarrage de la simulation DEM avec {self.num_particles} particules...")
print(f"Durée totale: {self.total_time} secondes")
print(f"Pas de temps: {self.dt} secondes")
start_time = time.time()
# Configuration de l'animation
fig, ax, scatter, time_text = None, None, None, None
if save_animation:
fig, ax = plt.subplots(figsize=(10, 8))
ax.set_xlim(0, self.box_size)
ax.set_ylim(0, self.box_size)
ax.set_aspect('equal')
ax.grid(True, alpha=0.3)
ax.set_xlabel('Position X (m)')
ax.set_ylabel('Position Y (m)')
ax.set_title('Simulation DEM - Matériaux Granulaires')
# Scatter plot des particules
scatter = ax.scatter(
self.particles[:, 0], self.particles[:, 1],
s=self.radii*500, c='blue', alpha=0.7, edgecolors='black', linewidth=0.5
)
# Texte pour afficher le temps et l'énergie
time_text = ax.text(0.02, 0.98, '', transform=ax.transAxes, fontsize=12,
verticalalignment='top', bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
# Boucle de simulation
num_steps = int(self.total_time / self.dt)
for step_idx in range(num_steps):
current_time = step_idx * self.dt
# Pas de simulation
self.step()
# Enregistrement des données
if save_data and step_idx % 10 == 0: # Tous les 10 pas
ke = self.compute_kinetic_energy()
self.kinetic_energy_history.append(ke)
self.time_history.append(current_time)
# Mise à jour de l'animation
if save_animation and scatter is not None and time_text is not None and step_idx % 20 == 0: # Tous les 20 pas pour l'animation
scatter.set_offsets(self.particles[:, :2])
ke = self.compute_kinetic_energy()
time_text.set_text('.3f')
plt.pause(0.001) # Permet la mise à jour de l'affichage
if step_idx % 500 == 0: # Affichage de progression
progress = (step_idx / num_steps) * 100
print(".1f")
# Sauvegarde des résultats
if save_data:
self.save_results()
elapsed_time = time.time() - start_time
print(".2f")
print(".1f")
if save_animation:
plt.show()
def save_results(self):
"""Sauvegarde les résultats de simulation"""
# Sauvegarde des positions finales
np.savetxt('final_positions.csv',
np.column_stack([self.particles[:, :2], self.radii]),
header='x,y,radius', delimiter=',', fmt='%.6f')
# Sauvegarde de l'énergie cinétique
np.savetxt('kinetic_energy.csv',
np.column_stack([self.time_history, self.kinetic_energy_history]),
header='time,kinetic_energy', delimiter=',', fmt='%.6f')
print("Résultats sauvegardés dans 'final_positions.csv' et 'kinetic_energy.csv'")
def plot_energy_evolution(self):
"""Affiche l'évolution de l'énergie cinétique"""
plt.figure(figsize=(10, 6))
plt.plot(self.time_history, self.kinetic_energy_history, 'r-', linewidth=2)
plt.xlabel('Temps (s)')
plt.ylabel('Énergie Cinétique (J)')
plt.title('Évolution de l\'énergie cinétique du système')
plt.grid(True, alpha=0.3)
plt.savefig('energy_evolution.png', dpi=300, bbox_inches='tight')
plt.show()
def main():
"""Fonction principale"""
# Configuration de la simulation
sim = DEMSimulation(num_particles=150, box_size=8.0)
# Paramètres personnalisables
sim.dt = 0.0005 # Pas de temps plus petit pour stabilité
sim.total_time = 3.0 # Simulation plus courte
sim.restitution = 0.7 # Moins élastique
sim.friction_coeff = 0.2 # Plus de frottement
# Exécution
sim.run_simulation(save_animation=True, save_data=True)
# Analyse post-simulation
sim.plot_energy_evolution()
if __name__ == "__main__":
main()

Xet Storage Details

Size:
11.6 kB
·
Xet hash:
148218bf1755e7dcf40891be8af8114359158568f898a44e24526f7b1c18cbe9

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.