User-defined file readers

OVITO Pro Python scripts

OVITO supports many file formats out of the box. To import a format it doesn’t support, you can write a file reader in Python. Once installed, the reader takes part in OVITO’s automatic format detection, both in the Load File function of OVITO Pro and in the import_file() function. It can also be used to load trajectories.

A file reader is a Python class derived from FileReaderInterface, which implements up to three methods:

from typing import Any, Callable
from ovito.data import DataCollection
from ovito.io import FileReaderInterface

class MyFileReader(FileReaderInterface):

    @staticmethod
    def detect(filename: str):
        ...

    def scan(self, filename: str, register_frame: Callable[..., None]):
        ...

    def parse(self, data: DataCollection, filename: str, frame_info: Any, **kwargs: Any):
        ...

detect()

Checks whether a file has the format the reader can handle.

scan()

Discovers the trajectory frames stored in a file. Only needed for formats that can store several frames.

parse()

Loads the data of one frame from the file. This is the only mandatory method.

The following sections develop a reader for this simple text-based trajectory format:

Header of MyFileFormat
Timestep 0: 6 particles
0.0 0.0 0.0
1.0 0.0 0.0
0.0 1.0 0.0
0.0 0.0 1.0
1.0 1.0 0.0
1.0 0.0 1.0
Timestep 10: 3 particles
0.1 0.0 0.0
1.1 0.0 0.0
0.1 1.0 0.0
Timestep 20: 4 particles

The file starts with a header line, followed by any number of frames. Each frame begins with a line specifying the timestep and the number of particles, followed by the xyz coordinates of the particles, one per line.

Tip

Although this example reads particle data, a file reader can load all kinds of data supported by OVITO, e.g. SurfaceMesh, VoxelGrid, or DataTable objects.

The detect method

When the user loads a file, OVITO calls the detect() method of all installed file readers. The first reader returning True gets used to import the file. The method is a static method, which receives the path of the file to inspect:

@staticmethod
def detect(filename: str):
    try:
        with open(filename, "r") as f:
            return f.readline().strip() == "Header of MyFileFormat"
    except (OSError, UnicodeDecodeError):
        return False

The method should decide as quickly as possible, e.g. by reading just the first line, to not slow down the import of files that are meant for other readers. It should also handle files it cannot read, e.g. binary files, gracefully by returning False.

The scan method

The optional scan() method builds an index of the trajectory frames stored in the file, which enables fast random access to individual frames later. It typically reads the entire file and calls the register_frame callback function provided by OVITO for each frame found:

def scan(self, filename: str, register_frame: Callable[..., None]):
    with open(filename, "r") as f:
        for line_number, line in enumerate(f):
            match = re.match(r"(Timestep \d+): (\d+) particles", line)
            if match:
                num_particles = int(match.group(2))
                register_frame(frame_info=(line_number, num_particles), label=match.group(1))

The frame_info argument of register_frame can be any Python value that helps the reader locate the frame’s data later, e.g. the line number or byte offset where the frame begins in the file. Here, we store the line number of the frame header and the number of particles. The optional label argument specifies a human-readable description of the frame, which OVITO Pro displays in its timeline.

If a reader doesn’t implement scan(), OVITO assumes that each file contains a single frame.

The parse method

The parse() method loads the data of one frame. It receives the frame_info value recorded by scan() for the requested frame, and a DataCollection to fill with data:

def parse(self, data: DataCollection, filename: str, frame_info: tuple[int, int], **kwargs: Any):
    start_line, num_particles = frame_info
    with open(filename, "r") as f:
        # Skip lines up to and including the frame header line:
        for _ in range(start_line + 1):
            f.readline()
        # Read the particle coordinates:
        coordinates = [[float(value) for value in f.readline().split()] for _ in range(num_particles)]

    particles = data.create_particles(count=num_particles)
    particles.create_property("Position", data=coordinates)

The first time parse() gets called, the DataCollection is empty. In subsequent calls, e.g. when the user jumps to a different frame, it contains the data from the previous frame. The reader should then update only what has changed, to preserve changes the user has made in the user interface in the meantime, e.g. to the colors of particle types or the settings of visual elements. The following methods take care of this by creating new data objects only if needed and otherwise reusing the existing ones:

The method can accept further keyword arguments passed by the system as additional parameters, e.g. frame_index (the zero-based number of the frame being loaded), url (the original, possibly remote location of the file), and is_new_file (indicates that the user has just opened a new file and any existing data in the collection should be discarded). See parse() for details.

Important

The parameter list of parse() must end with **kwargs to stay compatible with future versions of OVITO, which may pass additional arguments.

Tip

A file reader can store the particle coordinates and other real-valued standard properties in single precision, e.g. using particles.create_property("Position", dtype=numpy.float32). See Floating-point precision of standard properties. Note that the float_precision option of import_file() has no effect for custom file readers.

Here is the complete file reader:

import re
from typing import Any, Callable
from ovito.data import DataCollection
from ovito.io import FileReaderInterface

class MyFileReader(FileReaderInterface):

    @staticmethod
    def detect(filename: str):
        try:
            with open(filename, "r") as f:
                return f.readline().strip() == "Header of MyFileFormat"
        except (OSError, UnicodeDecodeError):
            return False

    def scan(self, filename: str, register_frame: Callable[..., None]):
        with open(filename, "r") as f:
            for line_number, line in enumerate(f):
                match = re.match(r"(Timestep \d+): (\d+) particles", line)
                if match:
                    num_particles = int(match.group(2))
                    register_frame(frame_info=(line_number, num_particles), label=match.group(1))

    def parse(self, data: DataCollection, filename: str, frame_info: tuple[int, int], **kwargs: Any):
        start_line, num_particles = frame_info
        with open(filename, "r") as f:
            # Skip lines up to and including the frame header line:
            for _ in range(start_line + 1):
                f.readline()
            # Read the particle coordinates:
            coordinates = [[float(value) for value in f.readline().split()] for _ in range(num_particles)]

        particles = data.create_particles(count=num_particles)
        particles.create_property("Position", data=coordinates)

The code examples FR1 and FR2 show a more complete reader, which also loads particle types and the simulation cell, and discuss how to make a reader fast for very large files.

Testing your file reader

To test your file reader, pass the class to the import_file() function in a Python script:

from ovito.io import import_file

if __name__ == "__main__":
    pipeline = import_file("input/example.myformat", input_format=MyFileReader)
    for data in pipeline.frames:
        print(data.particles.count, "particles")

The input_format argument bypasses the automatic format detection, i.e. OVITO calls scan() and parse() of your class directly, without calling detect() first. This is also how you use a custom file reader in a Python script without installing it.

Installing the reader in OVITO Pro

To make your file reader part of OVITO Pro’s automatic format detection, it must be installed as a Python package and registered as an extension, as described in Packaging and installing extensions.

User parameters

A file reader can expose parameters, which OVITO Pro displays in the panel of the file source. In a Python script, import_file() forwards additional keyword arguments to the file reader to initialize parameters with matching names.