Buckets:
| import streamlit as st | |
| import numpy as np | |
| import matplotlib.pyplot as plt | |
| from matplotlib.animation import FuncAnimation | |
| from io import BytesIO | |
| import base64 | |
| import time | |
| from functools import lru_cache | |
| import pandas as pd | |
| from scipy import integrate, signal | |
| from reportlab.lib.pagesizes import A4 | |
| from reportlab.lib import colors | |
| from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle | |
| from reportlab.lib.units import inch, cm | |
| from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image, PageBreak, Flowable | |
| from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT | |
| from reportlab.graphics.shapes import Drawing, Rect, String, Circle, Line, Group | |
| from reportlab.graphics import renderPDF | |
| import tempfile | |
| import os | |
| # Import des fonctions helpers | |
| from robot_souple_helpers import * | |
| # Configuration de la page | |
| st.set_page_config( | |
| page_title="Jumeaux Numériques : Robot Souple - Centrale Lyon ENISE", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| # CSS personnalisé | |
| st.markdown(""" | |
| <style> | |
| .main-header { | |
| background: linear-gradient(135deg, #1e3c72 0%, #2a5298 50%, #1e3c72 100%); | |
| padding: 2.5rem; | |
| border-radius: 15px; | |
| color: white; | |
| text-align: center; | |
| margin-bottom: 2rem; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.3); | |
| } | |
| .section-header { | |
| background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); | |
| padding: 1rem 2rem; | |
| border-radius: 10px; | |
| color: white; | |
| margin: 1.5rem 0; | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.2); | |
| } | |
| .metric-card { | |
| background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); | |
| padding: 1.5rem; | |
| border-radius: 10px; | |
| text-align: center; | |
| box-shadow: 0 4px 10px rgba(0,0,0,0.1); | |
| } | |
| .formula-box { | |
| background: #1a1a2e; | |
| color: #00ff88; | |
| padding: 1.5rem; | |
| border-radius: 10px; | |
| font-family: 'Courier New', monospace; | |
| margin: 1rem 0; | |
| border: 2px solid #00ff88; | |
| } | |
| .success-box { | |
| background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); | |
| padding: 1rem; | |
| border-radius: 10px; | |
| color: white; | |
| } | |
| .copyright { | |
| text-align: center; | |
| padding: 2rem; | |
| color: #666; | |
| font-size: 0.9rem; | |
| } | |
| .stButton>button { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| border: none; | |
| padding: 0.8rem 2rem; | |
| border-radius: 25px; | |
| font-weight: bold; | |
| box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); | |
| } | |
| .stButton>button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6); | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # Header | |
| st.markdown(""" | |
| <div class="main-header"> | |
| <h1 style="font-size: 2.5rem; margin-bottom: 0.5rem;">🤖 Jumeaux Numériques</h1> | |
| <h2 style="font-weight: 300; opacity: 0.9;">Simulation de Robot Souple</h2> | |
| <p style="margin-top: 1rem; opacity: 0.8;">École Centrale Lyon ENISE | Estimation d'état et pilotage</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| def cached_matrices(nx, dx, E, Ix, rho, S, cy): | |
| """Cache des matrices pour optimisation""" | |
| return neb_beam_matrices(nx, dx, E, Ix, rho, S, cy) | |
| class PDFReportGenerator: | |
| """Générateur de rapports PDF professionnel avec ReportLab""" | |
| def __init__(self): | |
| self.styles = getSampleStyleSheet() | |
| self.setup_custom_styles() | |
| def setup_custom_styles(self): | |
| """Configure les styles personnalisés""" | |
| # Style titre principal | |
| self.styles.add(ParagraphStyle( | |
| name='MainTitle', | |
| parent=self.styles['Heading1'], | |
| fontSize=24, | |
| spaceAfter=30, | |
| alignment=TA_CENTER, | |
| textColor=colors.HexColor('#1e3c72'), | |
| fontName='Helvetica-Bold' | |
| )) | |
| # Style sous-titre | |
| self.styles.add(ParagraphStyle( | |
| name='SubTitle', | |
| parent=self.styles['Heading2'], | |
| fontSize=16, | |
| spaceAfter=20, | |
| alignment=TA_CENTER, | |
| textColor=colors.HexColor('#667eea'), | |
| fontName='Helvetica' | |
| )) | |
| # Style section | |
| self.styles.add(ParagraphStyle( | |
| name='SectionHeader', | |
| parent=self.styles['Heading3'], | |
| fontSize=14, | |
| spaceBefore=20, | |
| spaceAfter=15, | |
| textColor=colors.white, | |
| backColor=colors.HexColor('#667eea'), | |
| padding=10, | |
| fontName='Helvetica-Bold' | |
| )) | |
| # Style normal | |
| self.styles.add(ParagraphStyle( | |
| name='NormalText', | |
| parent=self.styles['Normal'], | |
| fontSize=11, | |
| spaceAfter=8, | |
| textColor=colors.black, | |
| fontName='Helvetica' | |
| )) | |
| # Style métrique | |
| self.styles.add(ParagraphStyle( | |
| name='MetricLabel', | |
| parent=self.styles['Normal'], | |
| fontSize=10, | |
| spaceAfter=4, | |
| textColor=colors.HexColor('#1e3c72'), | |
| fontName='Helvetica-Bold' | |
| )) | |
| self.styles.add(ParagraphStyle( | |
| name='MetricValue', | |
| parent=self.styles['Normal'], | |
| fontSize=10, | |
| spaceAfter=4, | |
| textColor=colors.black, | |
| fontName='Courier' | |
| )) | |
| # Style copyright | |
| self.styles.add(ParagraphStyle( | |
| name='Copyright', | |
| parent=self.styles['Normal'], | |
| fontSize=10, | |
| alignment=TA_CENTER, | |
| textColor=colors.gray, | |
| fontName='Helvetica' | |
| )) | |
| # Style conclusión | |
| self.styles.add(ParagraphStyle( | |
| name='Conclusion', | |
| parent=self.styles['Normal'], | |
| fontSize=11, | |
| spaceAfter=10, | |
| leading=14, | |
| textColor=colors.black, | |
| fontName='Helvetica' | |
| )) | |
| def create_header(self): | |
| """Crée l'en-tête du rapport""" | |
| elements = [] | |
| # Logo/title block | |
| elements.append(Paragraph("🤖 Jumeaux Numériques", self.styles['MainTitle'])) | |
| elements.append(Paragraph("Simulation de Robot Souple", self.styles['SubTitle'])) | |
| elements.append(Spacer(1, 20)) | |
| elements.append(Paragraph("École Centrale Lyon ENISE", self.styles['Normal'])) | |
| elements.append(Spacer(1, 10)) | |
| elements.append(Paragraph(f"Date: {time.strftime('%d/%m/%Y à %H:%M')}", self.styles['Normal'])) | |
| elements.append(Spacer(1, 20)) | |
| # Ligne de séparation | |
| line = Drawing(500, 2) | |
| line.add(Rect(0, 0, 500, 2, fillColor=colors.HexColor('#667eea'))) | |
| elements.append(line) | |
| elements.append(Spacer(1, 20)) | |
| return elements | |
| def create_section(self, title): | |
| """Crée un en-tête de section""" | |
| elements = [] | |
| elements.append(Spacer(1, 15)) | |
| elements.append(Paragraph(title, self.styles['SectionHeader'])) | |
| return elements | |
| def create_metrics_table(self, metrics_dict, title="Métriques de Performance"): | |
| """Crée un tableau de métriques professionnel""" | |
| elements = [] | |
| elements.extend(self.create_section(title)) | |
| # Préparer les données | |
| data = [] | |
| for key, value in metrics_dict.items(): | |
| data.append([Paragraph(f"<b>{key}</b>", self.styles['MetricLabel']), | |
| Paragraph(str(value), self.styles['MetricValue'])]) | |
| # Créer le tableau | |
| table = Table(data, colWidths=[10*cm, 8*cm]) | |
| table.setStyle(TableStyle([ | |
| ('BACKGROUND', (0, 0), (-1, -1), colors.HexColor('#f5f7fa')), | |
| ('TEXTCOLOR', (0, 0), (0, -1), colors.HexColor('#1e3c72')), | |
| ('TEXTCOLOR', (1, 0), (1, -1), colors.black), | |
| ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'), | |
| ('FONTNAME', (1, 0), (1, -1), 'Courier'), | |
| ('FONTSIZE', (0, 0), (-1, -1), 10), | |
| ('BOTTOMPADDING', (0, 0), (-1, -1), 8), | |
| ('TOPPADDING', (0, 0), (-1, -1), 8), | |
| ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#ddd')), | |
| ('ROUNDEDCORNERS', (0, 0), (-1, -1), 5), | |
| ('SHADOW', (0, 0), (-1, -1), 1, (3, 3), colors.lightgrey), | |
| ])) | |
| elements.append(table) | |
| elements.append(Spacer(1, 20)) | |
| return elements | |
| def create_parameters_table(self, params_dict, title="Paramètres de Simulation"): | |
| """Crée un tableau des paramètres""" | |
| elements = [] | |
| elements.extend(self.create_section(title)) | |
| data = [] | |
| for key, value in params_dict.items(): | |
| formatted_value = f"{value:.6f}" if isinstance(value, float) else str(value) | |
| data.append([Paragraph(f"<b>{key}</b>", self.styles['Normal']), | |
| Paragraph(formatted_value, self.styles['MetricValue'])]) | |
| table = Table(data, colWidths=[8*cm, 8*cm]) | |
| table.setStyle(TableStyle([ | |
| ('BACKGROUND', (0, 0), (-1, -1), colors.HexColor('#e8f4e8')), | |
| ('TEXTCOLOR', (0, 0), (-1, -1), colors.HexColor('#2e7d32')), | |
| ('FONTNAME', (0, 0), (-1, -1), 'Helvetica'), | |
| ('FONTNAME', (1, 0), (-1, -1), 'Courier'), | |
| ('FONTSIZE', (0, 0), (-1, -1), 10), | |
| ('BOTTOMPADDING', (0, 0), (-1, -1), 6), | |
| ('TOPPADDING', (0, 0), (-1, -1), 6), | |
| ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#a5d6a7')), | |
| ])) | |
| elements.append(table) | |
| elements.append(Spacer(1, 20)) | |
| return elements | |
| def create_plot_image(self, fig, title, width=18*cm): | |
| """Ajoute un plot matplotlib au rapport""" | |
| elements = [] | |
| elements.extend(self.create_section(title)) | |
| # Sauvegarder le plot dans un fichier temporaire | |
| with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmpfile: | |
| fig.savefig(tmpfile.name, dpi=150, bbox_inches='tight', | |
| facecolor='white', edgecolor='none') | |
| tmpfile_path = tmpfile.name | |
| try: | |
| img = Image(tmpfile_path, width=width, height=width*0.6) | |
| img.hAlign = 'CENTER' | |
| elements.append(img) | |
| elements.append(Spacer(1, 15)) | |
| finally: | |
| if os.path.exists(tmpfile_path): | |
| os.remove(tmpfile_path) | |
| return elements | |
| def create_conclusion(self, simulation_type): | |
| """Crée la conclusion du rapport""" | |
| elements = [] | |
| elements.extend(self.create_section("Conclusion et Observations")) | |
| conclusions = { | |
| 'open': """ | |
| <b>Simulation en Boucle Ouverte</b><br/><br/> | |
| Cette simulation met en évidence le comportement dynamique naturel du système sans aucune rétroaction. | |
| Le déplacement du point B (tip) répond à la sollicitation appliquée au point A avec un retard de | |
| propagation caractéristique des systèmes distribués.<br/><br/> | |
| <b>Observations clés :</b><br/> | |
| • Le système présente une réponse transitoire avant stabilisation<br/> | |
| • Les perturbations appliquées en A n'affectent pas directement B (découplage)<br/> | |
| • L'énergie totale du système tend vers un régime permanent<br/> | |
| • Le temps de stabilisation dépend des propriétés mécaniques de la poutre | |
| """, | |
| 'pid': """ | |
| <b>Contrôle PID en Boucle Fermée</b><br/><br/> | |
| L'asservissement PID permet un suivi de consigne précis et robuste.<br/><br/> | |
| <b>Effets des paramètres :</b><br/> | |
| • <b>Kp</b> (Proportionnel) : Augmente la réactivité mais peut créer des oscillations<br/> | |
| • <b>Ki</b> (Intégral) : Élimine l'erreur statique mais peut ralentir la réponse<br/> | |
| • <b>Kd</b> (Dérivé) : Améliore la stabilité et réduit le dépassement<br/><br/> | |
| <b>Recommandations :</b><br/> | |
| • Ajuster Kp pour la réactivité souhaitée<br/> | |
| • Ajouter Ki seulement si nécessaire pour l'erreur statique<br/> | |
| • Utiliser Kd pour réduire les oscillations | |
| """, | |
| 'kalman': """ | |
| <b>Estimation par Filtre de Kalman</b><br/><br/> | |
| Le filtre de Kalman fournit une estimation optimale de l'état du système en présence de bruit.<br/><br/> | |
| <b>Fonctionnement :</b><br/> | |
| • <b>Prédiction</b> : Estimation basée sur le modèle physique (jumeau numérique)<br/> | |
| • <b>Correction</b> : Ajustement basé sur les mesures bruitées disponibles<br/> | |
| • <b>Covariance</b> : Mesure de l'incertitude de l'estimation<br/><br/> | |
| <b>Cas sans mesure directe :</b><br/> | |
| L'estimation repose uniquement sur le modèle (prédiction pure), | |
| l'incertitude augmente avec le temps jusqu'à divergence. | |
| """ | |
| } | |
| elements.append(Paragraph(conclusions.get(simulation_type, ""), self.styles['Conclusion'])) | |
| elements.append(Spacer(1, 20)) | |
| return elements | |
| def create_footer(self): | |
| """Crie le pied de page""" | |
| elements = [] | |
| elements.append(Spacer(1, 30)) | |
| line = Drawing(500, 1) | |
| line.add(Rect(0, 0, 500, 1, fillColor=colors.HexColor('#667eea'))) | |
| elements.append(line) | |
| elements.append(Spacer(1, 10)) | |
| elements.append(Paragraph("© 2026 École Centrale Lyon ENISE - Tous droits réservés", | |
| self.styles['Copyright'])) | |
| elements.append(Paragraph("Jumeaux Numériques : Simulation de Robot Souple", | |
| self.styles['Copyright'])) | |
| return elements | |
| def generate_report(self, simulation_type, parameters, results, figures, pid_metrics=None, kalman_metrics=None): | |
| """Génère le rapport PDF complet""" | |
| # Type de simulation | |
| type_titles = { | |
| 'open': 'Simulation en Boucle Ouverte', | |
| 'pid': 'Contrôle PID en Boucle Fermée', | |
| 'kalman': 'Estimation par Filtre de Kalman' | |
| } | |
| # Créer un buffer pour le PDF | |
| buffer = BytesIO() | |
| doc = SimpleDocTemplate( | |
| buffer, | |
| pagesize=A4, | |
| rightMargin=2*cm, | |
| leftMargin=2*cm, | |
| topMargin=2*cm, | |
| bottomMargin=2*cm | |
| ) | |
| elements = [] | |
| # En-tête | |
| elements.extend(self.create_header()) | |
| # Type de simulation | |
| elements.append(Paragraph(type_titles.get(simulation_type, 'Simulation'), | |
| self.styles['SubTitle'])) | |
| elements.append(Spacer(1, 20)) | |
| # Paramètres | |
| elements.extend(self.create_parameters_table(parameters)) | |
| # Métriques | |
| if 'metrics' in results: | |
| elements.extend(self.create_metrics_table(results['metrics'])) | |
| # Métriques PID spécifiques | |
| if simulation_type == 'pid' and pid_metrics: | |
| elements.extend(self.create_metrics_table(pid_metrics, "Paramètres PID")) | |
| # Métriques Kalman spécifiques | |
| if simulation_type == 'kalman' and kalman_metrics: | |
| elements.extend(self.create_metrics_table(kalman_metrics, "Métriques Kalman")) | |
| # Plots - nouvelle page | |
| elements.append(PageBreak()) | |
| elements.append(Paragraph("Visualisations", self.styles['SubTitle'])) | |
| for i, (fig, title) in enumerate(figures): | |
| if i > 0: | |
| elements.append(PageBreak()) | |
| elements.extend(self.create_plot_image(fig, title)) | |
| # Conclusion | |
| elements.append(PageBreak()) | |
| elements.extend(self.create_conclusion(simulation_type)) | |
| # Pied de page | |
| elements.extend(self.create_footer()) | |
| # Génération du PDF | |
| doc.build(elements) | |
| # Retourner les bytes | |
| pdf_bytes = buffer.getvalue() | |
| buffer.close() | |
| return pdf_bytes | |
| def generate_pdf_report(simulation_type, parameters, results, figures, pid_metrics=None, kalman_metrics=None): | |
| """Fonction wrapper pour générer un rapport PDF""" | |
| generator = PDFReportGenerator() | |
| doc = generator.generate_report( | |
| simulation_type, parameters, results, figures, | |
| pid_metrics=pid_metrics, kalman_metrics=kalman_metrics | |
| ) | |
| return doc | |
| def run_kalman_simulation(nx, delta, q, r, without_measure): | |
| """Simulation complète avec filtre de Kalman""" | |
| start_time = time.time() | |
| try: | |
| # Paramètres du système | |
| beta = 0.25 | |
| gamma = 0.5 | |
| L = 500 | |
| a = 5 | |
| b = 5 | |
| E = 70000 | |
| rho = 2700e-9 | |
| cy = 1e-4 | |
| nstep = min(500, int(5 / delta)) | |
| nA = int(np.floor(nx / 2)) | |
| S = a * b | |
| Ix = a * b**3 / 12 | |
| dx = L / nx | |
| time0 = np.linspace(0, delta * nstep, nstep + 1) | |
| ixe = np.linspace(dx, L, nx) | |
| # Bruits | |
| vseed_noise = 42 if not without_measure else None | |
| mpert = generateNoiseTemporal(time0, 1, r, vseed_noise) if r > 0 else np.zeros(len(time0)) | |
| # Matrices système | |
| Kfull, Cfull, Mfull = cached_matrices(nx, dx, E, Ix, rho, S, cy) | |
| ndo1 = Kfull.shape[0] - 2 | |
| K = Kfull[2:, 2:] | |
| C = Cfull[2:, 2:] | |
| M = Mfull[2:, 2:] | |
| induB = 2 * nx - 2 | |
| n_state = 2 * ndo1 # État = [u; v] | |
| # Construction des matrices d'état (Newmark) | |
| # x_k+1 = A * x_k + B * f_k | |
| # y_k = H * x_k + bruit | |
| # Matrice de transition A (discrétisée) | |
| nx_size = ndo1 | |
| I_matrix = np.eye(nx_size) | |
| zero_matrix = np.zeros((nx_size, nx_size)) | |
| # Approximation de la matrice d'état pour Newmark | |
| A11 = I_matrix | |
| A12 = delta * I_matrix | |
| A21 = -delta * np.linalg.solve(M, K) | |
| A22 = I_matrix - delta * np.linalg.solve(M, C) | |
| A = np.block([[A11, A12], [A21, A22]]) | |
| # Matrice d'entrée B | |
| B_input = np.zeros((n_state, ndo1)) | |
| B_input[ndo1:, :] = delta * np.linalg.solve(M, I_matrix) # Accélération | |
| # Matrice d'observation (mesure de u_B uniquement) | |
| H = np.zeros((1, n_state)) | |
| H[0, induB] = 1.0 | |
| # Matrices de covariance | |
| Q = np.eye(n_state) * q * 10 # Bruit de processus | |
| R = np.array([[r]]) # Bruit de mesure | |
| # Initialisation | |
| P = np.eye(n_state) * 1e-3 # Covariance initiale | |
| x_est = np.zeros(n_state) # État estimé initial | |
| # Simulation réelle du système | |
| u_sim = np.zeros((ndo1, len(time0))) | |
| v_sim = np.zeros((ndo1, len(time0))) | |
| # Sollicitation échelon | |
| fA = Heaviside(time0 - 1) | |
| f = np.zeros((ndo1, len(time0))) | |
| f[induB//2, :] = fA * 10 # Force au point B | |
| # Conditions initiales | |
| u_sim[:, 0] = np.zeros(ndo1) | |
| v_sim[:, 0] = np.zeros(ndo1) | |
| # Stockage des estimations | |
| estimates = [] | |
| measurements = [] | |
| covariances = [] | |
| # Simulation et estimation Kalman | |
| for step in range(1, len(time0)): | |
| # Simulation physique | |
| f_k = f[:, step] | |
| u_new, v_new, _ = newmark1stepMRHS( | |
| M, C, K, f_k, u_sim[:, step-1], v_sim[:, step-1], | |
| np.zeros(ndo1), delta, beta, gamma | |
| ) | |
| u_sim[:, step] = u_new | |
| v_sim[:, step] = v_new | |
| # Mesure bruitée | |
| z = u_sim[induB, step] + mpert[step] | |
| measurements.append(z) | |
| # Prédiction Kalman | |
| x_pred = A @ x_est | |
| P_pred = A @ P @ A.T + Q | |
| # Mise à jour Kalman | |
| if not without_measure: | |
| K_kal = P_pred @ H.T @ np.linalg.inv(H @ P_pred @ H.T + R) | |
| y = z - H @ x_pred | |
| x_est = x_pred + K_kal @ y | |
| P = (np.eye(n_state) - K_kal @ H) @ P_pred | |
| else: | |
| x_est = x_pred | |
| P = P_pred | |
| estimates.append(x_est[induB]) | |
| covariances.append(np.sqrt(P[induB, induB])) | |
| # Métriques Kalman | |
| uB_true = u_sim[induB, 1:] | |
| estimates_arr = np.array(estimates) | |
| rmse = np.sqrt(np.mean((estimates_arr - uB_true)**2)) | |
| max_cov = max(covariances) if covariances else 0 | |
| mean_cov = np.mean(covariances) if covariances else 0 | |
| improvement = np.mean(np.abs(estimates_arr - uB_true)) / np.mean(np.abs(mpert[1:])) if r > 0 else 0 | |
| # Création des figures | |
| fig = plt.figure(figsize=(16, 12)) | |
| fig.patch.set_facecolor('#f8f9fa') | |
| # 1. Estimation vs Vérité terrain | |
| ax1 = plt.subplot(2, 3, 1) | |
| ax1.plot(time0[1:], uB_true, 'b-', linewidth=2, label='Vérité terrain (mesure)', alpha=0.8) | |
| ax1.plot(time0[1:], estimates_arr, 'r--', linewidth=2, label='Estimation Kalman', alpha=0.8) | |
| if without_measure: | |
| ax1.fill_between(time0[1:], | |
| estimates_arr - 2*np.array(covariances), | |
| estimates_arr + 2*np.array(covariances), | |
| alpha=0.2, color='red', label='±2σ') | |
| ax1.set_xlabel('Temps (s)', fontsize=11, fontweight='bold') | |
| ax1.set_ylabel('Déplacement u_B (m)', fontsize=11, fontweight='bold') | |
| ax1.set_title('Estimation vs Vérité Terrain', fontsize=13, fontweight='bold', pad=15) | |
| ax1.legend(loc='best', fontsize=10) | |
| ax1.grid(True, alpha=0.3, linestyle='--') | |
| ax1.set_facecolor('#fafafa') | |
| # 2. Erreur d'estimation | |
| ax2 = plt.subplot(2, 3, 2) | |
| estimation_error = estimates_arr - uB_true | |
| ax2.plot(time0[1:], estimation_error, 'purple', linewidth=2, alpha=0.8) | |
| ax2.axhline(y=0, color='green', linestyle='--', alpha=0.5) | |
| ax2.fill_between(time0[1:], -2*np.array(covariances), 2*np.array(covariances), | |
| alpha=0.2, color='orange', label='Bande incertitude 2σ') | |
| ax2.set_xlabel('Temps (s)', fontsize=11, fontweight='bold') | |
| ax2.set_ylabel('Erreur (m)', fontsize=11, fontweight='bold') | |
| ax2.set_title('Erreur d\'Estimation', fontsize=13, fontweight='bold', pad=15) | |
| ax2.legend(loc='best', fontsize=10) | |
| ax2.grid(True, alpha=0.3, linestyle='--') | |
| ax2.set_facecolor('#fafafa') | |
| # 3. Évolution de la covariance | |
| ax3 = plt.subplot(2, 3, 3) | |
| ax3.plot(time0[1:], covariances, 'orange', linewidth=2, alpha=0.8) | |
| ax3.fill_between(time0[1:], 0, covariances, alpha=0.3, color='orange') | |
| ax3.set_xlabel('Temps (s)', fontsize=11, fontweight='bold') | |
| ax3.set_ylabel('Écart-type σ (m)', fontsize=11, fontweight='bold') | |
| ax3.set_title('Évolution de l\'Incertitude', fontsize=13, fontweight='bold', pad=15) | |
| ax3.grid(True, alpha=0.3, linestyle='--') | |
| ax3.set_facecolor('#fafafa') | |
| # 4. Densité de l'erreur (si assez de données) | |
| ax4 = plt.subplot(2, 3, 4) | |
| if len(estimation_error) > 100 and not np.any(np.isnan(estimation_error)) and not np.any(np.isinf(estimation_error)): | |
| from scipy import stats | |
| # Nettoyer les données | |
| clean_error = estimation_error[~np.isnan(estimation_error) & ~np.isinf(estimation_error)] | |
| if len(clean_error) > 100: | |
| kde = stats.gaussian_kde(clean_error) | |
| x_kde = np.linspace(min(clean_error), max(clean_error), 200) | |
| ax4.fill_between(x_kde, kde(x_kde), alpha=0.5, color='purple') | |
| ax4.plot(x_kde, kde(x_kde), 'purple', linewidth=2) | |
| ax4.axvline(x=0, color='green', linestyle='--', alpha=0.7) | |
| ax4.set_xlabel('Erreur (m)', fontsize=11, fontweight='bold') | |
| ax4.set_ylabel('Densité de probabilité', fontsize=11, fontweight='bold') | |
| ax4.set_title('Distribution de l\'Erreur (Gaussienne)', fontsize=13, fontweight='bold', pad=15) | |
| ax4.grid(True, alpha=0.3, linestyle='--') | |
| else: | |
| ax4.text(0.5, 0.5, 'Données insuffisantes\npour estimation', transform=ax4.transAxes, | |
| ha='center', va='center', fontsize=12) | |
| ax4.set_title('Distribution de l\'Erreur', fontsize=13, fontweight='bold', pad=15) | |
| else: | |
| ax4.text(0.5, 0.5, 'Données insuffisantes\nou erreur nulle', transform=ax4.transAxes, | |
| ha='center', va='center', fontsize=12) | |
| ax4.set_title('Distribution de l\'Erreur', fontsize=13, fontweight='bold', pad=15) | |
| ax4.set_facecolor('#fafafa') | |
| # 5. Analyse spectrale | |
| ax5 = plt.subplot(2, 3, 5) | |
| freqs, psd_true = signal.welch(uB_true, fs=1/delta, nperseg=min(128, len(uB_true)//4)) | |
| freqs_est, psd_est = signal.welch(estimates_arr, fs=1/delta, nperseg=min(128, len(estimates_arr)//4)) | |
| ax5.semilogy(freqs[:len(freqs)//2], psd_true[:len(freqs)//2], 'b-', linewidth=2, label='Vérité terrain') | |
| ax5.semilogy(freqs_est[:len(freqs_est)//2], psd_est[:len(freqs_est)//2], 'r--', linewidth=2, label='Estimation') | |
| ax5.set_xlabel('Fréquence (Hz)', fontsize=11, fontweight='bold') | |
| ax5.set_ylabel('Densité spectrale', fontsize=11, fontweight='bold') | |
| ax5.set_title('Analyse Spectrale', fontsize=13, fontweight='bold', pad=15) | |
| ax5.legend(loc='best', fontsize=10) | |
| ax5.grid(True, alpha=0.3, linestyle='--') | |
| ax5.set_facecolor('#fafafa') | |
| # 6. Métriques | |
| ax6 = plt.subplot(2, 3, 6) | |
| ax6.axis('off') | |
| kalman_metrics = [ | |
| ('RMSE', f'{rmse:.6f}', '#e3f2fd'), | |
| ('Covariance max', f'{max_cov:.6f}', '#f3e5f5'), | |
| ('Covariance moy', f'{mean_cov:.6f}', '#e8f5e8'), | |
| ('Sans mesure', 'Oui' if without_measure else 'Non', '#fff3e0'), | |
| ('Points estimé', f'{len(estimates)}', '#fce4ec'), | |
| ('Temps calc', f'{time.time()-start_time:.2f}s', '#e0f2f1') | |
| ] | |
| metrics_text = 'FILTRE DE KALMAN\n' + '='*28 + '\n\n' | |
| for label, value, color in kalman_metrics: | |
| metrics_text += f'{label:18s} : {value}\n' | |
| ax6.text(0.5, 0.5, metrics_text, transform=ax6.transAxes, | |
| fontsize=12, fontfamily='monospace', va='center', ha='center', | |
| bbox=dict(boxstyle='round,pad=0.8', facecolor='#f0f0f5', | |
| edgecolor='#667eea', linewidth=2)) | |
| plt.tight_layout(pad=3.0) | |
| plt.subplots_adjust(top=0.93, hspace=0.35, wspace=0.25) | |
| fig.suptitle('🔍 FILTRE DE KALMAN - ESTIMATION D\'ÉTAT', | |
| fontsize=18, fontweight='bold', color='#1e3c72', y=0.98, | |
| bbox=dict(boxstyle="round,pad=0.7", facecolor='lightblue', alpha=0.8)) | |
| # Export des données | |
| data_df = pd.DataFrame({ | |
| 'Temps (s)': time0[1:], | |
| 'Vérité u_B (m)': uB_true, | |
| 'Estimation (m)': estimates_arr, | |
| 'Erreur (m)': estimation_error, | |
| 'Covariance': covariances | |
| }) | |
| metrics = { | |
| 'RMSE': f'{rmse:.6f}', | |
| 'Covariance max': f'{max_cov:.6f}', | |
| 'Sans mesure': 'Oui' if without_measure else 'Non' | |
| } | |
| return fig, data_df, metrics | |
| except Exception as e: | |
| st.error(f"Erreur dans la simulation Kalman: {str(e)}") | |
| import traceback | |
| st.error(traceback.format_exc()) | |
| return None, None, None | |
| def run_open_loop_simulation(nx, delta, q, solicitation_type, with_perturbation): | |
| """Simulation boucle ouverte""" | |
| start_time = time.time() | |
| try: | |
| beta = 0.25 | |
| gamma = 0.5 | |
| L = 500 | |
| a = 5 | |
| b = 5 | |
| E = 70000 | |
| rho = 2700e-9 | |
| cy = 1e-4 | |
| nstep = min(500, int(5 / delta)) | |
| nA = int(np.floor(nx / 2)) | |
| S = a * b | |
| Ix = a * b**3 / 12 | |
| dx = L / nx | |
| time0 = np.linspace(0, delta * nstep, nstep + 1) | |
| ixe = np.linspace(dx, L, nx) | |
| vseed = 42 if not with_perturbation else None | |
| fpert = generateNoiseTemporal(time0, 1, q, vseed) if with_perturbation else np.zeros(len(time0)) | |
| Kfull, Cfull, Mfull = cached_matrices(nx, dx, E, Ix, rho, S, cy) | |
| ndo1 = Kfull.shape[0] - 2 | |
| K = Kfull[2:, 2:] | |
| C = Cfull[2:, 2:] | |
| M = Mfull[2:, 2:] | |
| induA = 2 * nA - 2 | |
| induB = 2 * nx - 2 | |
| if solicitation_type == "échelon": | |
| fA = Heaviside(time0 - 1) | |
| elif solicitation_type == "sinusoïdale": | |
| fA = np.sin(2 * np.pi * 0.1 * time0) | |
| else: | |
| fA = np.zeros_like(time0) | |
| fA[time0 > 1] = 1 | |
| f = np.zeros((ndo1, len(time0))) | |
| f[induA, :] = fA + fpert | |
| u0 = np.zeros(ndo1) | |
| v0 = np.zeros(ndo1) | |
| a0 = np.linalg.solve(M, f[:, 0] - C @ v0 - K @ u0) | |
| u, v, a = Newmark(M, C, K, f, u0, v0, a0, delta, beta, gamma) | |
| uB_final = u[induB, -1] | |
| threshold = 0.01 * abs(uB_final) | |
| stable_idx = np.where(np.abs(u[induB, :] - uB_final) < threshold)[0] | |
| settling_time = time0[stable_idx[0]] if len(stable_idx) > 0 else time0[-1] | |
| delay = 0.0 | |
| if solicitation_type == "échelon": | |
| threshold_A = 0.1 * np.max(np.abs(u[induA, :])) | |
| threshold_B = 0.1 * np.max(np.abs(u[induB, :])) | |
| rise_time_A = time0[np.where(np.abs(u[induA, :]) > threshold_A)[0][0]] | |
| rise_time_B = time0[np.where(np.abs(u[induB, :]) > threshold_B)[0][0]] if np.any(np.abs(u[induB, :]) > threshold_B) else time0[-1] | |
| delay = rise_time_B - rise_time_A | |
| # Figures | |
| plt.style.use('seaborn-v0_8-whitegrid') | |
| fig = plt.figure(figsize=(16, 10)) | |
| fig.patch.set_facecolor('#f8f9fa') | |
| colors = {'ub': '#1f77b4', 'consigne': '#d62728', 'pert': '#2ca02c', 'ua': '#ff7f0e'} | |
| # 1. Déplacement temporel | |
| ax1 = plt.subplot(2, 3, 1) | |
| ax1.plot(time0, u[induB, :], color=colors['ub'], linewidth=3, label='u_B (tip)', alpha=0.9) | |
| ax1.plot(time0, fA, color=colors['consigne'], linewidth=2.5, linestyle='--', label='Consigne f_A', alpha=0.8) | |
| if with_perturbation: | |
| ax1.plot(time0, fpert, color=colors['pert'], linewidth=1.5, linestyle=':', label='Perturbation', alpha=0.7) | |
| ax1.fill_between(time0, 0, fpert, alpha=0.2, color=colors['pert']) | |
| max_idx = np.argmax(np.abs(u[induB, :])) | |
| ax1.plot(time0[max_idx], u[induB, max_idx], 'ro', markersize=8, label=f'Max: {u[induB, max_idx]:.4f}') | |
| ax1.set_xlabel('Temps (s)', fontsize=12, fontweight='bold') | |
| ax1.set_ylabel('Déplacement (m)', fontsize=12, fontweight='bold') | |
| ax1.set_title('📈 Déplacement Temporel du Tip', fontsize=14, fontweight='bold', pad=20) | |
| ax1.legend(loc='best', fontsize=11, framealpha=0.9) | |
| ax1.grid(True, alpha=0.3, linestyle='--') | |
| ax1.set_facecolor('#fafafa') | |
| # 2. Déformée finale | |
| ax2 = plt.subplot(2, 3, 2) | |
| u_final = u[::2, -1] | |
| ixe_plot = np.linspace(dx, L, len(u_final)) | |
| ax2.plot(ixe_plot, u_final, color=colors['ub'], linewidth=3, alpha=0.9) | |
| ax2.fill_between(ixe_plot, 0, u_final, alpha=0.3, color=colors['ub']) | |
| ax2.scatter(ixe_plot, u_final, color=colors['ub'], s=30, zorder=5, label='Points de calcul') | |
| ax2.set_xlabel('Position x (m)', fontsize=12, fontweight='bold') | |
| ax2.set_ylabel('Déplacement u (m)', fontsize=12, fontweight='bold') | |
| ax2.set_title('🏗️ Déformée Finale', fontsize=14, fontweight='bold', pad=20) | |
| ax2.legend(loc='best', fontsize=10) | |
| ax2.grid(True, alpha=0.3, linestyle='--') | |
| ax2.set_facecolor('#fafafa') | |
| # 3. Analyse fréquentielle | |
| ax3 = plt.subplot(2, 3, 3) | |
| freqs = np.fft.fftfreq(len(time0), delta) | |
| fft_uB = np.abs(np.fft.fft(u[induB, :])) | |
| pos_freqs = freqs[:len(freqs)//2] | |
| pos_fft = fft_uB[:len(fft_uB)//2] | |
| ax3.semilogy(pos_freqs[1:], pos_fft[1:], color=colors['ub'], linewidth=2, alpha=0.8) | |
| ax3.set_xlabel('Fréquence (Hz)', fontsize=12, fontweight='bold') | |
| ax3.set_ylabel('Amplitude', fontsize=12, fontweight='bold') | |
| ax3.set_title('📊 Analyse Fréquentielle', fontsize=14, fontweight='bold', pad=20) | |
| ax3.grid(True, alpha=0.3, linestyle='--') | |
| ax3.set_facecolor('#fafafa') | |
| # 4. Énergie | |
| ax4 = plt.subplot(2, 3, 4) | |
| energie_cinetique = 0.5 * np.sum(v * (M @ v), axis=0) | |
| energie_potentielle = 0.5 * np.sum(u * (K @ u), axis=0) | |
| energie_totale = energie_cinetique + energie_potentielle | |
| ax4.plot(time0, energie_cinetique, 'r-', linewidth=2, label='Cinétique') | |
| ax4.plot(time0, energie_potentielle, 'b-', linewidth=2, label='Potentielle') | |
| ax4.plot(time0, energie_totale, 'k--', linewidth=2, label='Totale') | |
| ax4.set_xlabel('Temps (s)', fontsize=12, fontweight='bold') | |
| ax4.set_ylabel('Énergie (J)', fontsize=12, fontweight='bold') | |
| ax4.set_title('⚡ Évolution Énergétique', fontsize=14, fontweight='bold', pad=20) | |
| ax4.legend(loc='best', fontsize=10) | |
| ax4.grid(True, alpha=0.3, linestyle='--') | |
| ax4.set_facecolor('#fafafa') | |
| # 5. Comparaison A vs B | |
| ax5 = plt.subplot(2, 3, 5) | |
| ax5.plot(time0, u[induA, :], color=colors['ua'], linewidth=2.5, alpha=0.8, label='Point A') | |
| ax5.plot(time0, u[induB, :], color=colors['ub'], linewidth=2.5, alpha=0.8, label='Point B') | |
| ax5.set_xlabel('Temps (s)', fontsize=12, fontweight='bold') | |
| ax5.set_ylabel('Déplacement (m)', fontsize=12, fontweight='bold') | |
| ax5.set_title('🔄 Point A vs Point B', fontsize=14, fontweight='bold', pad=20) | |
| ax5.legend(loc='best', fontsize=10) | |
| ax5.grid(True, alpha=0.3, linestyle='--') | |
| ax5.set_facecolor('#fafafa') | |
| # 6. Métriques | |
| ax6 = plt.subplot(2, 3, 6) | |
| ax6.axis('off') | |
| metrics_text = 'BOUCLE OUVERTE\n' + '='*28 + '\n\n' | |
| metrics_data = [ | |
| ('Déplacement max', f'{np.max(np.abs(u[induB, :])):.6f}'), | |
| ('Temps stabil.', f'{settling_time:.3f}s'), | |
| ('Retard prop.', f'{delay:.3f}s'), | |
| ('Énergie finale', f'{energie_totale[-1]:.2e}'), | |
| ('Perturbation', 'Oui' if with_perturbation else 'Non'), | |
| ('Temps calcul', f'{time.time()-start_time:.2f}s') | |
| ] | |
| for label, value in metrics_data: | |
| metrics_text += f'{label:18s} : {value}\n' | |
| ax6.text(0.5, 0.5, metrics_text, transform=ax6.transAxes, | |
| fontsize=12, fontfamily='monospace', va='center', ha='center', | |
| bbox=dict(boxstyle='round,pad=0.8', facecolor='#f0f0f5', | |
| edgecolor='#667eea', linewidth=2)) | |
| plt.tight_layout(pad=3.0) | |
| plt.subplots_adjust(top=0.93, hspace=0.35, wspace=0.25) | |
| fig.suptitle('🔓 SIMULATION BOUCLE OUVERTE', | |
| fontsize=18, fontweight='bold', color='#1e3c72', y=0.98, | |
| bbox=dict(boxstyle="round,pad=0.7", facecolor='lightblue', alpha=0.8)) | |
| data_df = pd.DataFrame({ | |
| 'Temps (s)': time0, | |
| 'u_A (m)': u[induA, :], | |
| 'u_B (m)': u[induB, :], | |
| 'Consigne': fA | |
| }) | |
| metrics = { | |
| 'Déplacement max': f'{np.max(np.abs(u[induB, :])):.6f}', | |
| 'Temps stabilisation': f'{settling_time:.3f}', | |
| 'Retard': f'{delay:.3f}' | |
| } | |
| return fig, data_df, metrics | |
| except Exception as e: | |
| st.error(f"Erreur: {str(e)}") | |
| import traceback | |
| st.error(traceback.format_exc()) | |
| return None, None, None | |
| def run_pid_simulation(nx, delta, r, Kp, Ki, Kd, with_noise, with_perturbation, compare_open): | |
| """Simulation PID""" | |
| start_time = time.time() | |
| try: | |
| beta = 0.25 | |
| gamma = 0.5 | |
| L = 500 | |
| a = 5 | |
| b = 5 | |
| E = 70000 | |
| rho = 2700e-9 | |
| cy = 1e-4 | |
| nstep = min(500, int(5 / delta)) | |
| nA = int(np.floor(nx / 2)) | |
| S = a * b | |
| Ix = a * b**3 / 12 | |
| dx = L / nx | |
| time0 = np.linspace(0, delta * nstep, nstep + 1) | |
| ixe = np.linspace(dx, L, nx) | |
| vseed_pert = 42 if not with_perturbation else None | |
| vseed_noise = 123 if not with_noise else None | |
| fpert = generateNoiseTemporal(time0, 1, 1e-4, vseed_pert) if with_perturbation else np.zeros(len(time0)) | |
| mpert = generateNoiseTemporal(time0, 1, r, vseed_noise) if with_noise else np.zeros(len(time0)) | |
| Kfull, Cfull, Mfull = cached_matrices(nx, dx, E, Ix, rho, S, cy) | |
| ndo1 = Kfull.shape[0] - 2 | |
| K = Kfull[2:, 2:] | |
| C = Cfull[2:, 2:] | |
| M = Mfull[2:, 2:] | |
| induA = 2 * nA - 2 | |
| induB = 2 * nx - 2 | |
| u_ref = Heaviside(time0 - 1) | |
| u = np.zeros((ndo1, len(time0))) | |
| v = np.zeros((ndo1, len(time0))) | |
| a = np.zeros((ndo1, len(time0))) | |
| u[:, 0] = np.zeros(ndo1) | |
| v[:, 0] = np.zeros(ndo1) | |
| a[:, 0] = np.linalg.solve(M, -C @ v[:, 0] - K @ u[:, 0]) | |
| integ_e = 0 | |
| prev_e = 0 | |
| errors = [] | |
| commandes = [] | |
| for step in range(1, len(time0)): | |
| uB_meas = u[induB, step-1] + mpert[step-1] | |
| e = u_ref[step] - uB_meas | |
| integ_e += e * delta | |
| de = (e - prev_e) / delta | |
| Fpid = Kp * e + Ki * integ_e + Kd * de | |
| f_k = np.zeros(ndo1) | |
| f_k[induA] = Fpid + fpert[step] | |
| u[:, step], v[:, step], a[:, step] = newmark1stepMRHS( | |
| M, C, K, f_k, u[:, step-1], v[:, step-1], a[:, step-1], delta, beta, gamma | |
| ) | |
| prev_e = e | |
| errors.append(e) | |
| commandes.append(Fpid) | |
| u_open = None | |
| if compare_open: | |
| fA_comp = Heaviside(time0 - 1) | |
| f_comp = np.zeros((ndo1, len(time0))) | |
| f_comp[induA, :] = fA_comp + fpert | |
| u_open, _, _ = Newmark(M, C, K, f_comp, np.zeros(ndo1), np.zeros(ndo1), | |
| np.linalg.solve(M, f_comp[:, 0]), delta, beta, gamma) | |
| plt.style.use('seaborn-v0_8-whitegrid') | |
| fig = plt.figure(figsize=(16, 10)) | |
| fig.patch.set_facecolor('#f8f9fa') | |
| colors = {'pid': '#1f77b4', 'open': '#2ca02c', 'ref': '#d62728', 'error': '#ff7f0e', 'command': '#9467bd'} | |
| # 1. Suivi de consigne | |
| ax1 = plt.subplot(2, 3, 1) | |
| ax1.plot(time0, u[induB, :], color=colors['pid'], linewidth=3, label='PID', alpha=0.9) | |
| if compare_open and u_open is not None: | |
| ax1.plot(time0, u_open[induB, :], color=colors['open'], linewidth=2.5, linestyle='--', label='Boucle ouverte', alpha=0.8) | |
| ax1.plot(time0, u_ref, color=colors['ref'], linewidth=2.5, linestyle='-.', label='Consigne', alpha=0.9) | |
| ax1.fill_between(time0, u[induB, :] - 0.001, u[induB, :] + 0.001, alpha=0.2, color=colors['pid']) | |
| ax1.set_xlabel('Temps (s)', fontsize=12, fontweight='bold') | |
| ax1.set_ylabel('Déplacement (m)', fontsize=12, fontweight='bold') | |
| ax1.set_title('🎯 Suivi de Consigne PID', fontsize=14, fontweight='bold', pad=20) | |
| ax1.legend(loc='best', fontsize=11, framealpha=0.9) | |
| ax1.grid(True, alpha=0.3, linestyle='--') | |
| ax1.set_facecolor('#fafafa') | |
| # 2. Erreur | |
| ax2 = plt.subplot(2, 3, 2) | |
| ax2.plot(time0[1:], errors, color=colors['error'], linewidth=2.5, alpha=0.8) | |
| ax2.axhline(y=0, color='green', linestyle='-', alpha=0.3, linewidth=1) | |
| ax2.fill_between(time0[1:], -0.001, 0.001, alpha=0.1, color='green') | |
| max_err_idx = np.argmax(np.abs(errors)) | |
| ax2.plot(time0[1:][max_err_idx], errors[max_err_idx], 'ro', markersize=8) | |
| ax2.set_xlabel('Temps (s)', fontsize=12, fontweight='bold') | |
| ax2.set_ylabel('Erreur (m)', fontsize=12, fontweight='bold') | |
| ax2.set_title('📊 Erreur de Suivi', fontsize=14, fontweight='bold', pad=20) | |
| ax2.grid(True, alpha=0.3, linestyle='--') | |
| ax2.set_facecolor('#fafafa') | |
| # 3. Commande | |
| ax3 = plt.subplot(2, 3, 3) | |
| ax3.plot(time0[1:], commandes, color=colors['command'], linewidth=2.5, alpha=0.8) | |
| ax3.axhline(y=10, color='red', linestyle=':', alpha=0.5) | |
| ax3.axhline(y=-10, color='red', linestyle=':', alpha=0.5) | |
| window_size = min(20, len(commandes)//4) | |
| if window_size > 1: | |
| moving_avg = np.convolve(commandes, np.ones(window_size)/window_size, mode='valid') | |
| time_avg = time0[window_size:len(moving_avg)+window_size] | |
| ax3.plot(time_avg, moving_avg, 'w-', linewidth=2, alpha=0.7) | |
| ax3.set_xlabel('Temps (s)', fontsize=12, fontweight='bold') | |
| ax3.set_ylabel('Commande F (N)', fontsize=12, fontweight='bold') | |
| ax3.set_title('⚙️ Commande PID', fontsize=14, fontweight='bold', pad=20) | |
| ax3.grid(True, alpha=0.3, linestyle='--') | |
| ax3.set_facecolor('#fafafa') | |
| # 4. Métriques | |
| ax4 = plt.subplot(2, 3, 4) | |
| ax4.axis('off') | |
| mse = np.mean(np.array(errors)**2) | |
| steady_state_error = np.mean(errors[-100:]) if len(errors) > 100 else np.mean(errors[-len(errors):]) | |
| rise_time_idx = np.where(np.abs(np.array(errors)) < 0.05 * max(np.abs(u_ref)))[0] | |
| rise_time = time0[rise_time_idx[0]] if len(rise_time_idx) > 0 else time0[-1] | |
| max_overshoot = max(errors) if max(errors) > 0 else 0 | |
| metrics_text = 'MÉTRIQUES PID\n' + '='*28 + '\n\n' | |
| metrics_data = [ | |
| ('MSE', f'{mse:.6f}'), | |
| ('Erreur régime', f'{steady_state_error:.6f}'), | |
| ('Temps réponse', f'{rise_time:.3f}s'), | |
| ('Dépassement', f'{max_overshoot:.6f}'), | |
| ('Erreur finale', f'{errors[-1]:.6f}'), | |
| ('Erreur RMS', f'{np.sqrt(mse):.6f}') | |
| ] | |
| for label, value in metrics_data: | |
| metrics_text += f'{label:18s} : {value}\n' | |
| ax4.text(0.5, 0.5, metrics_text, transform=ax4.transAxes, | |
| fontsize=12, fontfamily='monospace', va='center', ha='center', | |
| bbox=dict(boxstyle='round,pad=0.8', facecolor='#f0f0f5', | |
| edgecolor='#667eea', linewidth=2)) | |
| # 5. Déformée | |
| ax5 = plt.subplot(2, 3, 5) | |
| u_final_pid = u[::2, -1] | |
| ixe_pid = np.linspace(dx, L, len(u_final_pid)) | |
| ax5.plot(ixe_pid, u_final_pid, color=colors['pid'], linewidth=3, alpha=0.9) | |
| ax5.fill_between(ixe_pid, 0, u_final_pid, alpha=0.3, color=colors['pid']) | |
| if compare_open and u_open is not None: | |
| u_open_final = u_open[::2, -1] | |
| ixe_open = np.linspace(dx, L, len(u_open_final)) | |
| ax5.plot(ixe_open, u_open_final, color=colors['open'], linewidth=2.5, linestyle='--', alpha=0.7, label='Boucle ouverte') | |
| ax5.set_xlabel('Position x (m)', fontsize=12, fontweight='bold') | |
| ax5.set_ylabel('Déplacement (m)', fontsize=12, fontweight='bold') | |
| ax5.set_title('🏗️ Déformée Finale', fontsize=14, fontweight='bold', pad=20) | |
| ax5.legend(loc='best', fontsize=10) | |
| ax5.grid(True, alpha=0.3, linestyle='--') | |
| ax5.set_facecolor('#fafafa') | |
| # 6. Paramètres PID | |
| ax6 = plt.subplot(2, 3, 6) | |
| ax6.axis('off') | |
| pid_text = 'PARAMÈTRES PID\n' + '='*28 + '\n\n' | |
| pid_data = [ | |
| ('Kp (Proportionnel)', f'{Kp:.3f}'), | |
| ('Ki (Intégral)', f'{Ki:.3f}'), | |
| ('Kd (Dérivé)', f'{Kd:.4f}'), | |
| ('Bruit mesure', 'Oui' if with_noise else 'Non'), | |
| ('Perturbation', 'Oui' if with_perturbation else 'Non'), | |
| ('Comparaison', 'Oui' if compare_open else 'Non') | |
| ] | |
| for label, value in pid_data: | |
| pid_text += f'{label:20s} : {value}\n' | |
| ax6.text(0.5, 0.5, pid_text, transform=ax6.transAxes, | |
| fontsize=12, fontfamily='monospace', va='center', ha='center', | |
| bbox=dict(boxstyle='round,pad=0.8', facecolor='#e8f4e8', | |
| edgecolor='#38ef7d', linewidth=2)) | |
| plt.tight_layout(pad=3.0) | |
| plt.subplots_adjust(top=0.93, hspace=0.35, wspace=0.25) | |
| fig.suptitle('🔒 CONTRÔLE PID - BOUCLE FERMÉE', | |
| fontsize=18, fontweight='bold', color='#1e3c72', y=0.98, | |
| bbox=dict(boxstyle="round,pad=0.7", facecolor='lightblue', alpha=0.8)) | |
| data_df = pd.DataFrame({ | |
| 'Temps (s)': time0[1:], | |
| 'u_B (m)': u[induB, 1:], | |
| 'Consigne': u_ref[1:], | |
| 'Erreur': errors, | |
| 'Commande': commandes | |
| }) | |
| metrics = { | |
| 'MSE': f'{mse:.6f}', | |
| 'Temps réponse': f'{rise_time:.3f}', | |
| 'Erreur finale': f'{errors[-1]:.6f}' | |
| } | |
| return fig, data_df, metrics | |
| except Exception as e: | |
| st.error(f"Erreur: {str(e)}") | |
| import traceback | |
| st.error(traceback.format_exc()) | |
| return None, None, None | |
| # Sidebar | |
| with st.sidebar: | |
| st.header("🔧 Paramètres Globaux") | |
| nx_global = st.slider("Éléments (nx)", 5, 20, 10) | |
| delta_global = st.slider("Pas de temps (δt)", 0.001, 0.05, 0.01) | |
| q_global = st.slider("Variance perturbation (q)", 0.0, 0.01, 0.0001) | |
| r_global = st.slider("Variance bruit (r)", 0.0, 0.01, 0.0001) | |
| st.info(f"Fréquence: {1/delta_global:.1f}Hz | Durée: {5/delta_global:.1f}s") | |
| # Main tabs | |
| tab1, tab2, tab3, tab4 = st.tabs(["🔓 Boucle Ouverte", "🔒 PID", "🔍 Kalman", "📄 Rapport"]) | |
| with tab1: | |
| st.markdown('<div class="section-header">', unsafe_allow_html=True) | |
| st.markdown("### 🔓 Simulation en Boucle Ouverte") | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| solicitation = st.selectbox("Sollicitation", ["échelon", "sinusoïdale"]) | |
| with_pert = st.checkbox("Perturbation", value=True) | |
| with col2: | |
| nx_open = st.slider("nx", 5, 20, nx_global, key="nx_open") | |
| delta_open = st.slider("δt", 0.001, 0.05, delta_global, key="delta_open") | |
| q_open = st.slider("q", 0.0, 0.01, q_global, key="q_open") | |
| if st.button("🚀 Lancer Simulation", type="primary"): | |
| with st.spinner("Simulation en cours..."): | |
| fig, data_df, metrics = run_open_loop_simulation(nx_open, delta_open, q_open, solicitation, with_pert) | |
| if fig: | |
| st.success(f"✅ Terminé en {metrics.get('Temps calcul', 'N/A')}") | |
| st.pyplot(fig, use_container_width=True) | |
| st.subheader("📊 Export") | |
| csv = data_df.to_csv(index=False) | |
| st.download_button("📥 CSV", csv, "open_loop.csv", "text/csv") | |
| # PDF Report button | |
| with st.expander("📄 Générer Rapport PDF"): | |
| st.write("Générez un rapport PDF complet avec tous les résultats") | |
| if st.button("📄 Créer Rapport PDF", key="pdf_open"): | |
| with st.spinner("Génération du rapport..."): | |
| pdf_data = generate_pdf_report('open', { | |
| 'nx': nx_open, 'δt': delta_open, 'q': q_open, | |
| 'sollicitation': solicitation, 'perturbation': with_pert | |
| }, {'metrics': metrics}, [(fig, "Boucle Ouverte")]) | |
| st.download_button( | |
| "📥 Télécharger PDF", | |
| pdf_data, | |
| f"rapport_boucle_ouverte_{int(time.time())}.pdf", | |
| "application/pdf" | |
| ) | |
| with tab2: | |
| st.markdown('<div class="section-header">', unsafe_allow_html=True) | |
| st.markdown("### 🔒 Contrôle PID en Boucle Fermée") | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.subheader("Paramètres PID") | |
| Kp = st.slider("Kp", 0.0, 10.0, 0.5) | |
| Ki = st.slider("Ki", 0.0, 1.0, 0.1) | |
| Kd = st.slider("Kd", 0.0, 0.1, 0.01) | |
| with col2: | |
| st.subheader("Conditions") | |
| with_noise = st.checkbox("Bruit mesure", value=True) | |
| with_pert_pid = st.checkbox("Perturbation", value=False) | |
| compare_open = st.checkbox("Comparer BO", value=True) | |
| nx_pid = st.slider("nx", 5, 20, nx_global, key="nx_pid") | |
| delta_pid = st.slider("δt", 0.001, 0.05, delta_global, key="delta_pid") | |
| r_pid = st.slider("r", 0.0, 0.01, r_global, key="r_pid") | |
| if st.button("🚀 Lancer PID", type="primary"): | |
| with st.spinner("Simulation PID..."): | |
| fig, data_df, metrics = run_pid_simulation( | |
| nx_pid, delta_pid, r_pid, Kp, Ki, Kd, with_noise, with_pert_pid, compare_open | |
| ) | |
| if fig: | |
| st.success("✅ Simulation PID terminée") | |
| st.pyplot(fig, use_container_width=True) | |
| st.subheader("📊 Export") | |
| csv = data_df.to_csv(index=False) | |
| st.download_button("📥 CSV", csv, "pid.csv", "text/csv") | |
| with st.expander("📄 Générer Rapport PDF"): | |
| if st.button("📄 Créer Rapport PDF PID", key="pdf_pid"): | |
| with st.spinner("Génération..."): | |
| pdf_data = generate_pdf_report('pid', { | |
| 'nx': nx_pid, 'δt': delta_pid, 'r': r_pid, | |
| 'Kp': Kp, 'Ki': Ki, 'Kd': Kd, | |
| 'bruit': with_noise, 'perturbation': with_pert_pid | |
| }, {'metrics': metrics}, [(fig, "PID")], | |
| pid_metrics={'Kp': Kp, 'Ki': Ki, 'Kd': Kd}) | |
| st.download_button( | |
| "📥 Télécharger PDF", | |
| pdf_data, | |
| f"rapport_pid_{Kp}_{Ki}_{Kd}_{int(time.time())}.pdf", | |
| "application/pdf" | |
| ) | |
| with tab3: | |
| st.markdown('<div class="section-header">', unsafe_allow_html=True) | |
| st.markdown("### 🔍 Filtre de Kalman - Estimation d'État") | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| nx_kal = st.slider("nx", 5, 15, nx_global, key="nx_kal") | |
| delta_kal = st.slider("δt", 0.001, 0.05, delta_global, key="delta_kal") | |
| q_kal = st.slider("Bruit processus (q)", 0.0, 0.01, q_global, key="q_kal") | |
| with col2: | |
| r_kal = st.slider("Bruit mesure (r)", 0.0, 0.01, r_global, key="r_kal") | |
| without_measure = st.checkbox("Estimation sans mesure directe", value=False, key="without_measure") | |
| st.info(""" | |
| **Filtre de Kalman** : Estimation optimale de l'état du système | |
| en présence de bruit de processus et de mesure. | |
| Sans mesure directe : Le jumeau estime u_B uniquement à partir | |
| du modèle physique, sans utiliser de capteurs. | |
| """) | |
| if st.button("🚀 Lancer Kalman", type="primary"): | |
| with st.spinner("Estimation Kalman..."): | |
| fig, data_df, metrics = run_kalman_simulation(nx_kal, delta_kal, q_kal, r_kal, without_measure) | |
| if fig: | |
| st.success("✅ Estimation terminée") | |
| st.pyplot(fig, use_container_width=True) | |
| st.subheader("📊 Export") | |
| csv = data_df.to_csv(index=False) | |
| st.download_button("📥 CSV", csv, "kalman.csv", "text/csv") | |
| with st.expander("📄 Générer Rapport PDF"): | |
| if st.button("📄 Créer Rapport PDF Kalman", key="pdf_kalman"): | |
| with st.spinner("Génération..."): | |
| pdf_data = generate_pdf_report('kalman', { | |
| 'nx': nx_kal, 'δt': delta_kal, 'q': q_kal, 'r': r_kal, | |
| 'sans_mesure': without_measure | |
| }, {'metrics': metrics}, [(fig, "Kalman")], | |
| kalman_metrics=metrics) | |
| st.download_button( | |
| "📥 Télécharger PDF", | |
| pdf_data, | |
| f"rapport_kalman_{int(time.time())}.pdf", | |
| "application/pdf" | |
| ) | |
| with tab4: | |
| st.markdown('<div class="section-header">', unsafe_allow_html=True) | |
| st.markdown("### 📄 Générateur de Rapports PDF") | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| st.markdown(""" | |
| <div class="formula-box"> | |
| Fonctionnalités du générateur de rapports : | |
| • Métriques complètes de simulation | |
| • Graphiques haute résolution | |
| • Paramètres de simulation documentés | |
| • Conclusion automatique basée sur le type | |
| • Copyright © 2026 | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown("#### 📋 Instructions") | |
| st.write(""" | |
| 1. Lancez une simulation dans l'un des onglets précédents | |
| 2. Ouvrez l'expander "📄 Générer Rapport PDF" | |
| 3. Cliquez sur "📄 Créer Rapport PDF" | |
| 4. Téléchargez le fichier PDF généré | |
| Chaque rapport PDF inclut : | |
| - Type de simulation et paramètres utilisés | |
| - Toutes les métriques de performance | |
| - Graphiques de visualisation | |
| - Conclusion automatique | |
| - Copyright 2026 | |
| """) | |
| # Footer | |
| st.markdown("---") | |
| st.markdown(f""" | |
| <div class="copyright"> | |
| <p style="font-size: 1.1rem; font-weight: bold; margin-bottom: 0.5rem;">🤖 Jumeaux Numériques : Robot Souple</p> | |
| <p>© 2026 École Centrale Lyon ENISE - Tous droits réservés</p> | |
| <p style="margin-top: 0.5rem; opacity: 0.7;">Développé avec Streamlit | Python | NumPy | Matplotlib | SciPy</p> | |
| </div> | |
| """, unsafe_allow_html=True) |
Xet Storage Details
- Size:
- 55 kB
- Xet hash:
- d6cea191368fa59781e0afc22b579d48102650d40e2714783ebb1a572126eca1
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.