Animate data over time#

MAPDL LS-DYNA FLUENT CFX

This tutorial demonstrates how to create 3D animations of data in time.

Download tutorial as Python script Download tutorial as Jupyter notebook

To animate data across time, you must store the data in a FieldsContainer with a time label.

Get the result files#

First, import a results file. For this tutorial, you can use the one available in the Examples module. For more information about how to import your own result file in DPF, see the Import Data tutorial section.

# Import the ``ansys.dpf.core`` module
from ansys.dpf import core as dpf
# Import the examples module
from ansys.dpf.core import examples
# Import the operators module
from ansys.dpf.core import operators as ops

# Define the result file path
result_file_path = examples.find_msup_transient()
# Create the model
model = dpf.Model(data_sources=result_file_path)

Define a time scoping#

To animate across time, you must define the time steps you are interested in. This tutorial retrieves all the time steps available in TimeFreqSupport, but you can also filter them. For more information on how to define a scoping, see the Narrow down data tutorial in the Import Data tutorials section.

# Get a scoping of all time steps available
time_steps = model.metadata.time_freq_support.time_frequencies

Extract the results#

Extract the results to animate. In this tutorial, you extract the displacement and stress results.

Note

Only the elemental, nodal, or Faces locations are supported for animations. overall and elemental_nodal locations are not currently supported.

# Get the displacement fields (already on nodes) at all time steps
disp_fc = model.results.displacement(time_scoping=time_steps).eval()
print(disp_fc)
DPF  Fields Container
  with 20 field(s)
  defined on labels: time 

  with:
  - field 0 {time:  1} with Nodal location, 3 components and 393 entities.
  - field 1 {time:  2} with Nodal location, 3 components and 393 entities.
  - field 2 {time:  3} with Nodal location, 3 components and 393 entities.
  - field 3 {time:  4} with Nodal location, 3 components and 393 entities.
  - field 4 {time:  5} with Nodal location, 3 components and 393 entities.
  - field 5 {time:  6} with Nodal location, 3 components and 393 entities.
  - field 6 {time:  7} with Nodal location, 3 components and 393 entities.
  - field 7 {time:  8} with Nodal location, 3 components and 393 entities.
  - field 8 {time:  9} with Nodal location, 3 components and 393 entities.
  - field 9 {time:  10} with Nodal location, 3 components and 393 entities.
  - field 10 {time:  11} with Nodal location, 3 components and 393 entities.
  - field 11 {time:  12} with Nodal location, 3 components and 393 entities.
  - field 12 {time:  13} with Nodal location, 3 components and 393 entities.
  - field 13 {time:  14} with Nodal location, 3 components and 393 entities.
  - field 14 {time:  15} with Nodal location, 3 components and 393 entities.
  - field 15 {time:  16} with Nodal location, 3 components and 393 entities.
  - field 16 {time:  17} with Nodal location, 3 components and 393 entities.
  - field 17 {time:  18} with Nodal location, 3 components and 393 entities.
  - field 18 {time:  19} with Nodal location, 3 components and 393 entities.
  - field 19 {time:  20} with Nodal location, 3 components and 393 entities.

# Get the stress fields on nodes at all time steps
# Request the stress on |Nodal| location as the default |ElementalNodal| location is not supported.
stress_fc = model.results.stress.on_location(location=dpf.locations.nodal).on_time_scoping(time_scoping=time_steps).eval()
print(stress_fc)
DPF  Fields Container
  with 20 field(s)
  defined on labels: time 

  with:
  - field 0 {time:  1} with Nodal location, 6 components and 393 entities.
  - field 1 {time:  2} with Nodal location, 6 components and 393 entities.
  - field 2 {time:  3} with Nodal location, 6 components and 393 entities.
  - field 3 {time:  4} with Nodal location, 6 components and 393 entities.
  - field 4 {time:  5} with Nodal location, 6 components and 393 entities.
  - field 5 {time:  6} with Nodal location, 6 components and 393 entities.
  - field 6 {time:  7} with Nodal location, 6 components and 393 entities.
  - field 7 {time:  8} with Nodal location, 6 components and 393 entities.
  - field 8 {time:  9} with Nodal location, 6 components and 393 entities.
  - field 9 {time:  10} with Nodal location, 6 components and 393 entities.
  - field 10 {time:  11} with Nodal location, 6 components and 393 entities.
  - field 11 {time:  12} with Nodal location, 6 components and 393 entities.
  - field 12 {time:  13} with Nodal location, 6 components and 393 entities.
  - field 13 {time:  14} with Nodal location, 6 components and 393 entities.
  - field 14 {time:  15} with Nodal location, 6 components and 393 entities.
  - field 15 {time:  16} with Nodal location, 6 components and 393 entities.
  - field 16 {time:  17} with Nodal location, 6 components and 393 entities.
  - field 17 {time:  18} with Nodal location, 6 components and 393 entities.
  - field 18 {time:  19} with Nodal location, 6 components and 393 entities.
  - field 19 {time:  20} with Nodal location, 6 components and 393 entities.

Animate the results#

Animate the results with the FieldsContainer.animate() method. You can animate them on a deformed mesh (animate the color map and the mesh) or on a static mesh (animate the color map only).

The default behavior of the FieldsContainer.animate() method is to:

  • Display the norm of the data components;

  • Display data at the top layer for shells;

  • Display the deformed mesh when animating displacements;

  • Display the static mesh for other types of results;

  • Use a constant and uniform scale factor of 1.0 when deforming the mesh.

You can animate any result on a deformed geometry by providing displacement results in the deform_by parameter.

The geometry can be deformed by a Result object, an Operator (It must evaluate to a FieldsContainer of same length as the one being animated), or a FieldsContainer (also of same length as the one being animated).

Note

The behavior of the FieldsContainer.animate() method is defined by a Workflow it creates and feeds to an Animator. This Workflow loops over a Field of frame indices and for each frame generates a field of norm contours to render, as well as a displacement field to deform the mesh if deform_by is provided. For more information on plots on deformed meshes see: ref_plotting_data_on_deformed_mesh.

Animate the displacement results#

Use FieldsContainer.animate() with the displacement results.

# Animate the displacement results in a deformed geometry
disp_fc.animate()
../../../_images/animate_disp_1.gif
# Animate the displacement results on a static mesh using ``deform_by=False``
disp_fc.animate(deform_by=False)
../../../_images/animate_disp_2.gif

Animate the stress#

Use FieldsContainer.animate() with the stress results.

 # Animate the stress results on a deformed mesh
 # Use the ``deform_by`` argument and give the displacement results.
 stress_fc.animate(deform_by=disp_fc)
../../../_images/animate_stress_1.gif
 # Animate the stress results in a static geometry
 stress_fc.animate()
../../../_images/animate_stress_2.gif

Change the scale factor#

You can change the scale factor using:

  • A single number for a uniform constant scaling;

  • A list of numbers for a varying scaling (same length as the number of frames).

Uniform constant scaling#

# Define a uniform scale factor
uniform_scale_factor=10.
# Animate the displacements
disp_fc.animate(scale_factor=uniform_scale_factor)
../../../_images/animate_disp_3.gif

Varying scaling#

# Define a varying scale factor
varying_scale_factor = [float(i) for i in range(len(disp_fc))]
# Animate the displacements
disp_fc.animate(scale_factor=varying_scale_factor)
../../../_images/animate_disp_4.gif

Save the animation#

You can save the animation using the save_as argument with a target file path with the desired format as the extension key. Accepted extensions are:

  • .gif;

  • .avi;

  • .mp4

For more information see pyvista.Plotter.open_movie.

 # Animate the stress results and save it
 stress_fc.animate(deform_by=disp_fc, save_as="animate_stress.gif")

Control the camera#

Control the camera with the cpos argument.

A camera position is a combination of:

  • A position;

  • A focal point (the target);

  • A upwards vector.

It results in a list of format:

camera_position= [[pos_x, pos_y, pos_z],  # position
                  [fp_x, fp_y, fp_z],  # focal point
                  [up_x, up_y, up_z]]  # upwards vector

The FieldsContainer.animate() method accepts a single camera position or a list of camera positions for each frame.

Note

A tip for defining a camera position is to do a first interactive plot of the data with argument return_cpos=True, position the camera as desired in the view, and retrieve the output of the plotting command.

Fixed camera#

# Define the camera position
cam_pos = [[0., 2.0, 0.6], [0.05, 0.005, 0.5], [0.0, 0.0, 1.0]]
# Animate the stress with a custom fixed camera position
stress_fc.animate(cpos=cam_pos)
../../../_images/animate_disp_5.gif

Moving camera#

import copy
# Define the list of camera positions
cpos_list = [cam_pos]
# Incrementally increase the x coordinate of the camera by 0.1 for each frame
for i in range(1, len(disp_fc)):
    new_pos = copy.deepcopy(cpos_list[i-1])
    new_pos[0][0] += 0.1
    cpos_list.append(new_pos)

# Animate the stress with a moving camera
stress_fc.animate(cpos=cpos_list)
../../../_images/animate_disp_6.gif

Additional options#

You can use additional PyVista arguments of pyvista.Plotter.open_movie), such as:

  • Show or hide the coordinate system axis with show_axes=True or show_axes=False;

  • Render off-screen for batch animation creation with off_screen=True;

  • Change the frame-rate with framerate;

  • Change the image quality with quality.