-
Notifications
You must be signed in to change notification settings - Fork 23
Description
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
-
Setup CONSTANTS.py with the following constants:
ANSYS_PROGRAM_PATH=...
RESOLUTION=... #(2560,1440) for example -
Run the class and give it the path to your .rst modal file
-
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