User-defined modifiers

The Python programming interface allows you to write your own data modifiers that participate in the data pipeline system of OVITO. Writing your own modifier functions is useful in cases where the built-in modifier types (listed in the ovito.modifiers module) are not sufficient to solve your specific problem at hand.

Custom modifier functions

You can develop a new user-defined modifier simply by writing a Python function, which will automatically be called by OVITO’s pipeline system whenever the pipeline results need to be recomputed. This function must have the following function signature:

def modify(frame, data):
    ...

When the pipeline system calls your user-defined modifier function, it will pass in two parameters: The current animation frame number (frame) at which the pipeline is being evaluated and a DataCollection (data) holding the information that is flowing down the pipeline and which the modifier function should operate on. Your modifier function should not return any value. If you want you function to modify or extend the data in some way, it should do so by editing the DataCollection in-place.

Depending on the context, you need to perform one of the following steps to insert your modifier function into the pipeline.

  • If you are working in the graphical version of OVITO, you can insert the function into the current pipeline by choosing the Python script modifier entry from the list of available modifiers. The panel of this modifier lets you open an editor window for entering the source code of the Python function.

  • If you are using the modifier function in a batch script context, your script should include a statement as part of the main program to insert the modifier function into the pipeline:

    def my_mod_function(frame, data):
        ...
        ...
    
    pipeline.modifiers.append(my_mod_function)
    

    The user-defined function, which can have an arbitrary name in this case, is inserted into a Pipeline by appending it to the modifiers list. Behind the scenes, OVITO will automatically create a PythonScriptModifier instance to wrap the Python function object.

Keep in mind that OVITO is going to invoke your Python function whenever it needs to (and as many times as it needs to). Typically this will happen when the pipeline is being evaluated. In the graphical version of OVITO a pipeline evaluation routinely occurs as part of updating the interactive viewports or when you render an image or an animation. In a batch script you typically request the pipeline evaluation explicitly by calling Pipeline.compute() or indirectly by invoking a function such as export_file().

Implementing a modifier function

Warning

The following sections on this page are out of date! They have not been updated yet to reflect the changes made in the current development version of OVITO.

The custom modifier function defined above is called by OVITO every time the modification pipeline is evaluated. The function receives the data produced by the upstream part of the pipeline (e.g. the particles loaded by a FileSource and further processed by other modifiers that precede the custom modifier in the pipeline). Our Python modifier function then has the possibility to modify or extend the data as needed. After the user-defined Python function has done its work and returns, the output flows further down the pipeline, and, eventually, the final results are stored in the output cache of the PipelineSceneNode and are rendered in the viewports.

It is important to note that the user-defined modifier function is subject to certain restrictions. Since it is repeatedly called by the pipeline system in a callback fashion, it may only manipulate the simulation data that flows through the pipeline and which it receives as an input. It should not manipulate the pipeline itself that it is part of (e.g. adding/removing modifiers) or otherwise change the global program state.

When our custom modifier function is invoked by the pipeline system, it gets passed three arguments:

  • frame (int) – The animation frame number at which the pipeline is evaluated.

  • input (DataCollection) – Contains the input data objects that the modifier receives from upstream.

  • output (DataCollection) – This is where the modifier function should put its output data objects.

The input DataCollection, and in particular the data objects stored in it, should not be modified by the modifier function. They are owned by the upstream part of the modification pipeline and must be accessed in a read-only fashion (e.g. by using the array attribute instead of marray to access per-particle values of a ParticleProperty).

On function entry, i.e. when the modifier function is invoked by the system, the output data collection already contains all data objects also found in the input collection. Thus, the default behavior is that all objects (e.g. particle properties, simulation cell, sttributes, etc.) are passed through unmodified.

Modifying existing data objects

For performance reasons no data copies are made by default, and the output collection consists of references to the original data objects from the input collection. This means, before it is safe to modify a data object in the output data collection, you have to make a copy first. Otherwise you risk permanently modifying data that is owned by the upstream part of the modification pipeline (e.g. the FileSource data cache). An in-place copy of a data object is made using the DataCollection.copy_if_needed() method. The following example demonstrates the principle:

def modify(frame, input, output):

    # Original simulation cell is passed through by default.
    # Output simulation cell is just a reference to the input cell.
    assert(output.cell is input.cell)

    # Make a copy of the simulation cell:
    cell = output.copy_if_needed(output.cell)

    # copy_if_needed() made a deep copy of the simulation cell object.
    # Now the the input and output each point to different objects.
    assert(cell is output.cell)
    assert(cell is not input.cell)

    # Now it's safe to modify the object copy:
    cell.pbc = (False, False, False)

Output of new attributes

In addition to data objects like the simulation cell or particle properties, global quantities (i.e. scalar values) flow down the data pipeline too. They are called attributes in OVITO and can be read, modified or newly added by our modifier function. For example, we can output a new attribute on the basis of an existing attribute in the input:

def modify(frame, input, output):
    output.attributes['dislocation_density'] =
        input.attributes['DislocationAnalysis.total_line_length'] / input.cell.volume

This modifier function generates a new attribute named dislocation_density, which is calculated as the ratio of the dislocation line length in a crystal (which, as we assume in this example, is computed by a DislocationAnalysisModifier preceding our custom modifier in the pipeline) and the simulation box volume.

Creating new data objects (e.g. particle properties)

The custom modifier function can inject new data objects into the modification pipeline simply by adding them to the output data collection:

def modify(frame, input, output):

    # Create a new bonds data object and a bond between atoms 0 and 1.
    bonds = ovito.data.Bonds()
    bonds.add_full(0, 1)

    # Insert into output collection:
    output.add(bonds)

For adding new particle properties (or overwriting existing properties), a special method create_particle_property() is provided by the DataCollection class:

def modify(frame, input, output):
    # Create the 'Color' particle property and set the color of all particles to green:
    color_property = output.create_particle_property(ParticleProperty.Type.Color)
    color_property.marray[:] = (1.0, 0.0, 0.0)

Note that create_particle_property() checks if the particle property already exists. If yes, it automatically copies it in place so you can overwrite its content. Otherwise a fresh ParticleProperty instance is created and added to the output data collection. That means create_particle_property() can be used in both scenarios: to modify an existing particle property or to output a new property.

Furthermore, there exists a second method, create_user_particle_property(), which is used to create custom particle properties (in contrast to standard properties like color, radius, etc.).

Initialization phase

Initialization of parameters and other inputs needed by our custom modifier function should be done outside of the function. For example, our modifier may require reference coordinates of particles, which need to be loaded from an external file. One example is the Displacement vectors modifier of OVITO, which asks the user to load a reference configuration file with the coordinates that should be subtracted from the current particle coordinates. A corresponding implementation of this modifier in Python would look as follows:

from ovito.data import ParticleProperty
from ovito.io import FileSource

reference = FileSource(adjust_animation_interval = False)
reference.load("simulation.0.dump")

def modify(frame, input, output):
    prop = output.create_particle_property(ParticleProperty.Type.Displacement)

    prop.marray[:] = (    input.particle_properties.position.array -
                      reference.particle_properties.position.array)

The script above creates a FileSource to load the reference particle positions from an external data file. Setting adjust_animation_interval to false is required to prevent OVITO from automatically changing the animation length. Within the actual modify() function we can then access the particle coordinates loaded by the FileSource object.

Asynchronous modifiers and progress reporting

Due to technical limitations the custom modifier function is always executed in the main thread of the application. This is in contrast to the built-in asynchronous modifiers of OVITO, which are implemented in C++. They are executed in a background thread to not block the graphical user interface during long-running operations.

That means, if our Python modifier function takes a long time to compute before returning control to OVITO, no input events can be processed by the application and the user interface will freeze. To avoid this, you can make your modifier function asynchronous using the yield Python statement (see the Python docs for more information). Calling yield within the modifier function temporarily yields control to the main program, giving it the chance to process waiting user input events or repaint the viewports:

def modify(frame, input, output):
    for i in range(input.number_of_particles):
        # Perform a small computation step
        ...
        # Temporarily yield control to the system
        yield

In general, yield should be called periodically and as frequently as possible, for example after processing one particle from the input as in the code above.

The yield keyword also gives the user (and the system) the possibility to cancel the execution of the custom modifier function. When the evaluation of the modification pipeline is interrupted by the system, the yield statement does not return and the Python function execution is discontinued.

Finally, the yield mechanism gives the custom modifier function the possibility to report its progress back to the system. The progress must be reported as a fraction in the range 0.0 to 1.0 using the yield statement. For example:

def modify(frame, input, output):
    total_count = input.number_of_particles
    for i in range(0, total_count):
        ...
        yield (i/total_count)

The current progress value will be displayed in the status bar by OVITO. Moreover, a string describing the current status can be yielded, which will also be displayed in the status bar:

def modify(frame, input, output):
    yield "Performing an expensive analysis..."
    ...

Setting display parameters

Many data objects such as the Bonds or SimulationCell object are associated with a corresponding Display object, which is responsible for rendering (visualizing) the data in the viewports. The necessary Display object is created automatically when the data object is created and is attached to it by OVITO. It can be accessed through the vis attribute of the DataObject base class.

If the script modifier function injects a new data objects into the pipeline, it can configure the parameters of the attached display object. In the following example, the parameters of the BondsVis are being initialized:

def modify(frame, input, output):

    # Create a new bonds data object.
    bonds = ovito.data.Bonds()
    output.add(bonds)
    ...

    # Configure visual appearance of bonds.
    bonds.vis.color = (1.0, 1.0, 1.0)
    bonds.vis.use_particle_colors = False
    bonds.vis.width = 0.4

However, every time our modifier function is executed, it will create a new Bonds object together with a new BondsVis instance. If the modifier is used in an interactive OVITO session, this will lead to unexpected behavior when the user tries to change the display settings. All parameter changes made by the user will get lost as soon as the modification pipeline is re-evaluated. To mitigate the problem, it is a good idea to create the BondsVis just once outside the modifier function and then attach it to the Bonds object created by the modifier function:

bonds_display = BondsVis(color=(1,0,0), use_particle_colors=False, width=0.4)

def modify(frame, input, output):
    bonds = ovito.data.Bonds(display = bonds_display)
    output.add(bonds)