Buckets:
| #!/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.