User-defined pipeline sources

OVITO Pro Python scripts

Normally, a pipeline gets its input data from a FileSource, which loads a simulation file. A user-defined pipeline source instead generates the input data with Python code. Use it, for example, to build a synthetic model, such as a crystal lattice, or to feed data computed by another Python package into a pipeline, where it can be processed by modifiers and visualized. If the data is stored in files of a format OVITO doesn’t support, write a custom file reader instead.

Like user-defined modifiers, pipeline sources come in a function-based and a class-based variant.

Function-based interface

A pipeline source function receives an empty DataCollection and fills it with data for the requested trajectory frame. In a Python script, you wrap the function in a PythonSource and create a pipeline with it:

import numpy
from ovito.data import DataCollection
from ovito.pipeline import Pipeline, PythonSource

def create(frame: int, data: DataCollection):
    # Create a simple cubic lattice of particles:
    n = 5
    data.create_cell(numpy.diag([n, n, n]), pbc=(True, True, True))
    grid = numpy.mgrid[0:n, 0:n, 0:n].reshape(3, -1).T
    particles = data.create_particles(count=len(grid))
    particles.create_property("Position", data=grid + 0.5)

pipeline = Pipeline(source=PythonSource(function=create))

The Creating data from scratch section explains how to populate a DataCollection. In OVITO Pro, choose Python script as the data source of a new pipeline and enter the function, which must be named create, in the code editor.

Class-based interface

The class-based interface lets you define parameters and control the number of frames the source produces. The class derives from PipelineSourceInterface and implements the create() method:

from ovito.pipeline import PipelineSourceInterface
from traits.api import Int, Range, observe

class LatticeSource(PipelineSourceInterface):

    size = Int(5, label="Lattice size")
    num_frames = Range(low=1, value=20, label="Number of frames")

    def compute_trajectory_length(self, **kwargs):
        return self.num_frames

    @observe("num_frames")
    def _num_frames_changed(self, event):
        self.notify_trajectory_length_changed()

    def create(self, data: DataCollection, *, frame: int, **kwargs):
        n = self.size
        data.create_cell(numpy.diag([n, n, n]), pbc=(True, True, True))
        grid = numpy.mgrid[0:n, 0:n, 0:n].reshape(3, -1).T + 0.5
        # Let the particles oscillate over time:
        grid[:,2] += 0.2 * numpy.sin(2 * numpy.pi * frame / self.num_frames)
        particles = data.create_particles(count=len(grid))
        particles.create_property("Position", data=grid)

By default, a pipeline source produces a single frame. The optional compute_trajectory_length() method specifies the number of frames it can generate, and the pipeline system then calls create() with frame numbers in that range. Since the number of frames depends on a parameter in this example, the class calls notify_trajectory_length_changed() whenever the parameter changes, using the @observe decorator of the Traits framework.

In a Python script, you use the class like this:

pipeline = Pipeline(source=PythonSource(delegate=LatticeSource(size=8)))
print("Number of frames:", pipeline.num_frames)

Important

The pipeline system may call create() repeatedly for the same frame, e.g. when the user changes a parameter. Each time, it passes in the data collection from the previous call. Methods such as create_particles() and create_cell() reuse existing data objects in that case, which preserves settings the user has made in the meantime, e.g. the colors of particle types or the settings of visual elements.