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'
'
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()