download
raw
55 kB
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)
@lru_cache(maxsize=10)
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.