ACTIVITE 3 – Utilisation de solve_ivp() pour le pendule aux grands angles

Chargement de Python... Veuillez patienter.

I - La fonction solve_ivp

Nous reprenons la résolution numérique d'équations différentielles, étudiée lors de la séance précédente. Nous avons utilisé la méthode d'Euler, que nous avons codé nous-même en Python. En fait, il existe une fonction solve_ivp déjà implémentée dans la bibliothèque scipy qui permet de résoudre numériquement des équations différentielles (mais toujours d'ordre 1 seulement !), avec un algorithme plus performant que celui d'Euler.

Pour s'entraîner à l'utiliser sur un exemple simple, reprendre le cas du circuit RC série de la séance précédente, et résoudre numériquement l'équation différentielle avec solve_ivp pour déterminer la tension \(u_C(t)\) aux bornes du condensateur entre \(t=0\) et \(t=5RC\).

Commenter la solution obtenue : vous semble-t-elle correcte ?

II - Pendule simple aux grands angles

Cette fois-ci, nous allons nous intéresser à un système physique régit par une équation différentielle qui n'admet pas de solution analytique : le pendule simple.
Les conditions initiales seront les suivantes : \[ \begin{Bmatrix} \theta(0)=\theta_0 \\ \dot{\theta}(0)=0 \end{Bmatrix} \]L'angle initial \(\theta_0\) ne sera pas nécessairement petit devant 1 radian.
La longueur du fil sera de \(L=20\text{ cm}\). La pesanteur vaut \(g=9,8\text{ m.s}^{-2}\). On pourra poser \(\omega_0=\sqrt{\frac{g}{L}}\).

Quelle est l'équation différentielle vérifiée par l'angle \(\theta(t)\) ?

En vous aidant de l'énoncé papier, utiliser solve_ivp pour résoudre numériquement cette équation différentielle entre \(t=0\) et \(t=6\times\frac{2\pi}{\omega_0}\).

On pourra tester plusieurs valeurs d'angles initial \(\theta_0\), éventuellement très proches de \(\pi\) (\(\theta_0 = 0,5\pi\), \(0,8\pi\), \(0,9\pi\), \(0,99\pi\)...).

Que pensez-vous de l'allure des solutions pour des "grands" angles de départ ?
Dans l'approximation des petits angles, on rappelle qu'il y a isochronisme des oscillations. Qu'est-ce que cela signifie ?
Que vaut la période des oscillations, dans ce cas ?
T=

Est-ce encore le cas quand on n'est plus aux "petits angles" ? Pour le savoir, nous allons superposer les courbes de \(\theta(t)\) sur le même graphe, pour 10 valeurs différentes d'angle initial \(\theta_0\) échelonnées entre \(0\) et \(\pi\).
Quelques conseils pour vous aider :

  • Faire un copier-coller de la cellule précédente dans la cellule vide ci-dessous.
  • Remplacer la valeur unique la condition initiale theta0 par un tableau numpy contenant 10 valeurs de conditions initiales échelonnées entre \(0\) et \(\pi\).[Note]
  • Insérer l'instruction solution = solve_ivp() dans une boucle, en l'adaptant.
  • Déplacer l'instruction plt.plot() à l'intérieur de cette boucle, pour qu'une nouvelle courbe s'affiche à chaque valeur de \(\theta_0\).
Attention : l'instruction fig,ax=plt.subplots() (qui crée la nouvelle zone de graphique) doit être située avant les plt.plot(). La déplacer au début du programme.
Pour mieux voir, on pourra tracer sur une durée entre \(t=0\) et \(t_f=3\frac{2\pi}{\omega_0}\), au lieu de \(t_f=6\frac{2\pi}{\omega_0}\) précédemment.

Commenter : y a-t-il isochronisme des oscillations ?
terminal = false packages = ["numpy", "matplotlib", "scipy"] [[fetch]] files = [] import sys import io import traceback from js import document, console, window from pyodide.ffi import create_proxy import base64 # Configuration matplotlib pour PyScript import matplotlib matplotlib.use('Agg') # Backend non-interactif pour PyScript import matplotlib.pyplot as plt # Contexte global partagé entre toutes les cellules global_context = {'plt': plt, 'matplotlib': matplotlib} def capture_output(func, *args, **kwargs): """Capture la sortie d'une fonction""" old_stdout = sys.stdout old_stderr = sys.stderr stdout_capture = io.StringIO() stderr_capture = io.StringIO() sys.stdout = stdout_capture sys.stderr = stderr_capture try: result = func(*args, **kwargs) stdout_output = stdout_capture.getvalue() stderr_output = stderr_capture.getvalue() return result, stdout_output, stderr_output finally: sys.stdout = old_stdout sys.stderr = old_stderr def handle_matplotlib_figures(output_element): """Gère l'affichage des figures matplotlib""" current_figures = plt.get_fignums() if current_figures: # Créer ou récupérer le conteneur des figures figures_container = output_element.querySelector('.figures-container') if not figures_container: figures_container = document.createElement('div') figures_container.className = 'figures-container' figures_container.style.cssText = ''' display: flex; flex-wrap: wrap; gap: 15px; margin: 10px 0; justify-content: center; ''' output_element.appendChild(figures_container) for fig_num in current_figures: fig = plt.figure(fig_num) # Convertir la figure en image base64 buf = io.BytesIO() fig.savefig(buf, format='png', bbox_inches='tight', dpi=150) buf.seek(0) img_data = base64.b64encode(buf.read()).decode() buf.close() # Créer un conteneur pour la figure avec bouton de suppression figure_wrapper = document.createElement('div') figure_wrapper.className = 'figure-wrapper' figure_wrapper.style.cssText = ''' position: relative; display: inline-block; border: 1px solid #ddd; border-radius: 8px; padding: 10px; background: white; box-shadow: 0 2px 5px rgba(0,0,0,0.1); margin: 5px; ''' # Créer l'élément img pour afficher la figure img_element = document.createElement('img') img_element.src = f'data:image/png;base64,{img_data}' img_element.className = 'matplotlib-figure' img_element.style.cssText = ''' max-width: 300px; max-height: 250px; height: auto; display: block; border-radius: 4px; ''' # Créer le bouton de suppression delete_btn = document.createElement('button') delete_btn.innerHTML = '×' delete_btn.className = 'figure-delete-btn' delete_btn.style.cssText = ''' position: absolute; top: 5px; right: 5px; width: 25px; height: 25px; border: none; border-radius: 50%; background: rgba(220, 53, 69, 0.8); color: white; font-size: 16px; font-weight: bold; cursor: pointer; display: flex; align-items: center; justify-content: center; line-height: 1; z-index: 10; ''' delete_btn.title = 'Supprimer cette figure' # Ajouter l'événement de suppression def create_delete_handler(wrapper): def delete_figure(event): wrapper.remove() return delete_figure delete_btn.onclick = create_delete_handler(figure_wrapper) # Assembler la figure figure_wrapper.appendChild(img_element) figure_wrapper.appendChild(delete_btn) figures_container.appendChild(figure_wrapper) # Fermer toutes les figures pour éviter les accumulations plt.close('all') return True return False def execute_python_code(code, cell_id): """Exécute le code Python et affiche le résultat""" output_element = document.querySelector(f'#output-{cell_id}') loading_element = document.querySelector(f'#loading-{cell_id}') # Afficher le loading loading_element.style.display = 'block' # Vérifier s'il y a déjà des figures figures_container = output_element.querySelector('.figures-container') if figures_container: # S'il y a des figures, effacer seulement le contenu texte text_outputs = output_element.querySelectorAll('.text-output') for text_output in text_outputs: text_output.remove() # Réinitialiser le style de base sans effacer les figures output_element.className = 'output' # Vider seulement le texte direct, pas les éléments enfants nodes_to_remove = [] for node in output_element.childNodes: if node.nodeType == 3: # Text node nodes_to_remove.append(node) for node in nodes_to_remove: node.remove() else: # Pas de figures, effacer tout output_element.innerHTML = '' output_element.className = 'output' try: def run_code(): # Exécuter le code dans le contexte global exec(code, global_context) # Si la dernière ligne est une expression (pas un statement), l'évaluer et retourner le résultat lines = code.strip().split('\n') if lines: last_line = lines[-1].strip() # Vérifier que ce n'est pas un statement Python commun if (last_line and not last_line.startswith((' ', '\t')) and not last_line.endswith(':') and not last_line.startswith(('print(', 'import ', 'from ', 'def ', 'class ', 'if ', 'for ', 'while ', 'try:', 'except', 'with ', 'assert ', 'del ', 'pass', 'break', 'continue', 'return', 'yield', 'raise', 'global ', 'nonlocal ', 'plt.', 'fig')) and '=' not in last_line.split('#')[0]): # Éviter les assignments try: return eval(last_line, global_context) except: pass return None result, stdout_output, stderr_output = capture_output(run_code) # Gérer l'affichage des figures matplotlib (s'ajoute aux existantes) has_figures = handle_matplotlib_figures(output_element) # Construire la sortie texte output = "" if stdout_output: output += stdout_output if stderr_output: output += stderr_output if result is not None: output += str(result) if output: # S'il y a déjà des figures, ajouter le texte à la suite if has_figures: text_div = document.createElement('div') text_div.className = 'text-output' text_div.textContent = output output_element.appendChild(text_div) else: output_element.textContent = output elif not has_figures: output_element.textContent = "✓ Code exécuté avec succès" output_element.className = 'output success' except Exception as e: error_output = f"❌ Erreur:\n{str(e)}\n\n{traceback.format_exc()}" output_element.textContent = error_output output_element.className = 'output error' finally: loading_element.style.display = 'none' # Exposer les fonctions à JavaScript window.execute_python_code = create_proxy(execute_python_code) # Signaler que PyScript est prêt console.log("PyScript ready!") try: # Activer l'interface immédiatement status_element = document.querySelector('#pyscript-status') if status_element: status_element.innerHTML = 'Python est prêt ! Vous pouvez maintenant exécuter votre code.' status_element.className = 'pyscript-status pyscript-ready' # Activer tous les boutons d'exécution run_buttons = document.querySelectorAll('.btn-run') for button in run_buttons: button.disabled = False # Notifier JavaScript window.pyScriptIsReady = True except Exception as e: console.log(f"Error during PyScript initialization: {e}")