tiffank1802 commited on
Commit ·
b078a34
1
Parent(s): a42151d
Simplify code: minimal interface with ultra-fast simulation
Browse files- Replace complex simulation with simplified math functions
- Reduce dependencies from 5 to 3 packages
- Ultra-fast computation: 50 steps max, 1 second simulation time
- Simple plots instead of animations
- One-click interface without complex tabs
- app.py +81 -324
- app_complex.py +347 -0
- requirements.txt +1 -4
- requirements_complex.txt +6 -0
app.py
CHANGED
|
@@ -1,347 +1,104 @@
|
|
| 1 |
import gradio as gr
|
| 2 |
import numpy as np
|
| 3 |
import matplotlib.pyplot as plt
|
| 4 |
-
from matplotlib.animation import FuncAnimation
|
| 5 |
from io import BytesIO
|
| 6 |
import base64
|
| 7 |
import time
|
| 8 |
-
from functools import lru_cache, partial
|
| 9 |
-
from robot_souple_helpers import *
|
| 10 |
|
| 11 |
-
|
| 12 |
-
def cached_neb_matrices(nx, dx, E, Ix, rho, S, cy):
|
| 13 |
-
return neb_beam_matrices(nx, dx, E, Ix, rho, S, cy)
|
| 14 |
-
|
| 15 |
-
def generate_animation(ixe, u_history, title="Animation de la déformation"):
|
| 16 |
-
if u_history.shape[1] < 2:
|
| 17 |
-
return "Animation non disponible (pas assez de frames)"
|
| 18 |
-
fig, ax = plt.subplots()
|
| 19 |
-
ax.set_xlim(0, max(ixe))
|
| 20 |
-
ax.set_ylim(np.min(u_history) - 0.1, np.max(u_history) + 0.1)
|
| 21 |
-
line, = ax.plot([], [], lw=2, color='blue')
|
| 22 |
-
ax.set_title(title)
|
| 23 |
-
ax.set_xlabel('Position x (m)')
|
| 24 |
-
ax.set_ylabel('Déplacement u (m)')
|
| 25 |
-
def init():
|
| 26 |
-
line.set_data([], [])
|
| 27 |
-
return line,
|
| 28 |
-
def animate(i):
|
| 29 |
-
line.set_data(ixe, u_history[:, i])
|
| 30 |
-
return line,
|
| 31 |
-
# Further reduce frames for faster generation
|
| 32 |
-
max_frames = 10
|
| 33 |
-
frames = min(u_history.shape[1], max_frames)
|
| 34 |
-
step = max(1, u_history.shape[1] // frames)
|
| 35 |
-
anim = FuncAnimation(fig, animate, init_func=init, frames=range(0, u_history.shape[1], step), interval=300, blit=True)
|
| 36 |
-
buf = BytesIO()
|
| 37 |
-
anim.save(buf, writer='pillow', fps=3)
|
| 38 |
-
buf.seek(0)
|
| 39 |
-
gif = base64.b64encode(buf.read()).decode('utf-8')
|
| 40 |
-
plt.close(fig)
|
| 41 |
-
return f'<img src="data:image/gif;base64,{gif}" alt="Animation">'
|
| 42 |
-
|
| 43 |
-
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"):
|
| 44 |
start_time = time.time()
|
|
|
|
| 45 |
try:
|
| 46 |
-
|
| 47 |
-
gamma = 0.5
|
| 48 |
L = 500
|
| 49 |
-
|
| 50 |
-
b = 5
|
| 51 |
-
E = 70000
|
| 52 |
-
rho = 2700e-9
|
| 53 |
-
cy = 1e-4
|
| 54 |
-
nstep = min(200, int(2 / delta))
|
| 55 |
-
nA = int(np.floor(nx / 2))
|
| 56 |
-
S = a * b
|
| 57 |
-
Ix = a * b**3 / 12
|
| 58 |
-
dx = L / nx
|
| 59 |
time0 = np.linspace(0, delta * nstep, nstep + 1)
|
| 60 |
-
ixe = np.linspace(
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
Kfull, Cfull, Mfull = cached_neb_matrices(nx, dx, E, Ix, rho, S, cy)
|
| 64 |
-
ndo1 = Kfull.shape[0] - 2
|
| 65 |
-
K = Kfull[2:, 2:]
|
| 66 |
-
C = Cfull[2:, 2:]
|
| 67 |
-
M = Mfull[2:, 2:]
|
| 68 |
-
induA = 2 * nA - 2
|
| 69 |
-
induB = 2 * nx - 2
|
| 70 |
-
fpert = generateNoiseTemporal(time0, 1, q, vseed1) if with_pert else np.zeros(len(time0))
|
| 71 |
-
mpert = generateNoiseTemporal(time0, 1, r, vseed2) if with_noise else np.zeros(len(time0))
|
| 72 |
-
settling_time = ""
|
| 73 |
-
|
| 74 |
if mode == "Boucle ouverte":
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
fA = Heaviside(time0 - 1)
|
| 81 |
-
f = np.zeros((ndo1, len(time0)))
|
| 82 |
-
f[induA, :] = fA + fpert
|
| 83 |
-
u0 = np.zeros(ndo1)
|
| 84 |
-
v0 = np.zeros(ndo1)
|
| 85 |
-
a0 = np.linalg.solve(M, f[:, 0] - C @ v0 - K @ u0)
|
| 86 |
-
u, v, a = Newmark(M, C, K, f, u0, v0, a0, delta, beta, gamma)
|
| 87 |
-
uB_final = u[induB, -1]
|
| 88 |
-
threshold = 0.01 * abs(uB_final)
|
| 89 |
-
stable_idx = np.where(np.abs(u[induB, :] - uB_final) < threshold)[0]
|
| 90 |
-
settling_time = time0[stable_idx[0]] if len(stable_idx) > 0 else "Non stabilisé"
|
| 91 |
-
fig, ax = plt.subplots()
|
| 92 |
-
ax.plot(time0, u[induB, :], label='u_B (tip)', linewidth=2)
|
| 93 |
-
ax.plot(time0, fA, label='Consigne f_A', linestyle='--')
|
| 94 |
-
ax.legend()
|
| 95 |
-
ax.set_title(f"Boucle ouverte ({solicitation}) : Déplacement du tip")
|
| 96 |
-
ax.set_xlabel("Temps (s)")
|
| 97 |
-
ax.set_ylabel("Déplacement (m)")
|
| 98 |
-
animation = generate_animation(ixe, u[::2, :], f"Déformation en boucle ouverte ({solicitation})")
|
| 99 |
-
elif mode == "Boucle fermée PID":
|
| 100 |
-
u_ref = Heaviside(time0 - 1)
|
| 101 |
-
u = np.zeros((ndo1, len(time0)))
|
| 102 |
-
v = np.zeros((ndo1, len(time0)))
|
| 103 |
-
a = np.zeros((ndo1, len(time0)))
|
| 104 |
-
u[:, 0] = np.zeros(ndo1)
|
| 105 |
-
v[:, 0] = np.zeros(ndo1)
|
| 106 |
-
a[:, 0] = np.linalg.solve(M, -C @ v[:, 0] - K @ u[:, 0])
|
| 107 |
-
integ_e = 0
|
| 108 |
-
prev_e = 0
|
| 109 |
-
errors = []
|
| 110 |
-
for step in range(1, len(time0)):
|
| 111 |
-
uB_meas = u[induB, step-1] + mpert[step-1]
|
| 112 |
-
e = u_ref[step] - uB_meas
|
| 113 |
-
integ_e += e * delta
|
| 114 |
-
de = (e - prev_e) / delta
|
| 115 |
-
Fpid = Kp * e + Ki * integ_e + Kd * de
|
| 116 |
-
f_k = np.zeros(ndo1)
|
| 117 |
-
f_k[induA] = Fpid + fpert[step]
|
| 118 |
-
u[:, step], v[:, step], a[:, step] = newmark1stepMRHS(M, C, K, f_k, u[:, step-1], v[:, step-1], a[:, step-1], delta, beta, gamma)
|
| 119 |
-
prev_e = e
|
| 120 |
-
errors.append(e)
|
| 121 |
-
u_open = None
|
| 122 |
-
if compare_open:
|
| 123 |
-
fA_comp = Heaviside(time0 - 1)
|
| 124 |
-
f_comp = np.zeros((ndo1, len(time0)))
|
| 125 |
-
f_comp[induA, :] = fA_comp + fpert
|
| 126 |
-
u_open, _, _ = Newmark(M, C, K, f_comp, np.zeros(ndo1), np.zeros(ndo1), np.linalg.solve(M, f_comp[:, 0]), delta, beta, gamma)
|
| 127 |
-
fig, ax = plt.subplots()
|
| 128 |
-
ax.plot(time0, u[induB, :], label='u_B PID', linewidth=2)
|
| 129 |
-
if u_open is not None:
|
| 130 |
-
ax.plot(time0, u_open[induB, :], label='u_B open loop', linestyle='--')
|
| 131 |
-
ax.plot(time0, u_ref, label='Consigne', linestyle='-.')
|
| 132 |
-
ax.legend()
|
| 133 |
-
ax.set_title("Boucle fermée PID : Déplacement u_B")
|
| 134 |
-
ax.set_xlabel("Temps (s)")
|
| 135 |
-
ax.set_ylabel("Déplacement (m)")
|
| 136 |
-
animation = generate_animation(ixe, u[::2, :], "Déformation en boucle fermée")
|
| 137 |
-
elif mode == "Filtre de Kalman":
|
| 138 |
-
n_state = 2 * ndo1
|
| 139 |
-
Q = np.eye(n_state) * q
|
| 140 |
-
R = r
|
| 141 |
-
P = np.eye(n_state) * 1e-3
|
| 142 |
-
x_est = np.zeros(n_state)
|
| 143 |
-
A = Fconstruct(M, C, K, delta, beta, gamma)[:n_state, :n_state]
|
| 144 |
-
B_mat = Bconstruct(M, C, K, nA, delta, beta, gamma)[:n_state, :ndo1]
|
| 145 |
-
H = np.zeros((1, n_state))
|
| 146 |
-
H[0, induB] = 1
|
| 147 |
-
u_sim = np.zeros((ndo1, len(time0)))
|
| 148 |
-
v_sim = np.zeros((ndo1, len(time0)))
|
| 149 |
-
u_sim[:, 0] = np.zeros(ndo1)
|
| 150 |
-
v_sim[:, 0] = np.zeros(ndo1)
|
| 151 |
-
estimates = []
|
| 152 |
-
P_history = [P.copy()]
|
| 153 |
-
for step in range(1, len(time0)):
|
| 154 |
-
f_k = np.zeros(ndo1)
|
| 155 |
-
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)
|
| 156 |
-
z = None
|
| 157 |
-
if not without_measure:
|
| 158 |
-
z = u_sim[induB, step] + mpert[step]
|
| 159 |
-
x_pred = A @ x_est
|
| 160 |
-
P_pred = A @ P @ A.T + Q
|
| 161 |
-
if not without_measure and z is not None:
|
| 162 |
-
K_kal = P_pred @ H.T @ np.linalg.inv(H @ P_pred @ H.T + R)
|
| 163 |
-
x_est = x_pred + K_kal @ (z - H @ x_pred)
|
| 164 |
-
P = (np.eye(n_state) - K_kal @ H) @ P_pred
|
| 165 |
else:
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
buf = BytesIO()
|
| 188 |
-
fig.savefig(buf, format=
|
| 189 |
buf.seek(0)
|
| 190 |
-
|
| 191 |
plt.close(fig)
|
| 192 |
-
|
|
|
|
| 193 |
elapsed = f"Temps de calcul: {time.time() - start_time:.2f}s"
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
info_str = elapsed
|
| 198 |
-
return plot_html, animation, info_str
|
| 199 |
except Exception as e:
|
| 200 |
-
return f"Erreur: {str(e)}",
|
| 201 |
-
|
| 202 |
-
def generate_schema():
|
| 203 |
-
fig, ax = plt.subplots(figsize=(10, 6))
|
| 204 |
-
ax.text(0.1, 0.8, 'i', fontsize=14, ha='center')
|
| 205 |
-
ax.arrow(0.15, 0.8, 0.1, 0, head_width=0.03, head_length=0.03, fc='k', ec='k')
|
| 206 |
-
ax.text(0.3, 0.8, 'Moteur', fontsize=14, ha='center', bbox=dict(boxstyle="round,pad=0.3", facecolor="lightblue"))
|
| 207 |
-
ax.arrow(0.4, 0.8, 0.1, 0, head_width=0.03, head_length=0.03, fc='k', ec='k')
|
| 208 |
-
ax.text(0.55, 0.8, 'F_m', fontsize=14, ha='center')
|
| 209 |
-
ax.arrow(0.6, 0.8, 0.1, 0, head_width=0.03, head_length=0.03, fc='k', ec='k')
|
| 210 |
-
ax.text(0.75, 0.8, 'Transmission', fontsize=14, ha='center', bbox=dict(boxstyle="round,pad=0.3", facecolor="lightgreen"))
|
| 211 |
-
ax.arrow(0.85, 0.8, 0.1, 0, head_width=0.03, head_length=0.03, fc='k', ec='k')
|
| 212 |
-
ax.text(0.95, 0.8, 'F_t', fontsize=14, ha='center')
|
| 213 |
-
ax.arrow(0.15, 0.5, 0.1, 0, head_width=0.03, head_length=0.03, fc='k', ec='k')
|
| 214 |
-
ax.text(0.3, 0.5, 'C_m', fontsize=14, ha='center')
|
| 215 |
-
ax.arrow(0.4, 0.5, 0.1, 0, head_width=0.03, head_length=0.03, fc='k', ec='k')
|
| 216 |
-
ax.text(0.55, 0.5, 'θ', fontsize=14, ha='center')
|
| 217 |
-
ax.arrow(0.6, 0.5, 0.1, 0, head_width=0.03, head_length=0.03, fc='k', ec='k')
|
| 218 |
-
ax.text(0.75, 0.5, 'f_A', fontsize=14, ha='center')
|
| 219 |
-
ax.arrow(0.85, 0.5, 0.1, 0, head_width=0.03, head_length=0.03, fc='k', ec='k')
|
| 220 |
-
ax.text(0.95, 0.5, 'u_A', fontsize=14, ha='center')
|
| 221 |
-
ax.arrow(0.5, 0.3, 0, -0.1, head_width=0.03, head_length=0.03, fc='k', ec='k')
|
| 222 |
-
ax.text(0.5, 0.2, 'Poutre Flexible', fontsize=14, ha='center', bbox=dict(boxstyle="round,pad=0.3", facecolor="lightcoral"))
|
| 223 |
-
ax.arrow(0.5, 0.1, 0, -0.05, head_width=0.03, head_length=0.03, fc='k', ec='k')
|
| 224 |
-
ax.text(0.5, 0.05, 'u_B', fontsize=14, ha='center')
|
| 225 |
-
ax.set_xlim(0, 1)
|
| 226 |
-
ax.set_ylim(0, 1)
|
| 227 |
-
ax.axis('off')
|
| 228 |
-
buf = BytesIO()
|
| 229 |
-
fig.savefig(buf, format="png", dpi=100)
|
| 230 |
-
buf.seek(0)
|
| 231 |
-
img_b64 = base64.b64encode(buf.read()).decode('utf-8')
|
| 232 |
-
plt.close(fig)
|
| 233 |
-
return f'<img src="data:image/png;base64,{img_b64}" style="max-width:100%;">'
|
| 234 |
-
|
| 235 |
-
def compute_ft_gt(rendement, cm, theta_dot, fa_dot, ua_dot):
|
| 236 |
-
if rendement == 1:
|
| 237 |
-
ft = cm * theta_dot
|
| 238 |
-
gt = fa_dot * ua_dot
|
| 239 |
-
equal = "Oui" if np.isclose(ft, gt) else "Non"
|
| 240 |
-
return f"F_t = {ft:.3f}, G_t = {gt:.3f}, Égal: {equal}"
|
| 241 |
-
else:
|
| 242 |
-
return r"Pour rendement unitaire, F_t = C_m * \dot{\theta}, G_t = f_A * \dot{u_A}"
|
| 243 |
-
|
| 244 |
-
def quiz_answer(choice):
|
| 245 |
-
if choice == "Estimer u_B sans mesure directe":
|
| 246 |
-
return "Correct ! Le jumeau permet d'estimer états non mesurés."
|
| 247 |
-
else:
|
| 248 |
-
return "Incorrect. Réessayez."
|
| 249 |
-
|
| 250 |
-
desc_prep = r"""
|
| 251 |
-
### Travail Préparatoire (Section 4.1 des rapports)
|
| 252 |
-
É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}).
|
| 253 |
-
Intérêt du jumeau : Estimer u_B sans mesure directe pour contrôle en boucle fermée.
|
| 254 |
-
"""
|
| 255 |
-
desc_open = """
|
| 256 |
-
### Boucle Ouverte (Section 4.2.1)
|
| 257 |
-
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).
|
| 258 |
-
"""
|
| 259 |
-
desc_pid = """
|
| 260 |
-
### Boucle Fermée PID
|
| 261 |
-
Contrôle avec PID pour suivre la consigne. Observez l'erreur de suivi.
|
| 262 |
-
"""
|
| 263 |
-
desc_kalman = """
|
| 264 |
-
### Filtre de Kalman
|
| 265 |
-
Estimation d'état avec filtre Kalman pour filtrer le bruit sur la mesure u_B.
|
| 266 |
-
"""
|
| 267 |
|
|
|
|
| 268 |
with gr.Blocks() as demo:
|
| 269 |
-
gr.Markdown("#
|
| 270 |
|
| 271 |
-
with gr.
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
quiz_btn.click(quiz_answer, inputs=quiz_radio, outputs=quiz_output)
|
| 291 |
-
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.")
|
| 292 |
-
|
| 293 |
-
with gr.Tab("Boucle ouverte"):
|
| 294 |
-
gr.Markdown(desc_open)
|
| 295 |
-
solicitation_dropdown = gr.Dropdown(["échelon", "sinusoïdale"], value="échelon", label="Type de sollicitation", info="Échelon à t=1s ou sinusoïdale")
|
| 296 |
-
with gr.Row():
|
| 297 |
-
nx_slider = gr.Slider(5, 20, 10, 1, label="nx (éléments)", info="Précision poutre")
|
| 298 |
-
delta_slider = gr.Slider(0.001, 0.05, 0.01, label="δt (pas temps)")
|
| 299 |
-
q_slider = gr.Slider(0, 0.001, 0.0001, label="q (variance perturbation)")
|
| 300 |
-
with_pert = gr.Checkbox(True, label="Perturbation ?")
|
| 301 |
-
compare_open_open = gr.Checkbox(False, visible=False)
|
| 302 |
-
without_measure_open = gr.Checkbox(False, visible=False)
|
| 303 |
-
mode_open = gr.Textbox("Boucle ouverte", visible=False)
|
| 304 |
-
run_open = gr.Button("Simuler")
|
| 305 |
-
plot_open = gr.HTML()
|
| 306 |
-
anim_open = gr.HTML()
|
| 307 |
-
time_open = gr.Textbox(label="Info")
|
| 308 |
-
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])
|
| 309 |
-
|
| 310 |
-
with gr.Tab("Boucle fermée PID"):
|
| 311 |
-
gr.Markdown(desc_pid)
|
| 312 |
-
with gr.Row():
|
| 313 |
-
nx_pid = gr.Slider(5, 20, 10, 1, label="nx")
|
| 314 |
-
delta_pid = gr.Slider(0.001, 0.05, 0.01, label="δt")
|
| 315 |
-
r_pid = gr.Slider(0, 0.001, 0.0001, label="r (bruit mesure)")
|
| 316 |
-
Kp_slider = gr.Slider(0, 10, 0.5, label="Kp")
|
| 317 |
-
Ki_slider = gr.Slider(0, 1, 0.1, label="Ki")
|
| 318 |
-
Kd_slider = gr.Slider(0, 0.1, 0.01, label="Kd")
|
| 319 |
-
with_noise_pid = gr.Checkbox(True, label="Bruit mesure ?")
|
| 320 |
-
compare_open_pid = gr.Checkbox(False, label="Comparer avec boucle ouverte")
|
| 321 |
-
solicitation_pid = gr.Textbox("échelon", visible=False)
|
| 322 |
-
without_measure_pid = gr.Checkbox(False, visible=False)
|
| 323 |
-
mode_pid = gr.Textbox("Boucle fermée PID", visible=False)
|
| 324 |
-
run_pid = gr.Button("Simuler")
|
| 325 |
-
plot_pid = gr.HTML()
|
| 326 |
-
anim_pid = gr.HTML()
|
| 327 |
-
time_pid = gr.Textbox()
|
| 328 |
-
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])
|
| 329 |
-
|
| 330 |
-
with gr.Tab("Filtre de Kalman"):
|
| 331 |
-
gr.Markdown(desc_kalman)
|
| 332 |
-
with gr.Row():
|
| 333 |
-
nx_kal = gr.Slider(5, 15, 10, 1, label="nx")
|
| 334 |
-
delta_kal = gr.Slider(0.001, 0.05, 0.01, label="δt")
|
| 335 |
-
q_kal = gr.Slider(0, 0.001, 0.0001, label="q (bruit processus)")
|
| 336 |
-
r_kal = gr.Slider(0, 0.001, 0.0001, label="r (bruit mesure)")
|
| 337 |
-
without_measure_kal = gr.Checkbox(False, label="Sans mesure u_B (jumeau)")
|
| 338 |
-
solicitation_kal = gr.Textbox("échelon", visible=False)
|
| 339 |
-
compare_open_kal = gr.Checkbox(False, visible=False)
|
| 340 |
-
mode_kal = gr.Textbox("Filtre de Kalman", visible=False)
|
| 341 |
-
run_kal = gr.Button("Simuler")
|
| 342 |
-
plot_kal = gr.HTML()
|
| 343 |
-
anim_kal = gr.HTML()
|
| 344 |
-
time_kal = gr.Textbox()
|
| 345 |
-
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])
|
| 346 |
|
| 347 |
demo.launch()
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
import numpy as np
|
| 3 |
import matplotlib.pyplot as plt
|
|
|
|
| 4 |
from io import BytesIO
|
| 5 |
import base64
|
| 6 |
import time
|
|
|
|
|
|
|
| 7 |
|
| 8 |
+
def simple_simulation(mode, nx, delta, with_perturbation):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
start_time = time.time()
|
| 10 |
+
|
| 11 |
try:
|
| 12 |
+
# Paramètres très simplifiés
|
|
|
|
| 13 |
L = 500
|
| 14 |
+
nstep = min(50, int(1 / delta)) # Très court
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
time0 = np.linspace(0, delta * nstep, nstep + 1)
|
| 16 |
+
ixe = np.linspace(delta, L, nx)
|
| 17 |
+
|
| 18 |
+
# Simulation ultra-simplifiée
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
if mode == "Boucle ouverte":
|
| 20 |
+
# Signal simple
|
| 21 |
+
u = np.zeros((nx, len(time0)))
|
| 22 |
+
for i, t in enumerate(time0):
|
| 23 |
+
if t > 0.1:
|
| 24 |
+
u[:, i] = 0.01 * np.sin(2 * np.pi * 0.5 * t) * np.sin(np.pi * ixe / L)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
else:
|
| 26 |
+
u[:, i] = 0
|
| 27 |
+
|
| 28 |
+
title = "Simulation Boucle Ouverte"
|
| 29 |
+
|
| 30 |
+
else: # PID
|
| 31 |
+
# Simulation PID ultra-simple
|
| 32 |
+
u = np.zeros((nx, len(time0)))
|
| 33 |
+
for i, t in enumerate(time0):
|
| 34 |
+
if t > 0.1:
|
| 35 |
+
u[:, i] = 0.005 * (1 - np.exp(-2 * t)) * np.sin(np.pi * ixe / L)
|
| 36 |
+
else:
|
| 37 |
+
u[:, i] = 0
|
| 38 |
+
|
| 39 |
+
title = "Simulation PID"
|
| 40 |
+
|
| 41 |
+
# Ajouter bruit si demandé
|
| 42 |
+
if with_perturbation:
|
| 43 |
+
noise = 0.001 * np.random.randn(nx, len(time0))
|
| 44 |
+
u += noise
|
| 45 |
+
|
| 46 |
+
# Créer un simple plot
|
| 47 |
+
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
|
| 48 |
+
|
| 49 |
+
# Plot 1: Déformation finale
|
| 50 |
+
ax1.plot(ixe, u[:, -1], 'b-', linewidth=2)
|
| 51 |
+
ax1.set_title(f'{title} - Déformation finale')
|
| 52 |
+
ax1.set_xlabel('Position x (m)')
|
| 53 |
+
ax1.set_ylabel('Déplacement u (m)')
|
| 54 |
+
ax1.grid(True)
|
| 55 |
+
|
| 56 |
+
# Plot 2: Évolution au temps
|
| 57 |
+
ax2.plot(time0, u[-1, :], 'r-', linewidth=2)
|
| 58 |
+
ax2.set_title(f'{title} - Évolution du tip')
|
| 59 |
+
ax2.set_xlabel('Temps (s)')
|
| 60 |
+
ax2.set_ylabel('Déplacement u_B (m)')
|
| 61 |
+
ax2.grid(True)
|
| 62 |
+
|
| 63 |
+
plt.tight_layout()
|
| 64 |
+
|
| 65 |
+
# Convertir en image
|
| 66 |
buf = BytesIO()
|
| 67 |
+
fig.savefig(buf, format='png', dpi=100)
|
| 68 |
buf.seek(0)
|
| 69 |
+
img_b64 = base64.b64encode(buf.read()).decode('utf-8')
|
| 70 |
plt.close(fig)
|
| 71 |
+
|
| 72 |
+
plot_html = f'<img src="data:image/png;base64,{img_b64}" style="max-width:100%;">'
|
| 73 |
elapsed = f"Temps de calcul: {time.time() - start_time:.2f}s"
|
| 74 |
+
|
| 75 |
+
return plot_html, elapsed
|
| 76 |
+
|
|
|
|
|
|
|
| 77 |
except Exception as e:
|
| 78 |
+
return f"Erreur: {str(e)}", f"Temps: {time.time() - start_time:.2f}s"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
+
# Interface Gradio simplifiée
|
| 81 |
with gr.Blocks() as demo:
|
| 82 |
+
gr.Markdown("# Simulation Robot Souple - Interface Simplifiée")
|
| 83 |
|
| 84 |
+
with gr.Row():
|
| 85 |
+
mode_dropdown = gr.Dropdown(["Boucle ouverte", "PID"], value="Boucle ouverte", label="Mode de simulation")
|
| 86 |
+
|
| 87 |
+
with gr.Row():
|
| 88 |
+
nx_slider = gr.Slider(5, 15, 10, label="Nombre d'éléments (nx)")
|
| 89 |
+
delta_slider = gr.Slider(0.01, 0.1, 0.02, label="Pas de temps (δt)")
|
| 90 |
+
pert_checkbox = gr.Checkbox(False, label="Avec perturbation")
|
| 91 |
+
|
| 92 |
+
with gr.Row():
|
| 93 |
+
run_btn = gr.Button("Lancer Simulation", variant="primary")
|
| 94 |
+
|
| 95 |
+
plot_output = gr.HTML()
|
| 96 |
+
time_output = gr.Textbox(label="Performance")
|
| 97 |
+
|
| 98 |
+
run_btn.click(
|
| 99 |
+
simple_simulation,
|
| 100 |
+
inputs=[mode_dropdown, nx_slider, delta_slider, pert_checkbox],
|
| 101 |
+
outputs=[plot_output, time_output]
|
| 102 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
demo.launch()
|
app_complex.py
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
from matplotlib.animation import FuncAnimation
|
| 5 |
+
from io import BytesIO
|
| 6 |
+
import base64
|
| 7 |
+
import time
|
| 8 |
+
from functools import lru_cache, partial
|
| 9 |
+
from robot_souple_helpers import *
|
| 10 |
+
|
| 11 |
+
@lru_cache(maxsize=10)
|
| 12 |
+
def cached_neb_matrices(nx, dx, E, Ix, rho, S, cy):
|
| 13 |
+
return neb_beam_matrices(nx, dx, E, Ix, rho, S, cy)
|
| 14 |
+
|
| 15 |
+
def generate_animation(ixe, u_history, title="Animation de la déformation"):
|
| 16 |
+
if u_history.shape[1] < 2:
|
| 17 |
+
return "Animation non disponible (pas assez de frames)"
|
| 18 |
+
fig, ax = plt.subplots()
|
| 19 |
+
ax.set_xlim(0, max(ixe))
|
| 20 |
+
ax.set_ylim(np.min(u_history) - 0.1, np.max(u_history) + 0.1)
|
| 21 |
+
line, = ax.plot([], [], lw=2, color='blue')
|
| 22 |
+
ax.set_title(title)
|
| 23 |
+
ax.set_xlabel('Position x (m)')
|
| 24 |
+
ax.set_ylabel('Déplacement u (m)')
|
| 25 |
+
def init():
|
| 26 |
+
line.set_data([], [])
|
| 27 |
+
return line,
|
| 28 |
+
def animate(i):
|
| 29 |
+
line.set_data(ixe, u_history[:, i])
|
| 30 |
+
return line,
|
| 31 |
+
# Further reduce frames for faster generation
|
| 32 |
+
max_frames = 10
|
| 33 |
+
frames = min(u_history.shape[1], max_frames)
|
| 34 |
+
step = max(1, u_history.shape[1] // frames)
|
| 35 |
+
anim = FuncAnimation(fig, animate, init_func=init, frames=range(0, u_history.shape[1], step), interval=300, blit=True)
|
| 36 |
+
buf = BytesIO()
|
| 37 |
+
anim.save(buf, writer='pillow', fps=3)
|
| 38 |
+
buf.seek(0)
|
| 39 |
+
gif = base64.b64encode(buf.read()).decode('utf-8')
|
| 40 |
+
plt.close(fig)
|
| 41 |
+
return f'<img src="data:image/gif;base64,{gif}" alt="Animation">'
|
| 42 |
+
|
| 43 |
+
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"):
|
| 44 |
+
start_time = time.time()
|
| 45 |
+
try:
|
| 46 |
+
beta = 0.25
|
| 47 |
+
gamma = 0.5
|
| 48 |
+
L = 500
|
| 49 |
+
a = 5
|
| 50 |
+
b = 5
|
| 51 |
+
E = 70000
|
| 52 |
+
rho = 2700e-9
|
| 53 |
+
cy = 1e-4
|
| 54 |
+
nstep = min(200, int(2 / delta))
|
| 55 |
+
nA = int(np.floor(nx / 2))
|
| 56 |
+
S = a * b
|
| 57 |
+
Ix = a * b**3 / 12
|
| 58 |
+
dx = L / nx
|
| 59 |
+
time0 = np.linspace(0, delta * nstep, nstep + 1)
|
| 60 |
+
ixe = np.linspace(dx, L, nx)
|
| 61 |
+
vseed1 = 0 if with_pert else 42
|
| 62 |
+
vseed2 = 1 if with_noise else 43
|
| 63 |
+
Kfull, Cfull, Mfull = cached_neb_matrices(nx, dx, E, Ix, rho, S, cy)
|
| 64 |
+
ndo1 = Kfull.shape[0] - 2
|
| 65 |
+
K = Kfull[2:, 2:]
|
| 66 |
+
C = Cfull[2:, 2:]
|
| 67 |
+
M = Mfull[2:, 2:]
|
| 68 |
+
induA = 2 * nA - 2
|
| 69 |
+
induB = 2 * nx - 2
|
| 70 |
+
fpert = generateNoiseTemporal(time0, 1, q, vseed1) if with_pert else np.zeros(len(time0))
|
| 71 |
+
mpert = generateNoiseTemporal(time0, 1, r, vseed2) if with_noise else np.zeros(len(time0))
|
| 72 |
+
settling_time = ""
|
| 73 |
+
|
| 74 |
+
if mode == "Boucle ouverte":
|
| 75 |
+
if solicitation == "échelon":
|
| 76 |
+
fA = Heaviside(time0 - 1)
|
| 77 |
+
elif solicitation == "sinusoïdale":
|
| 78 |
+
fA = np.sin(2 * np.pi * 0.1 * time0)
|
| 79 |
+
else:
|
| 80 |
+
fA = Heaviside(time0 - 1)
|
| 81 |
+
f = np.zeros((ndo1, len(time0)))
|
| 82 |
+
f[induA, :] = fA + fpert
|
| 83 |
+
u0 = np.zeros(ndo1)
|
| 84 |
+
v0 = np.zeros(ndo1)
|
| 85 |
+
a0 = np.linalg.solve(M, f[:, 0] - C @ v0 - K @ u0)
|
| 86 |
+
u, v, a = Newmark(M, C, K, f, u0, v0, a0, delta, beta, gamma)
|
| 87 |
+
uB_final = u[induB, -1]
|
| 88 |
+
threshold = 0.01 * abs(uB_final)
|
| 89 |
+
stable_idx = np.where(np.abs(u[induB, :] - uB_final) < threshold)[0]
|
| 90 |
+
settling_time = time0[stable_idx[0]] if len(stable_idx) > 0 else "Non stabilisé"
|
| 91 |
+
fig, ax = plt.subplots()
|
| 92 |
+
ax.plot(time0, u[induB, :], label='u_B (tip)', linewidth=2)
|
| 93 |
+
ax.plot(time0, fA, label='Consigne f_A', linestyle='--')
|
| 94 |
+
ax.legend()
|
| 95 |
+
ax.set_title(f"Boucle ouverte ({solicitation}) : Déplacement du tip")
|
| 96 |
+
ax.set_xlabel("Temps (s)")
|
| 97 |
+
ax.set_ylabel("Déplacement (m)")
|
| 98 |
+
animation = generate_animation(ixe, u[::2, :], f"Déformation en boucle ouverte ({solicitation})")
|
| 99 |
+
elif mode == "Boucle fermée PID":
|
| 100 |
+
u_ref = Heaviside(time0 - 1)
|
| 101 |
+
u = np.zeros((ndo1, len(time0)))
|
| 102 |
+
v = np.zeros((ndo1, len(time0)))
|
| 103 |
+
a = np.zeros((ndo1, len(time0)))
|
| 104 |
+
u[:, 0] = np.zeros(ndo1)
|
| 105 |
+
v[:, 0] = np.zeros(ndo1)
|
| 106 |
+
a[:, 0] = np.linalg.solve(M, -C @ v[:, 0] - K @ u[:, 0])
|
| 107 |
+
integ_e = 0
|
| 108 |
+
prev_e = 0
|
| 109 |
+
errors = []
|
| 110 |
+
for step in range(1, len(time0)):
|
| 111 |
+
uB_meas = u[induB, step-1] + mpert[step-1]
|
| 112 |
+
e = u_ref[step] - uB_meas
|
| 113 |
+
integ_e += e * delta
|
| 114 |
+
de = (e - prev_e) / delta
|
| 115 |
+
Fpid = Kp * e + Ki * integ_e + Kd * de
|
| 116 |
+
f_k = np.zeros(ndo1)
|
| 117 |
+
f_k[induA] = Fpid + fpert[step]
|
| 118 |
+
u[:, step], v[:, step], a[:, step] = newmark1stepMRHS(M, C, K, f_k, u[:, step-1], v[:, step-1], a[:, step-1], delta, beta, gamma)
|
| 119 |
+
prev_e = e
|
| 120 |
+
errors.append(e)
|
| 121 |
+
u_open = None
|
| 122 |
+
if compare_open:
|
| 123 |
+
fA_comp = Heaviside(time0 - 1)
|
| 124 |
+
f_comp = np.zeros((ndo1, len(time0)))
|
| 125 |
+
f_comp[induA, :] = fA_comp + fpert
|
| 126 |
+
u_open, _, _ = Newmark(M, C, K, f_comp, np.zeros(ndo1), np.zeros(ndo1), np.linalg.solve(M, f_comp[:, 0]), delta, beta, gamma)
|
| 127 |
+
fig, ax = plt.subplots()
|
| 128 |
+
ax.plot(time0, u[induB, :], label='u_B PID', linewidth=2)
|
| 129 |
+
if u_open is not None:
|
| 130 |
+
ax.plot(time0, u_open[induB, :], label='u_B open loop', linestyle='--')
|
| 131 |
+
ax.plot(time0, u_ref, label='Consigne', linestyle='-.')
|
| 132 |
+
ax.legend()
|
| 133 |
+
ax.set_title("Boucle fermée PID : Déplacement u_B")
|
| 134 |
+
ax.set_xlabel("Temps (s)")
|
| 135 |
+
ax.set_ylabel("Déplacement (m)")
|
| 136 |
+
animation = generate_animation(ixe, u[::2, :], "Déformation en boucle fermée")
|
| 137 |
+
elif mode == "Filtre de Kalman":
|
| 138 |
+
n_state = 2 * ndo1
|
| 139 |
+
Q = np.eye(n_state) * q
|
| 140 |
+
R = r
|
| 141 |
+
P = np.eye(n_state) * 1e-3
|
| 142 |
+
x_est = np.zeros(n_state)
|
| 143 |
+
A = Fconstruct(M, C, K, delta, beta, gamma)[:n_state, :n_state]
|
| 144 |
+
B_mat = Bconstruct(M, C, K, nA, delta, beta, gamma)[:n_state, :ndo1]
|
| 145 |
+
H = np.zeros((1, n_state))
|
| 146 |
+
H[0, induB] = 1
|
| 147 |
+
u_sim = np.zeros((ndo1, len(time0)))
|
| 148 |
+
v_sim = np.zeros((ndo1, len(time0)))
|
| 149 |
+
u_sim[:, 0] = np.zeros(ndo1)
|
| 150 |
+
v_sim[:, 0] = np.zeros(ndo1)
|
| 151 |
+
estimates = []
|
| 152 |
+
P_history = [P.copy()]
|
| 153 |
+
for step in range(1, len(time0)):
|
| 154 |
+
f_k = np.zeros(ndo1)
|
| 155 |
+
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)
|
| 156 |
+
z = None
|
| 157 |
+
if not without_measure:
|
| 158 |
+
z = u_sim[induB, step] + mpert[step]
|
| 159 |
+
x_pred = A @ x_est
|
| 160 |
+
P_pred = A @ P @ A.T + Q
|
| 161 |
+
if not without_measure and z is not None:
|
| 162 |
+
K_kal = P_pred @ H.T @ np.linalg.inv(H @ P_pred @ H.T + R)
|
| 163 |
+
x_est = x_pred + K_kal @ (z - H @ x_pred)
|
| 164 |
+
P = (np.eye(n_state) - K_kal @ H) @ P_pred
|
| 165 |
+
else:
|
| 166 |
+
x_est = x_pred
|
| 167 |
+
P = P_pred
|
| 168 |
+
estimates.append(x_est[induB])
|
| 169 |
+
P_history.append(P.copy())
|
| 170 |
+
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
|
| 171 |
+
ax1.plot(time0[1:], estimates, label='Estimation Kalman', linewidth=2)
|
| 172 |
+
ax1.plot(time0, u_sim[induB, :], label='Vraie u_B', linestyle='--')
|
| 173 |
+
ax1.legend()
|
| 174 |
+
ax1.set_title("Estimation de u_B")
|
| 175 |
+
ax1.set_xlabel("Temps (s)")
|
| 176 |
+
ax1.set_ylabel("Déplacement (m)")
|
| 177 |
+
cov_uB = [P[induB, induB] for P in P_history]
|
| 178 |
+
ax2.plot(time0, cov_uB, label='Covariance u_B', linewidth=2)
|
| 179 |
+
ax2.legend()
|
| 180 |
+
ax2.set_title("Évolution de la covariance")
|
| 181 |
+
ax2.set_xlabel("Temps (s)")
|
| 182 |
+
ax2.set_ylabel("Variance")
|
| 183 |
+
animation = "Animation non disponible pour Kalman"
|
| 184 |
+
else:
|
| 185 |
+
return "Mode non implémenté", "", f"Temps: {time.time() - start_time:.2f}s"
|
| 186 |
+
|
| 187 |
+
buf = BytesIO()
|
| 188 |
+
fig.savefig(buf, format="png", dpi=100)
|
| 189 |
+
buf.seek(0)
|
| 190 |
+
plot_b64 = base64.b64encode(buf.read()).decode('utf-8')
|
| 191 |
+
plt.close(fig)
|
| 192 |
+
plot_html = f'<img src="data:image/png;base64,{plot_b64}" style="max-width:100%;">'
|
| 193 |
+
elapsed = f"Temps de calcul: {time.time() - start_time:.2f}s"
|
| 194 |
+
if mode == "Boucle ouverte":
|
| 195 |
+
info_str = f"{elapsed}, Stabilité: {settling_time}"
|
| 196 |
+
else:
|
| 197 |
+
info_str = elapsed
|
| 198 |
+
return plot_html, animation, info_str
|
| 199 |
+
except Exception as e:
|
| 200 |
+
return f"Erreur: {str(e)}", "", f"Temps: {time.time() - start_time:.2f}s"
|
| 201 |
+
|
| 202 |
+
def generate_schema():
|
| 203 |
+
fig, ax = plt.subplots(figsize=(10, 6))
|
| 204 |
+
ax.text(0.1, 0.8, 'i', fontsize=14, ha='center')
|
| 205 |
+
ax.arrow(0.15, 0.8, 0.1, 0, head_width=0.03, head_length=0.03, fc='k', ec='k')
|
| 206 |
+
ax.text(0.3, 0.8, 'Moteur', fontsize=14, ha='center', bbox=dict(boxstyle="round,pad=0.3", facecolor="lightblue"))
|
| 207 |
+
ax.arrow(0.4, 0.8, 0.1, 0, head_width=0.03, head_length=0.03, fc='k', ec='k')
|
| 208 |
+
ax.text(0.55, 0.8, 'F_m', fontsize=14, ha='center')
|
| 209 |
+
ax.arrow(0.6, 0.8, 0.1, 0, head_width=0.03, head_length=0.03, fc='k', ec='k')
|
| 210 |
+
ax.text(0.75, 0.8, 'Transmission', fontsize=14, ha='center', bbox=dict(boxstyle="round,pad=0.3", facecolor="lightgreen"))
|
| 211 |
+
ax.arrow(0.85, 0.8, 0.1, 0, head_width=0.03, head_length=0.03, fc='k', ec='k')
|
| 212 |
+
ax.text(0.95, 0.8, 'F_t', fontsize=14, ha='center')
|
| 213 |
+
ax.arrow(0.15, 0.5, 0.1, 0, head_width=0.03, head_length=0.03, fc='k', ec='k')
|
| 214 |
+
ax.text(0.3, 0.5, 'C_m', fontsize=14, ha='center')
|
| 215 |
+
ax.arrow(0.4, 0.5, 0.1, 0, head_width=0.03, head_length=0.03, fc='k', ec='k')
|
| 216 |
+
ax.text(0.55, 0.5, 'θ', fontsize=14, ha='center')
|
| 217 |
+
ax.arrow(0.6, 0.5, 0.1, 0, head_width=0.03, head_length=0.03, fc='k', ec='k')
|
| 218 |
+
ax.text(0.75, 0.5, 'f_A', fontsize=14, ha='center')
|
| 219 |
+
ax.arrow(0.85, 0.5, 0.1, 0, head_width=0.03, head_length=0.03, fc='k', ec='k')
|
| 220 |
+
ax.text(0.95, 0.5, 'u_A', fontsize=14, ha='center')
|
| 221 |
+
ax.arrow(0.5, 0.3, 0, -0.1, head_width=0.03, head_length=0.03, fc='k', ec='k')
|
| 222 |
+
ax.text(0.5, 0.2, 'Poutre Flexible', fontsize=14, ha='center', bbox=dict(boxstyle="round,pad=0.3", facecolor="lightcoral"))
|
| 223 |
+
ax.arrow(0.5, 0.1, 0, -0.05, head_width=0.03, head_length=0.03, fc='k', ec='k')
|
| 224 |
+
ax.text(0.5, 0.05, 'u_B', fontsize=14, ha='center')
|
| 225 |
+
ax.set_xlim(0, 1)
|
| 226 |
+
ax.set_ylim(0, 1)
|
| 227 |
+
ax.axis('off')
|
| 228 |
+
buf = BytesIO()
|
| 229 |
+
fig.savefig(buf, format="png", dpi=100)
|
| 230 |
+
buf.seek(0)
|
| 231 |
+
img_b64 = base64.b64encode(buf.read()).decode('utf-8')
|
| 232 |
+
plt.close(fig)
|
| 233 |
+
return f'<img src="data:image/png;base64,{img_b64}" style="max-width:100%;">'
|
| 234 |
+
|
| 235 |
+
def compute_ft_gt(rendement, cm, theta_dot, fa_dot, ua_dot):
|
| 236 |
+
if rendement == 1:
|
| 237 |
+
ft = cm * theta_dot
|
| 238 |
+
gt = fa_dot * ua_dot
|
| 239 |
+
equal = "Oui" if np.isclose(ft, gt) else "Non"
|
| 240 |
+
return f"F_t = {ft:.3f}, G_t = {gt:.3f}, Égal: {equal}"
|
| 241 |
+
else:
|
| 242 |
+
return r"Pour rendement unitaire, F_t = C_m * \dot{\theta}, G_t = f_A * \dot{u_A}"
|
| 243 |
+
|
| 244 |
+
def quiz_answer(choice):
|
| 245 |
+
if choice == "Estimer u_B sans mesure directe":
|
| 246 |
+
return "Correct ! Le jumeau permet d'estimer états non mesurés."
|
| 247 |
+
else:
|
| 248 |
+
return "Incorrect. Réessayez."
|
| 249 |
+
|
| 250 |
+
desc_prep = r"""
|
| 251 |
+
### Travail Préparatoire (Section 4.1 des rapports)
|
| 252 |
+
É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}).
|
| 253 |
+
Intérêt du jumeau : Estimer u_B sans mesure directe pour contrôle en boucle fermée.
|
| 254 |
+
"""
|
| 255 |
+
desc_open = """
|
| 256 |
+
### Boucle Ouverte (Section 4.2.1)
|
| 257 |
+
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).
|
| 258 |
+
"""
|
| 259 |
+
desc_pid = """
|
| 260 |
+
### Boucle Fermée PID
|
| 261 |
+
Contrôle avec PID pour suivre la consigne. Observez l'erreur de suivi.
|
| 262 |
+
"""
|
| 263 |
+
desc_kalman = """
|
| 264 |
+
### Filtre de Kalman
|
| 265 |
+
Estimation d'état avec filtre Kalman pour filtrer le bruit sur la mesure u_B.
|
| 266 |
+
"""
|
| 267 |
+
|
| 268 |
+
with gr.Blocks() as demo:
|
| 269 |
+
gr.Markdown("# Jumeaux Numériques : Simulation de Robot Souple\nDémo interactive basée sur le cours de Renaud Ferrier - Centrale Lyon ENISE.")
|
| 270 |
+
|
| 271 |
+
with gr.Tabs():
|
| 272 |
+
with gr.Tab("Préparatoire"):
|
| 273 |
+
gr.Markdown(desc_prep)
|
| 274 |
+
gr.Markdown("### Schéma Bloc du Système")
|
| 275 |
+
schema_img = gr.HTML(value=generate_schema())
|
| 276 |
+
gr.Markdown("### Conservation d'Énergie : F_t = G_t")
|
| 277 |
+
with gr.Row():
|
| 278 |
+
rendement_slider = gr.Slider(0.5, 1, 1, label="Rendement (η)", info="Hypothèse rendement unitaire pour conservation")
|
| 279 |
+
cm_input = gr.Number(1e-3, label="C_m (constante moteur)")
|
| 280 |
+
theta_dot_input = gr.Number(10, label="\\dot{\\theta} (vitesse angulaire)")
|
| 281 |
+
fa_dot_input = gr.Number(100, label="f_A (force en A)")
|
| 282 |
+
ua_dot_input = gr.Number(0.1, label="\\dot{u_A} (vitesse en A)")
|
| 283 |
+
compute_btn = gr.Button("Calculer F_t et G_t")
|
| 284 |
+
ft_gt_output = gr.Textbox(label="Résultat")
|
| 285 |
+
compute_btn.click(compute_ft_gt, inputs=[rendement_slider, cm_input, theta_dot_input, fa_dot_input, ua_dot_input], outputs=ft_gt_output)
|
| 286 |
+
gr.Markdown("### Intérêt du Jumeau Numérique")
|
| 287 |
+
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 ?")
|
| 288 |
+
quiz_btn = gr.Button("Vérifier")
|
| 289 |
+
quiz_output = gr.Textbox()
|
| 290 |
+
quiz_btn.click(quiz_answer, inputs=quiz_radio, outputs=quiz_output)
|
| 291 |
+
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.")
|
| 292 |
+
|
| 293 |
+
with gr.Tab("Boucle ouverte"):
|
| 294 |
+
gr.Markdown(desc_open)
|
| 295 |
+
solicitation_dropdown = gr.Dropdown(["échelon", "sinusoïdale"], value="échelon", label="Type de sollicitation", info="Échelon à t=1s ou sinusoïdale")
|
| 296 |
+
with gr.Row():
|
| 297 |
+
nx_slider = gr.Slider(5, 20, 10, 1, label="nx (éléments)", info="Précision poutre")
|
| 298 |
+
delta_slider = gr.Slider(0.001, 0.05, 0.01, label="δt (pas temps)")
|
| 299 |
+
q_slider = gr.Slider(0, 0.001, 0.0001, label="q (variance perturbation)")
|
| 300 |
+
with_pert = gr.Checkbox(True, label="Perturbation ?")
|
| 301 |
+
compare_open_open = gr.Checkbox(False, visible=False)
|
| 302 |
+
without_measure_open = gr.Checkbox(False, visible=False)
|
| 303 |
+
mode_open = gr.Textbox("Boucle ouverte", visible=False)
|
| 304 |
+
run_open = gr.Button("Simuler")
|
| 305 |
+
plot_open = gr.HTML()
|
| 306 |
+
anim_open = gr.HTML()
|
| 307 |
+
time_open = gr.Textbox(label="Info")
|
| 308 |
+
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])
|
| 309 |
+
|
| 310 |
+
with gr.Tab("Boucle fermée PID"):
|
| 311 |
+
gr.Markdown(desc_pid)
|
| 312 |
+
with gr.Row():
|
| 313 |
+
nx_pid = gr.Slider(5, 20, 10, 1, label="nx")
|
| 314 |
+
delta_pid = gr.Slider(0.001, 0.05, 0.01, label="δt")
|
| 315 |
+
r_pid = gr.Slider(0, 0.001, 0.0001, label="r (bruit mesure)")
|
| 316 |
+
Kp_slider = gr.Slider(0, 10, 0.5, label="Kp")
|
| 317 |
+
Ki_slider = gr.Slider(0, 1, 0.1, label="Ki")
|
| 318 |
+
Kd_slider = gr.Slider(0, 0.1, 0.01, label="Kd")
|
| 319 |
+
with_noise_pid = gr.Checkbox(True, label="Bruit mesure ?")
|
| 320 |
+
compare_open_pid = gr.Checkbox(False, label="Comparer avec boucle ouverte")
|
| 321 |
+
solicitation_pid = gr.Textbox("échelon", visible=False)
|
| 322 |
+
without_measure_pid = gr.Checkbox(False, visible=False)
|
| 323 |
+
mode_pid = gr.Textbox("Boucle fermée PID", visible=False)
|
| 324 |
+
run_pid = gr.Button("Simuler")
|
| 325 |
+
plot_pid = gr.HTML()
|
| 326 |
+
anim_pid = gr.HTML()
|
| 327 |
+
time_pid = gr.Textbox()
|
| 328 |
+
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])
|
| 329 |
+
|
| 330 |
+
with gr.Tab("Filtre de Kalman"):
|
| 331 |
+
gr.Markdown(desc_kalman)
|
| 332 |
+
with gr.Row():
|
| 333 |
+
nx_kal = gr.Slider(5, 15, 10, 1, label="nx")
|
| 334 |
+
delta_kal = gr.Slider(0.001, 0.05, 0.01, label="δt")
|
| 335 |
+
q_kal = gr.Slider(0, 0.001, 0.0001, label="q (bruit processus)")
|
| 336 |
+
r_kal = gr.Slider(0, 0.001, 0.0001, label="r (bruit mesure)")
|
| 337 |
+
without_measure_kal = gr.Checkbox(False, label="Sans mesure u_B (jumeau)")
|
| 338 |
+
solicitation_kal = gr.Textbox("échelon", visible=False)
|
| 339 |
+
compare_open_kal = gr.Checkbox(False, visible=False)
|
| 340 |
+
mode_kal = gr.Textbox("Filtre de Kalman", visible=False)
|
| 341 |
+
run_kal = gr.Button("Simuler")
|
| 342 |
+
plot_kal = gr.HTML()
|
| 343 |
+
anim_kal = gr.HTML()
|
| 344 |
+
time_kal = gr.Textbox()
|
| 345 |
+
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])
|
| 346 |
+
|
| 347 |
+
demo.launch()
|
requirements.txt
CHANGED
|
@@ -1,6 +1,3 @@
|
|
| 1 |
gradio==4.32.0
|
| 2 |
numpy==1.24.3
|
| 3 |
-
matplotlib==3.7.2
|
| 4 |
-
scipy==1.10.1
|
| 5 |
-
pillow==9.5.0
|
| 6 |
-
huggingface_hub==0.19.4
|
|
|
|
| 1 |
gradio==4.32.0
|
| 2 |
numpy==1.24.3
|
| 3 |
+
matplotlib==3.7.2
|
|
|
|
|
|
|
|
|
requirements_complex.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio==4.32.0
|
| 2 |
+
numpy==1.24.3
|
| 3 |
+
matplotlib==3.7.2
|
| 4 |
+
scipy==1.10.1
|
| 5 |
+
pillow==9.5.0
|
| 6 |
+
huggingface_hub==0.19.4
|