.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "gallery/post-processing/example_mode_shape_animation.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code. .. rst-class:: sphx-glr-example-title .. _sphx_glr_gallery_post-processing_example_mode_shape_animation.py: Mode-shape animation — cantilever bending modes =============================================== A mode shape :math:`\phi_{i}` is the spatial pattern of a structure's :math:`i`-th free-vibration response. When the structure is excited at the corresponding natural frequency :math:`f_{i}`, every point moves in phase with amplitude proportional to :math:`\phi_{i}` and time profile .. math:: :label: mode-time-history \mathbf{u}(\mathbf{x}, t) \;=\; A\,\phi_{i}(\mathbf{x})\,\cos\!\bigl(2\pi f_{i} t + \theta\bigr). So an "animation" of mode :math:`i` is just a parametric sweep over phase :math:`\omega t \in [0, 2\pi]`, with the displacement field scaled by :math:`\cos(\omega t)` at each frame. The static plot :doc:`example_mode_shape_plot` shows one snapshot per mode; this example shows the *time evolution* of a single mode across one full period — the key visual that makes mode shapes click for new users. The standard recipe: 1. ``modal_result_to_grid(model, result)`` scatters every mode onto the mesh as a ``mode_{k}_disp`` point-data vector. 2. For each animation frame, ``grid.warp_by_vector("mode_k_disp", factor=A·cos(ωt))`` deforms the geometry by the phase-scaled shape. 3. Render either as a filmstrip (sphinx-gallery friendly) or as a true GIF/MP4 with :meth:`pyvista.Plotter.open_gif` / :meth:`pyvista.Plotter.write_frame`. Step 3 is the only one that branches: the gallery shows a filmstrip because the doc build is offscreen, but the script writes a GIF too — running this file locally produces ``mode_1_animation.gif`` next to the source. Implementation -------------- Slender HEX8 cantilever (clamped at :math:`x = 0`). Solve six modes, pick mode 1 (first transverse bending), filmstrip eight evenly-spaced phases over one period. Mode-shape amplitude is normalised so the peak displacement reaches 8 % of the beam length — a visually readable scale that is also physically meaningless (mode shapes carry no absolute magnitude; the norm choice is arbitrary). References ---------- * Bathe, K.-J. (2014) *Finite Element Procedures*, 2nd ed., Prentice Hall, §10.2 — undamped free vibration. * Chopra, A. K. (2017) *Dynamics of Structures*, 5th ed., Pearson, §10.2 — free-vibration response of MDOF systems. * Cook, R. D., Malkus, D. S., Plesha, M. E., Witt, R. J. (2002) *Concepts and Applications of Finite Element Analysis*, 4th ed., Wiley, §11.3 — eigenproblems of structural dynamics. .. GENERATED FROM PYTHON SOURCE LINES 62-74 .. code-block:: Python from __future__ import annotations from pathlib import Path import numpy as np import pyvista as pv import femorph_solver from femorph_solver import ELEMENTS from femorph_solver.io import modal_result_to_grid .. GENERATED FROM PYTHON SOURCE LINES 75-77 Build a slender cantilever beam (HEX8 EAS) ------------------------------------------ .. GENERATED FROM PYTHON SOURCE LINES 77-101 .. code-block:: Python E = 2.0e11 NU = 0.30 RHO = 7850.0 L = 4.0 WIDTH = 0.05 HEIGHT = 0.05 NX, NY, NZ = 60, 3, 3 xs = np.linspace(0.0, L, NX + 1) ys = np.linspace(0.0, WIDTH, NY + 1) zs = np.linspace(0.0, HEIGHT, NZ + 1) grid = pv.StructuredGrid(*np.meshgrid(xs, ys, zs, indexing="ij")).cast_to_unstructured_grid() m = femorph_solver.Model.from_grid(grid) m.assign( ELEMENTS.HEX8(integration="enhanced_strain"), material={"EX": E, "PRXY": NU, "DENS": RHO}, ) pts = np.asarray(m.grid.points) clamped = np.where(pts[:, 0] < 1e-9)[0] m.fix(nodes=(clamped + 1).tolist(), dof="ALL") .. GENERATED FROM PYTHON SOURCE LINES 102-104 Modal solve + scatter onto the grid ----------------------------------- .. GENERATED FROM PYTHON SOURCE LINES 104-117 .. code-block:: Python N_MODES = 6 res = m.solve_modal(n_modes=N_MODES) freqs = np.asarray(res.frequency, dtype=np.float64) print("Cantilever beam — first six natural frequencies") for i, f in enumerate(freqs): print(f" mode {i + 1}: f = {f:7.3f} Hz") # ``modal_result_to_grid`` attaches one ``mode_{k}_disp`` vector and # one ``mode_{k}_magnitude`` scalar per mode. ``scale`` is uniform — # we'll choose a per-mode amplitude later when we render. grid_modes = modal_result_to_grid(m, res, scale=1.0) .. rst-class:: sphx-glr-script-out .. code-block:: none Cantilever beam — first six natural frequencies mode 1: f = 2.554 Hz mode 2: f = 2.554 Hz mode 3: f = 16.005 Hz mode 4: f = 16.005 Hz mode 5: f = 44.825 Hz mode 6: f = 44.825 Hz .. GENERATED FROM PYTHON SOURCE LINES 118-125 Pick the visualisation amplitude -------------------------------- Mode shapes are mass-normalised, so the magnitudes look tiny in absolute units. Pick a render amplitude such that the peak displacement equals 8 % of the beam length — independent of the eigenvector norm, this gives a consistent visual. .. GENERATED FROM PYTHON SOURCE LINES 125-137 .. code-block:: Python mode_index = 1 # animate mode 1 (first transverse bending) disp = np.asarray(grid_modes.point_data[f"mode_{mode_index}_disp"]) peak = float(np.linalg.norm(disp, axis=1).max()) target_peak = 0.08 * L # 8 % of beam length amp = target_peak / peak if peak > 0 else 1.0 print( f"\n Animating mode {mode_index} at f = {freqs[mode_index - 1]:.3f} Hz, " f"amplitude scaled so peak displacement = {target_peak:.3f} m " f"({100 * target_peak / L:.1f}% of L)" ) .. rst-class:: sphx-glr-script-out .. code-block:: none Animating mode 1 at f = 2.554 Hz, amplitude scaled so peak displacement = 0.320 m (8.0% of L) .. GENERATED FROM PYTHON SOURCE LINES 138-144 Filmstrip: eight phase snapshots over one period ------------------------------------------------ Sphinx-gallery captures static images, so the rendered output is a 2 x 4 filmstrip of warps at ``ωt = 0, π/4, π/2, …, 7π/4``. Running this script locally also writes a real GIF — see the next cell. .. GENERATED FROM PYTHON SOURCE LINES 144-168 .. code-block:: Python n_frames = 8 phases = np.linspace(0.0, 2.0 * np.pi, n_frames, endpoint=False) plotter = pv.Plotter(shape=(2, 4), off_screen=True, window_size=(1200, 540), border=False) for k, phase in enumerate(phases): row, col = divmod(k, 4) plotter.subplot(row, col) factor = amp * float(np.cos(phase)) warped = grid_modes.warp_by_vector(f"mode_{mode_index}_disp", factor=factor) plotter.add_mesh( warped, scalars=f"mode_{mode_index}_magnitude", cmap="viridis", clim=(0.0, peak), show_edges=False, show_scalar_bar=False, ) plotter.add_text(f"ωt = {phase / np.pi:.2f}π", position="upper_left", font_size=10) plotter.view_xy() plotter.camera.zoom(1.4) plotter.link_views() plotter.show() .. image-sg:: /gallery/post-processing/images/sphx_glr_example_mode_shape_animation_001.png :alt: example mode shape animation :srcset: /gallery/post-processing/images/sphx_glr_example_mode_shape_animation_001.png :class: sphx-glr-single-img .. GENERATED FROM PYTHON SOURCE LINES 169-179 Write a real GIF for users running this script directly ------------------------------------------------------- The block below produces a 24-frame GIF that loops over one full period — this is the artefact you actually want to embed in a report or a slide. Sphinx-gallery captures only the static filmstrip above, so the GIF lands in :func:`tempfile.gettempdir` rather than the source tree (the printed path tells you exactly where). GIF writing uses pyvista's :meth:`~pyvista.Plotter.open_gif` / :meth:`~pyvista.Plotter.write_frame` pair and requires ``imageio`` (already pulled in by pyvista). .. GENERATED FROM PYTHON SOURCE LINES 179-218 .. code-block:: Python import importlib.util # noqa: E402 import tempfile # noqa: E402 if importlib.util.find_spec("imageio") is None: print( "\n imageio not installed — skipping GIF write. " "Install with `pip install imageio` to enable Plotter.open_gif." ) else: out_path = Path(tempfile.gettempdir()) / f"mode_{mode_index}_animation.gif" n_gif_frames = 24 gif_phases = np.linspace(0.0, 2.0 * np.pi, n_gif_frames, endpoint=False) p2 = pv.Plotter(off_screen=True, window_size=(720, 480)) p2.view_xy() p2.camera.zoom(1.3) p2.open_gif(str(out_path)) for phase in gif_phases: p2.clear_actors() factor = amp * float(np.cos(phase)) warped = grid_modes.warp_by_vector(f"mode_{mode_index}_disp", factor=factor) p2.add_mesh( warped, scalars=f"mode_{mode_index}_magnitude", cmap="viridis", clim=(0.0, peak), show_edges=False, show_scalar_bar=False, ) p2.add_text( f"mode {mode_index} — f = {freqs[mode_index - 1]:.2f} Hz", position="upper_left", font_size=11, ) p2.write_frame() p2.close() print(f"\n GIF written to {out_path}") .. rst-class:: sphx-glr-script-out .. code-block:: none imageio not installed — skipping GIF write. Install with `pip install imageio` to enable Plotter.open_gif. .. GENERATED FROM PYTHON SOURCE LINES 219-221 Take-aways ---------- .. GENERATED FROM PYTHON SOURCE LINES 221-244 .. code-block:: Python print() print("Take-aways:") print( " • A mode shape is a spatial pattern; its time evolution at the natural " "frequency is u(x,t) = A·φ(x)·cos(2π f t). Animation = phase sweep over " "ωt ∈ [0, 2π]." ) print( " • modal_result_to_grid(m, res) attaches per-mode vector arrays " "(mode_k_disp); pyvista's warp_by_vector(factor=A·cos(ωt)) deforms the " "mesh by the phase-scaled shape." ) print( " • Mode-shape magnitudes are arbitrary (mass-normalised eigenvectors). " "Pick the render amplitude to be a fraction of the structure's bounding " "box — never raw eigenvector units." ) print( " • For a true animation, use Plotter.open_gif(path) + write_frame() in " "an off-screen plotter; the same loop that fills the filmstrip produces " "the GIF frames." ) .. rst-class:: sphx-glr-script-out .. code-block:: none Take-aways: • A mode shape is a spatial pattern; its time evolution at the natural frequency is u(x,t) = A·φ(x)·cos(2π f t). Animation = phase sweep over ωt ∈ [0, 2π]. • modal_result_to_grid(m, res) attaches per-mode vector arrays (mode_k_disp); pyvista's warp_by_vector(factor=A·cos(ωt)) deforms the mesh by the phase-scaled shape. • Mode-shape magnitudes are arbitrary (mass-normalised eigenvectors). Pick the render amplitude to be a fraction of the structure's bounding box — never raw eigenvector units. • For a true animation, use Plotter.open_gif(path) + write_frame() in an off-screen plotter; the same loop that fills the filmstrip produces the GIF frames. .. rst-class:: sphx-glr-timing **Total running time of the script:** (0 minutes 0.754 seconds) .. _sphx_glr_download_gallery_post-processing_example_mode_shape_animation.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: example_mode_shape_animation.ipynb ` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: example_mode_shape_animation.py ` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: example_mode_shape_animation.zip ` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_