import gradio as gr 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, partial from robot_souple_helpers import * @lru_cache(maxsize=10) def cached_neb_matrices(nx, dx, E, Ix, rho, S, cy): return neb_beam_matrices(nx, dx, E, Ix, rho, S, cy) def generate_animation(ixe, u_history, title="Animation de la déformation"): if u_history.shape[1] < 2: return "Animation non disponible (pas assez de frames)" fig, ax = plt.subplots() ax.set_xlim(0, max(ixe)) ax.set_ylim(np.min(u_history) - 0.1, np.max(u_history) + 0.1) line, = ax.plot([], [], lw=2, color='blue') ax.set_title(title) ax.set_xlabel('Position x (m)') ax.set_ylabel('Déplacement u (m)') def init(): line.set_data([], []) return line, def animate(i): line.set_data(ixe, u_history[:, i]) return line, # Further reduce frames for faster generation max_frames = 10 frames = min(u_history.shape[1], max_frames) step = max(1, u_history.shape[1] // frames) anim = FuncAnimation(fig, animate, init_func=init, frames=range(0, u_history.shape[1], step), interval=300, blit=True) buf = BytesIO() anim.save(buf, writer='pillow', fps=3) buf.seek(0) gif = base64.b64encode(buf.read()).decode('utf-8') plt.close(fig) return f'Animation' def run_simulation(nx, delta, q, r, Kp, Ki, Kd, mu, with_noise, with_pert, niter, nopt, stagEps, solicitation, compare_open, without_measure, mode="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(200, int(2 / 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) vseed1 = 0 if with_pert else 42 vseed2 = 1 if with_noise else 43 Kfull, Cfull, Mfull = cached_neb_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 fpert = generateNoiseTemporal(time0, 1, q, vseed1) if with_pert else np.zeros(len(time0)) mpert = generateNoiseTemporal(time0, 1, r, vseed2) if with_noise else np.zeros(len(time0)) settling_time = "" if mode == "Boucle ouverte": if solicitation == "échelon": fA = Heaviside(time0 - 1) elif solicitation == "sinusoïdale": fA = np.sin(2 * np.pi * 0.1 * time0) else: fA = Heaviside(time0 - 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 "Non stabilisé" fig, ax = plt.subplots() ax.plot(time0, u[induB, :], label='u_B (tip)', linewidth=2) ax.plot(time0, fA, label='Consigne f_A', linestyle='--') ax.legend() ax.set_title(f"Boucle ouverte ({solicitation}) : Déplacement du tip") ax.set_xlabel("Temps (s)") ax.set_ylabel("Déplacement (m)") animation = generate_animation(ixe, u[::2, :], f"Déformation en boucle ouverte ({solicitation})") elif mode == "Boucle fermée PID": 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 = [] 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) 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) fig, ax = plt.subplots() ax.plot(time0, u[induB, :], label='u_B PID', linewidth=2) if u_open is not None: ax.plot(time0, u_open[induB, :], label='u_B open loop', linestyle='--') ax.plot(time0, u_ref, label='Consigne', linestyle='-.') ax.legend() ax.set_title("Boucle fermée PID : Déplacement u_B") ax.set_xlabel("Temps (s)") ax.set_ylabel("Déplacement (m)") animation = generate_animation(ixe, u[::2, :], "Déformation en boucle fermée") elif mode == "Filtre de Kalman": n_state = 2 * ndo1 Q = np.eye(n_state) * q R = r P = np.eye(n_state) * 1e-3 x_est = np.zeros(n_state) A = Fconstruct(M, C, K, delta, beta, gamma)[:n_state, :n_state] B_mat = Bconstruct(M, C, K, nA, delta, beta, gamma)[:n_state, :ndo1] H = np.zeros((1, n_state)) H[0, induB] = 1 u_sim = np.zeros((ndo1, len(time0))) v_sim = np.zeros((ndo1, len(time0))) u_sim[:, 0] = np.zeros(ndo1) v_sim[:, 0] = np.zeros(ndo1) estimates = [] P_history = [P.copy()] for step in range(1, len(time0)): f_k = np.zeros(ndo1) u_sim[:, step], v_sim[:, step], _ = newmark1stepMRHS(M, C, K, f_k, u_sim[:, step-1], v_sim[:, step-1], np.zeros(ndo1), delta, beta, gamma) z = None if not without_measure: z = u_sim[induB, step] + mpert[step] x_pred = A @ x_est P_pred = A @ P @ A.T + Q if not without_measure and z is not None: K_kal = P_pred @ H.T @ np.linalg.inv(H @ P_pred @ H.T + R) x_est = x_pred + K_kal @ (z - H @ x_pred) P = (np.eye(n_state) - K_kal @ H) @ P_pred else: x_est = x_pred P = P_pred estimates.append(x_est[induB]) P_history.append(P.copy()) fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) ax1.plot(time0[1:], estimates, label='Estimation Kalman', linewidth=2) ax1.plot(time0, u_sim[induB, :], label='Vraie u_B', linestyle='--') ax1.legend() ax1.set_title("Estimation de u_B") ax1.set_xlabel("Temps (s)") ax1.set_ylabel("Déplacement (m)") cov_uB = [P[induB, induB] for P in P_history] ax2.plot(time0, cov_uB, label='Covariance u_B', linewidth=2) ax2.legend() ax2.set_title("Évolution de la covariance") ax2.set_xlabel("Temps (s)") ax2.set_ylabel("Variance") animation = "Animation non disponible pour Kalman" else: return "Mode non implémenté", "", f"Temps: {time.time() - start_time:.2f}s" buf = BytesIO() fig.savefig(buf, format="png", dpi=100) buf.seek(0) plot_b64 = base64.b64encode(buf.read()).decode('utf-8') plt.close(fig) plot_html = f'' elapsed = f"Temps de calcul: {time.time() - start_time:.2f}s" if mode == "Boucle ouverte": info_str = f"{elapsed}, Stabilité: {settling_time}" else: info_str = elapsed return plot_html, animation, info_str except Exception as e: return f"Erreur: {str(e)}", "", f"Temps: {time.time() - start_time:.2f}s" def generate_schema(): fig, ax = plt.subplots(figsize=(10, 6)) ax.text(0.1, 0.8, 'i', fontsize=14, ha='center') ax.arrow(0.15, 0.8, 0.1, 0, head_width=0.03, head_length=0.03, fc='k', ec='k') ax.text(0.3, 0.8, 'Moteur', fontsize=14, ha='center', bbox=dict(boxstyle="round,pad=0.3", facecolor="lightblue")) ax.arrow(0.4, 0.8, 0.1, 0, head_width=0.03, head_length=0.03, fc='k', ec='k') ax.text(0.55, 0.8, 'F_m', fontsize=14, ha='center') ax.arrow(0.6, 0.8, 0.1, 0, head_width=0.03, head_length=0.03, fc='k', ec='k') ax.text(0.75, 0.8, 'Transmission', fontsize=14, ha='center', bbox=dict(boxstyle="round,pad=0.3", facecolor="lightgreen")) ax.arrow(0.85, 0.8, 0.1, 0, head_width=0.03, head_length=0.03, fc='k', ec='k') ax.text(0.95, 0.8, 'F_t', fontsize=14, ha='center') ax.arrow(0.15, 0.5, 0.1, 0, head_width=0.03, head_length=0.03, fc='k', ec='k') ax.text(0.3, 0.5, 'C_m', fontsize=14, ha='center') ax.arrow(0.4, 0.5, 0.1, 0, head_width=0.03, head_length=0.03, fc='k', ec='k') ax.text(0.55, 0.5, 'θ', fontsize=14, ha='center') ax.arrow(0.6, 0.5, 0.1, 0, head_width=0.03, head_length=0.03, fc='k', ec='k') ax.text(0.75, 0.5, 'f_A', fontsize=14, ha='center') ax.arrow(0.85, 0.5, 0.1, 0, head_width=0.03, head_length=0.03, fc='k', ec='k') ax.text(0.95, 0.5, 'u_A', fontsize=14, ha='center') ax.arrow(0.5, 0.3, 0, -0.1, head_width=0.03, head_length=0.03, fc='k', ec='k') ax.text(0.5, 0.2, 'Poutre Flexible', fontsize=14, ha='center', bbox=dict(boxstyle="round,pad=0.3", facecolor="lightcoral")) ax.arrow(0.5, 0.1, 0, -0.05, head_width=0.03, head_length=0.03, fc='k', ec='k') ax.text(0.5, 0.05, 'u_B', fontsize=14, ha='center') ax.set_xlim(0, 1) ax.set_ylim(0, 1) ax.axis('off') buf = BytesIO() fig.savefig(buf, format="png", dpi=100) buf.seek(0) img_b64 = base64.b64encode(buf.read()).decode('utf-8') plt.close(fig) return f'' def compute_ft_gt(rendement, cm, theta_dot, fa_dot, ua_dot): if rendement == 1: ft = cm * theta_dot gt = fa_dot * ua_dot equal = "Oui" if np.isclose(ft, gt) else "Non" return f"F_t = {ft:.3f}, G_t = {gt:.3f}, Égal: {equal}" else: return r"Pour rendement unitaire, F_t = C_m * \dot{\theta}, G_t = f_A * \dot{u_A}" def quiz_answer(choice): if choice == "Estimer u_B sans mesure directe": return "Correct ! Le jumeau permet d'estimer états non mesurés." else: return "Incorrect. Réessayez." desc_prep = r""" ### Travail Préparatoire (Section 4.1 des rapports) Étude du système : Schéma bloc en boucle ouverte. Relation F_t et G_t : Sous hypothèse de conservation d'énergie (rendement unitaire), F_t = G_t (d'après conservation C_m \dot{\theta} = f_A \dot{u_A}). Intérêt du jumeau : Estimer u_B sans mesure directe pour contrôle en boucle fermée. """ desc_open = """ ### Boucle Ouverte (Section 4.2.1) Simulation sans rétroaction. Observez le déplacement u_B avec/sans perturbation. La position de B suit A avec retard (forme sinusoïdale ou échelon). """ desc_pid = """ ### Boucle Fermée PID Contrôle avec PID pour suivre la consigne. Observez l'erreur de suivi. """ desc_kalman = """ ### Filtre de Kalman Estimation d'état avec filtre Kalman pour filtrer le bruit sur la mesure u_B. """ with gr.Blocks() as demo: gr.Markdown("# Jumeaux Numériques : Simulation de Robot Souple\nDémo interactive basée sur le cours de Renaud Ferrier - Centrale Lyon ENISE.") with gr.Tabs(): with gr.Tab("Préparatoire"): gr.Markdown(desc_prep) gr.Markdown("### Schéma Bloc du Système") schema_img = gr.HTML(value=generate_schema()) gr.Markdown("### Conservation d'Énergie : F_t = G_t") with gr.Row(): rendement_slider = gr.Slider(0.5, 1, 1, label="Rendement (η)", info="Hypothèse rendement unitaire pour conservation") cm_input = gr.Number(1e-3, label="C_m (constante moteur)") theta_dot_input = gr.Number(10, label="\\dot{\\theta} (vitesse angulaire)") fa_dot_input = gr.Number(100, label="f_A (force en A)") ua_dot_input = gr.Number(0.1, label="\\dot{u_A} (vitesse en A)") compute_btn = gr.Button("Calculer F_t et G_t") ft_gt_output = gr.Textbox(label="Résultat") compute_btn.click(compute_ft_gt, inputs=[rendement_slider, cm_input, theta_dot_input, fa_dot_input, ua_dot_input], outputs=ft_gt_output) gr.Markdown("### Intérêt du Jumeau Numérique") quiz_radio = gr.Radio(["Estimer u_B sans mesure directe", "Simuler rapidement", "Réduire coûts"], label="Quel est l'intérêt principal du jumeau ?") quiz_btn = gr.Button("Vérifier") quiz_output = gr.Textbox() quiz_btn.click(quiz_answer, inputs=quiz_radio, outputs=quiz_output) gr.Markdown("### Probabilités Gaussiennes pour Bruits\nDensité : $ p(x) = \\frac{1}{\\sqrt{2\\pi \\sigma^2}} \\exp\\left(-\\frac{(x-\\mu)^2}{2\\sigma^2}\\right) $\nUtilisée pour covariances Q, R dans Kalman.") with gr.Tab("Boucle ouverte"): gr.Markdown(desc_open) solicitation_dropdown = gr.Dropdown(["échelon", "sinusoïdale"], value="échelon", label="Type de sollicitation", info="Échelon à t=1s ou sinusoïdale") with gr.Row(): nx_slider = gr.Slider(5, 20, 10, 1, label="nx (éléments)", info="Précision poutre") delta_slider = gr.Slider(0.001, 0.05, 0.01, label="δt (pas temps)") q_slider = gr.Slider(0, 0.001, 0.0001, label="q (variance perturbation)") with_pert = gr.Checkbox(True, label="Perturbation ?") compare_open_open = gr.Checkbox(False, visible=False) without_measure_open = gr.Checkbox(False, visible=False) mode_open = gr.Textbox("Boucle ouverte", visible=False) run_open = gr.Button("Simuler") plot_open = gr.HTML() anim_open = gr.HTML() time_open = gr.Textbox(label="Info") run_open.click(run_simulation, inputs=[nx_slider, delta_slider, q_slider, gr.Number(value=0, visible=False), gr.Number(value=0, visible=False), gr.Number(value=0, visible=False), gr.Number(value=0, visible=False), gr.Number(value=0, visible=False), gr.Checkbox(value=False, visible=False), with_pert, gr.Number(value=0, visible=False), gr.Number(value=0, visible=False), gr.Number(value=0, visible=False), solicitation_dropdown, compare_open_open, without_measure_open, mode_open], outputs=[plot_open, anim_open, time_open]) with gr.Tab("Boucle fermée PID"): gr.Markdown(desc_pid) with gr.Row(): nx_pid = gr.Slider(5, 20, 10, 1, label="nx") delta_pid = gr.Slider(0.001, 0.05, 0.01, label="δt") r_pid = gr.Slider(0, 0.001, 0.0001, label="r (bruit mesure)") Kp_slider = gr.Slider(0, 10, 0.5, label="Kp") Ki_slider = gr.Slider(0, 1, 0.1, label="Ki") Kd_slider = gr.Slider(0, 0.1, 0.01, label="Kd") with_noise_pid = gr.Checkbox(True, label="Bruit mesure ?") compare_open_pid = gr.Checkbox(False, label="Comparer avec boucle ouverte") solicitation_pid = gr.Textbox("échelon", visible=False) without_measure_pid = gr.Checkbox(False, visible=False) mode_pid = gr.Textbox("Boucle fermée PID", visible=False) run_pid = gr.Button("Simuler") plot_pid = gr.HTML() anim_pid = gr.HTML() time_pid = gr.Textbox() run_pid.click(run_simulation, inputs=[nx_pid, delta_pid, gr.Number(value=0, visible=False), r_pid, Kp_slider, Ki_slider, Kd_slider, gr.Number(value=0, visible=False), with_noise_pid, gr.Checkbox(value=False, visible=False), gr.Number(value=0, visible=False), gr.Number(value=0, visible=False), gr.Number(value=0, visible=False), solicitation_pid, compare_open_pid, without_measure_pid, mode_pid], outputs=[plot_pid, anim_pid, time_pid]) with gr.Tab("Filtre de Kalman"): gr.Markdown(desc_kalman) with gr.Row(): nx_kal = gr.Slider(5, 15, 10, 1, label="nx") delta_kal = gr.Slider(0.001, 0.05, 0.01, label="δt") q_kal = gr.Slider(0, 0.001, 0.0001, label="q (bruit processus)") r_kal = gr.Slider(0, 0.001, 0.0001, label="r (bruit mesure)") without_measure_kal = gr.Checkbox(False, label="Sans mesure u_B (jumeau)") solicitation_kal = gr.Textbox("échelon", visible=False) compare_open_kal = gr.Checkbox(False, visible=False) mode_kal = gr.Textbox("Filtre de Kalman", visible=False) run_kal = gr.Button("Simuler") plot_kal = gr.HTML() anim_kal = gr.HTML() time_kal = gr.Textbox() run_kal.click(run_simulation, inputs=[nx_kal, delta_kal, q_kal, r_kal, gr.Number(value=0, visible=False), gr.Number(value=0, visible=False), gr.Number(value=0, visible=False), gr.Number(value=0, visible=False), gr.Checkbox(value=False, visible=False), gr.Checkbox(value=False, visible=False), gr.Number(value=0, visible=False), gr.Number(value=0, visible=False), gr.Number(value=0, visible=False), solicitation_kal, compare_open_kal, without_measure_kal, mode_kal], outputs=[plot_kal, anim_kal, time_kal]) demo.launch()