Skip to content

animate_mode does not correctly animate a complex mode #2730

@Timoxzy

Description

@Timoxzy

Before submitting the issue

  • I have checked for Compatibility issues
  • I have searched among the existing issues
  • I am using a Python virtual environment

Description of the bug

The animation module only animates the real part of the solution for the eigenmodes.

Apart from that it is also not possible to set the text in the animate_mode function, which is annoying when the time step does not match the mode number.

Here is the class initiator and the function for mode animation I used.
animate_complex uses the native animate_mode function whereas mode_animation is my code for animating the mode that incoporates the imaginary parts correctly.

Please let me know if I just used the animate_mode function wrong or if this is not correctly implemented.

from ansys.dpf import core as dpf
from ansys.dpf.core import animation
import os
import CONSTANTS as const
from ansys.dpf.core import start_local_server
from ansys.dpf.core.plotter import DpfPlotter

import numpy as np
import imageio

server = start_local_server(ansys_path=const.ANSYS_PROGRAM_PATH)

class ModalAnimation:
def init(self, result_file_path):
self.resolution = const.RESOLUTION
self.model = dpf.Model(result_file_path)
self.disp = self.model.results.displacement.on_all_time_freqs.eval()

    self.mesh = self.model.metadata.meshed_region
    
    self.cpos = None

    for field in self.disp:
        field.meshed_region = self.mesh
    self.freq_scoping = self.disp.get_time_scoping()
    self.freq_support = self.disp.time_freq_support
    self.unit = self.freq_support.time_frequencies.unit

    
    if round(self.freq_support.get_frequency(cumulative_index=0)) == round(self.freq_support.get_frequency(cumulative_index=1)):
        self.double_freq = True
    else:
        self.double_freq = False
def animate_complex(
    self, mode_number=1, deform_scale_factor=5, off_screen=True,
):
    """
    Animate an eigenmode and save it as .gif

    :param mode_number: Define which mode number you want to animate
    :param deform_scale_factor: Define the desired Scale Factor of the deformation
    :type deform_scale_factor: Int
    :param phi: Define the phase angle
    :type phi: float
    """
    if self.double_freq:
        time_index = mode_number * 2 - 1
    else:
        time_index = mode_number
    disp_complex = dpf.operators.result.displacement(
        time_scoping=[time_index],
        streams_container=self.model.metadata.streams_provider,
        sectors_to_expand=[0],
        read_cyclic=2,
        mesh=self.mesh,
    )
    disp_container = disp_complex.outputs.fields_container()
    mode = disp_container.deep_copy()
    os.makedirs("./mode_animation_complex", exist_ok=True)
    save_as = f"./mode_animation_complex/mode_{mode_number}.gif"
    mode_frequency = self.freq_support.get_frequency(
        cumulative_index=time_index-1
    )
    mode.time_freq_support.time_frequencies.data[0] = (
        mode_frequency  # sets the frequency of the first substep to the frequency of the desired mode. Needed for correct text in animation
    )
    
    animation.animate_mode(
        mode,
        mode_number=time_index,
        deform_scale_factor=deform_scale_factor,
        off_screen=off_screen,
        save_as=save_as,
        meshed_provider=self.mesh,
        show_edges=True,
        cpos=self.cpos,
        full_screen=True,
        lighting='none',
        window_size=self.resolution,
        edge_opacity = 0.25,
    )  # Mode_number in .gif displays the substep number and not the mode_number, solution t.b.d.
def mode_animation(
    self,
    mode_number=1,
    scale_factor=1.0,
    off_screen=True,
    frames=36,
    fps=10,
    dyn_cbar=True,
    animation=True,
    target_max_deformation=50.0,
):
    """
    Animate an eigenmode and save it as .gif and/or plot equally distributed phase angles according to set frame number

    :param mode_number: Define which mode number you want to animate
    :type mode_number: int
    :param frames: The number of plots generated. These will be distributed evenly over the 360° of phaseangle. Default frame number is 36
    :type frames: int
    :param fps: The frames per second used for the .gif Animation. Default is 10.
    :type fps: int
    :param scale_factor: Define the desired scale factor of the deformation
    :type scale_factor: float
    :param phi: Define the phase angle
    :type phi: float
    :param dyn_cbar: Enable/Disable a distinct colorbar for each frame/plot
    :type dyn_cbar: bool
    :param animation: Disable the automatic creation of a .gif animating the mode
    :type animation: bool
    :param target_max_deformation: Set the limit of the maximum allowed deformation to avoid bad visualisations
    :type target_max_deformation: float
    """

    print("Animation process START")

    # check if the frequencies are listed double
    if self.double_freq:
        time_index = mode_number * 2 - 1
    else:
        time_index = mode_number
    os.makedirs(f"./phase_plots_mode_{mode_number}", exist_ok=True)
    os.makedirs(f"./animation_mode_{mode_number}", exist_ok=True)

    freq_val = self.freq_support.get_frequency(cumulative_index=time_index - 1)
    text = f"Mode {mode_number}: {freq_val:.3f}{self.unit}"

    disp_re = self.disp.get_field({"time": time_index, "base_sector": 1})
    disp_im = self.disp.get_field({"time": time_index, "base_sector": 0})

    u_re = disp_re.data
    u_im = disp_im.data

    u_complex = u_re + 1j * u_im

    # save node ids to order the displacement results accordingly
    all_node_ids = self.mesh.nodes.scoping.ids
    node_id_to_index = {nid: idx for idx, nid in enumerate(all_node_ids)}
    render_frames = []
    # empty array for displacements
    full_u_t_template = np.zeros((len(all_node_ids), 3))

    # original coordinate field
    coords_field_orig = self.mesh.nodes.coordinates_field

    # new displacement coordinate field
    disp_mode_field = dpf.Field(
        nentities=self.mesh.nodes.scoping.size, location=dpf.locations.nodal
    )

    disp_mode_field.scoping = coords_field_orig.scoping

    u_max = 0
    for step in range(frames + 1):
        alpha = 2 * np.pi * step / frames  # 0..2π
        u_t_partial = np.real(u_complex * np.exp(1j * alpha))
        u_step_max = u_t_partial.max()
        if u_step_max > u_max:
            u_max = u_step_max

    norm_factor = target_max_deformation / u_max

    if dyn_cbar:
        clim = None
    else:
        clim = [0, target_max_deformation]

    print(f"Global Max Displacement: {u_max:.3f} -> Norm factor: {norm_factor:.5f}")

    for step in range(frames + 1):
        alpha = 2 * np.pi * step / frames  # 0..2π
        u_t_partial = np.real(u_complex * np.exp(1j * alpha))
        u_t_partial *= norm_factor

        full_u_t = full_u_t_template.copy()

        # map the results correctly to align with the mesh
        for i, nid in enumerate(disp_re.scoping.ids):
            full_u_t[node_id_to_index[nid], :] = u_t_partial[i, :]

        disp_mode_field.data = full_u_t

        pl = DpfPlotter(off_screen=off_screen)

        pl.add_field(
            disp_mode_field,
            meshed_region=self.mesh,
            deform_by=disp_mode_field,
            scale_factor=scale_factor,
            lighting=False,
            show_edges=True,
            edge_opacity=0.25,
            clim=clim,
        )
        img = pl.show_figure(
            return_img=True,
            window_size=self.resolution,
            text=text,
            cpos=self.cpos,
            screenshot=f"./phase_plots_mode_{mode_number}/mode_{mode_number}_phase_{round(alpha * 180 / (np.pi))}.png",
        )
        render_frames.append(img[0])

    if animation == True:
        imageio.mimwrite(
            f"./animation_mode_{mode_number}/mode_{mode_number}.gif",
            render_frames,
            fps=fps,
            loop=0,
        )
        print(f"Mode {mode_number} was saved as .gif animation")

Steps To Reproduce

  1. Setup CONSTANTS.py with the following constants:
    ANSYS_PROGRAM_PATH=...
    RESOLUTION=... #(2560,1440) for example

  2. Run the class and give it the path to your .rst modal file

  3. run mode_animation and animate_complex for a mode that has complex results.

Which Operating System causes the issue?

Linux

Which DPF/Ansys version are you using?

Ansys 2025 R2

Which Python version causes the issue?

3.11

Installed packages

ansys-api-mapdl==0.5.2
ansys-api-platform-instancemanagement==1.1.3
ansys-dpf-core==0.14.1
ansys-mapdl-core==0.71.0
ansys-mapdl-reader==0.55.1
ansys-math-core==0.2.4
ansys-platform-instancemanagement==1.1.2
ansys-tools-path==0.7.3
appdirs==1.4.4
certifi==2025.10.5
charset-normalizer==3.4.4
click==8.3.0
contourpy==1.3.3
cycler==0.12.1
fonttools==4.60.1
geomdl==5.4.0
grpcio==1.75.1
idna==3.11
imageio==2.37.0
imageio-ffmpeg==0.6.0
importlib_metadata==8.7.0
kiwisolver==1.4.9
matplotlib==3.10.7
numpy==2.3.4
packaging==25.0
pexpect==4.9.0
pillow==12.0.0
platformdirs==4.5.0
pooch==1.8.2
protobuf==4.25.8
psutil==7.1.0
ptyprocess==0.7.0
pyansys-tools-versioning==0.6.0
pyiges==0.3.2
pyparsing==3.2.5
python-dateutil==2.9.0.post0
pyvista==0.45.3
requests==2.32.5
ruff==0.14.2
scipy==1.16.2
scooby==0.10.2
six==1.17.0
tabulate==0.9.0
tqdm==4.67.1
typing_extensions==4.15.0
urllib3==2.5.0
vtk==9.4.2
zipp==3.23.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions