Example M7: Displacement vectors with reference configuration

This Python modifier computes each particle’s displacement vector with respect to an explicit reference configuration of the system, which is loaded by the modifier from a separate input file. Thus, it replicates some of the functionality provided by the built-in CalculateDisplacementsModifier of OVITO.

The modifier is based on the advanced programming interface, i.e., it is implemented in the form of a Python class inheriting from ModifierInterface.

from ovito.data import DataCollection
from ovito.pipeline import ModifierInterface, FileSource
from ovito.traits import OvitoObject
from ovito.vis import VectorVis
from traits.api import Int, Bool

class CalculateDisplacementsWithReference(ModifierInterface):

    # Give the modifier a second input slot for reading the reference config from a separate file:
    reference = OvitoObject(FileSource)

    # The trajectory frame from the reference file to use as (static) reference configuration (default 0).
    reference_frame = Int(default_value=0, label='Reference trajectory frame')

    # This flag controls whether the modifier tries to detect when particles have crossed a periodic boundary
    # of the simulation cell. The computed displacement vectors will be corrected accordingly.
    minimum_image_convention = Bool(default_value=True, label='Use minimum image convention')

    # A VectorVis visual element managed by this modifier, which will be assigned to the 'Displacement' output property to visualize the vectors.
    vector_vis = OvitoObject(VectorVis, alignment=VectorVis.Alignment.Head, flat_shading=False, title='Displacements')

    # Tell the pipeline system to keep two trajectory frames in memory: the current input frame and the reference configuration.
    def input_caching_hints(self, frame: int, **kwargs):
        return {
            'upstream': frame,
            'reference': self.reference_frame
        }

    # The actual function called by the pipeline system to let the modifier do its thing.
    def modify(self, data: DataCollection, *, input_slots: dict[str, ModifierInterface.InputSlot], **kwargs):

        # Request the reference configuration.
        ref_data = input_slots['reference'].compute(self.reference_frame)

        # Get current particle positions and reference positions, making sure the ordering of the two arrays
        # is the same even if the storage order of particles changes with time.
        current_positions   = data.particles.positions
        reference_positions = ref_data.particles.positions[ref_data.particles.remap_indices(data.particles)]

        # Compute particle displacement vectors. Use SimulationCell.delta_vector() method to
        # correctly handle particles that have crossed a periodic boundary.
        if self.minimum_image_convention and data.cell:
            displacements = data.cell.delta_vector(reference_positions, current_positions)
        else:
            displacements = current_positions - reference_positions

        # Output the computed displacement vectors as a new particle property.
        # Assign our visual element to the property to render the displacement vectors as arrows.
        data.particles_.create_property("Displacement", data=displacements).vis = self.vector_vis

For the sake of completeness, we also provide a version of the modifier that does not load an explicit reference configuration from a separate file. Instead, the following version of the modifier obtains the reference particle positions from the current upstream pipeline by evaluating it at a given animation time:

from ovito.data import DataCollection
from ovito.pipeline import ModifierInterface
from ovito.traits import OvitoObject
from ovito.vis import VectorVis
from traits.api import Int, Bool

class CalculateDisplacements(ModifierInterface):

    # The trajectory frame to use as reference configuration (default 0).
    reference_frame = Int(default_value=0, label='Reference trajectory frame')

    # This flag controls whether the modifier tries to detect when particles have crossed a periodic boundary
    # of the simulation cell. The computed displacement vectors will be corrected accordingly.
    minimum_image_convention = Bool(default_value=True, label='Use minimum image convention')

    # A VectorVis visual element managed by this modifier, which will be assigned to the 'Displacement' output property to visualize the vectors.
    vector_vis = OvitoObject(VectorVis, alignment=VectorVis.Alignment.Head, flat_shading=False, title='Displacements')

    # Tell the pipeline system to keep two trajectory frames in memory: the current input frame and the reference frame.
    def input_caching_hints(self, frame: int, **kwargs):
        return [frame, self.reference_frame]

    # The actual function called by the pipeline system to let the modifier do its thing.
    def modify(self, data: DataCollection, *, input_slots: dict[str, ModifierInterface.InputSlot], **kwargs):

        # Request the reference configuration from the upstream pipeline.
        ref_data = input_slots['upstream'].compute(self.reference_frame)

        # Get current particle positions and reference positions, making sure the ordering of the two arrays
        # is the same even if the storage order of particles changes with time.
        current_positions   = data.particles.positions
        reference_positions = ref_data.particles.positions[ref_data.particles.remap_indices(data.particles)]

        # Compute particle displacement vectors. Use SimulationCell.delta_vector() method to
        # correctly handle particles that have crossed a periodic boundary.
        if self.minimum_image_convention and data.cell:
            displacements = data.cell.delta_vector(reference_positions, current_positions)
        else:
            displacements = current_positions - reference_positions

        # Output the computed displacement vectors as a new particle property.
        # Assign our visual element to the property to render the displacement vectors as arrows.
        data.particles_.create_property("Displacement", data=displacements).vis = self.vector_vis