File size: 18,453 Bytes
b078a34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
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'<img src="data:image/gif;base64,{gif}" alt="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'<img src="data:image/png;base64,{plot_b64}" style="max-width:100%;">'
        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'<img src="data:image/png;base64,{img_b64}" style="max-width:100%;">'

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