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.
See also