Example FR1: Custom file reader loading particle properties and the simulation cell — OVITO Python Reference 3.16.0 documentation

# Example FR1: Custom file reader loading particle properties and the simulation cell

In this example we will implement a reader component for a custom file format using OVITO’s custom file reader API.

A few key assumptions and provisions before we start writing a file reader for our custom file format:

- The file format is easily recognizable from the characteristic file suffix `*.ExampleFormat`.

- We will use the Timestep tag as the delimiter separating successive trajectory frames in the file.

- The dimensions of the simulation cell may change in every frame. We assume the simulations always use 3D periodic boundary conditions.

- The total number of atoms and their types may change during a simulation. Notice how they do in the third trajectory frame below.

- Each line starts with the particle type of the atom encoded as the chemical symbol.

- Particle positions are given in reduced cell coordinates (preceded by the line Reduced coordinates).

    Timestep 0
    7.865954 0.000000 0.000000
    0.000000 7.865954 0.000000
    0.000000 0.000000 6.973035
    Atoms 18
    Reduced coordinates
    Na 0.09306390 0.30261238 0.46908935
    Na 0.90693610 0.69738762 0.46908938
    Na 0.30261235 0.90693613 0.53091062
    Na 0.69738763 0.09306392 0.53091062
    Na 0.40693612 0.80261241 0.03091062
    Na 0.59306393 0.19738766 0.03091059
    Na 0.19738763 0.40693613 0.96908938
    Na 0.80261236 0.59306396 0.96908938
    S 0.24180739 0.09397526 0.19064404
    S 0.75819276 0.90602458 0.19064394
    S 0.09397538 0.75819274 0.80935606
    S 0.90602453 0.24180732 0.80935606
    S 0.25819273 0.59397545 0.30935596
    S 0.74180726 0.40602446 0.30935606
    S 0.40602443 0.25819271 0.69064391
    S 0.59397545 0.74180729 0.69064404
    Sn 0.00000000 0.00000000 0.00000000
    Sn 0.50000000 0.50000000 0.49999997
    Timestep 100
    7.870939 0.000000 0.000000
    0.000000 7.870939 0.000000
    0.000000 0.000000 6.977331
    Atoms 18
    Reduced coordinates
    Na 0.09309428 0.30260112 0.46914038
    Na 0.90690570 0.69739888 0.46914042
    Na 0.30260109 0.90690574 0.53085958
    Na 0.69739888 0.09309430 0.53085958
    Na 0.40690573 0.80260115 0.03085957
    Na 0.59309430 0.19739893 0.03085954
    Na 0.19739889 0.40690574 0.96914042
    Na 0.80260110 0.59309434 0.96914043
    S 0.24179056 0.09398270 0.19066976
    S 0.75820959 0.90601718 0.19066973
    S 0.09398279 0.75820959 0.80933034
    S 0.90601713 0.24179048 0.80933037
    S 0.25820958 0.59398272 0.30933030
    S 0.74179038 0.40601712 0.30933031
    S 0.40601707 0.25820957 0.69066957
    S 0.59398288 0.74179044 0.69066971
    Sn 0.00000002 0.00000002 -0.00000002
    Sn 0.49999998 0.50000000 0.49999997
    Timestep 200
    7.878383 0.000000 0.000000
    0.000000 7.878383 0.000000
    0.000000 0.000000 6.982911
    Atoms 19
    Reduced coordinates
    Na 0.09309963 0.30259914 0.46914936
    Na 0.90690035 0.69740086 0.46914940
    Na 0.30259910 0.90690039 0.53085060
    Na 0.69740086 0.09309965 0.53085059
    Na 0.40690038 0.80259917 0.03085059
    Na 0.59309965 0.19740091 0.03085055
    Na 0.19740087 0.40690039 0.96914940
    Na 0.80259911 0.59309969 0.96914941
    S 0.24178760 0.09398401 0.19067428
    S 0.75821255 0.90601588 0.19067427
    S 0.09398409 0.75821256 0.80932582
    S 0.90601583 0.24178752 0.80932585
    S 0.25821254 0.59398400 0.30932578
    S 0.74178741 0.40601583 0.30932578
    S 0.40601578 0.25821253 0.69067409
    S 0.59398418 0.74178748 0.69067423
    Sn 0.00000002 0.00000002 -0.00000002
    Sn 0.49999997 0.50000000 0.49999997
    Fe 0.32000002 0.41690039 0.42785059

## The `detect` method

For simplicity we will identify this file type by its name. That means, if the file extension is .ExampleFormat, the file reader will attempt to parse it. Extracting and validating the file extension is done in line 10:

     1from ovito.data import DataCollection
     2from ovito.io import FileReaderInterface
     3from typing import Callable, Any
     4import os
     5
     6class ExampleFileFormatReader(FileReaderInterface):
     7
     8    @staticmethod
     9    def detect(filename: str):
    10        return os.path.splitext(filename)[1] == ".ExampleFormat"
    11
    12    def scan(self, filename: str, register_frame: Callable[..., None]):
    13        ...
    14
    15    def parse(self, data: DataCollection, filename: str, frame_info: Any, **kwargs: Any):
    16        ...

## The `scan` method

Frames are delimited by the phrase Timestep N, where N is the simulation timestep at that frame. We will step through the file and track the line numbers where this tag appears in the file.

Within the scan method, the whole file is read in a `for` loop (lines 13-14). The following lines 15-16 process each frame, here, each time the Timestep tag is encountered, the current line number and frame label are registered with OVITO using the `register_frame` callback. The line number will be used to quickly find a specific frame in the file again, while the timestep value is used as a human-readable timeline label of the frame in the GUI.

     1from ovito.data import DataCollection
     2from ovito.io import FileReaderInterface
     3from typing import Callable, Any
     4import os
     5
     6class ExampleFileFormatReader(FileReaderInterface):
     7
     8    @staticmethod
     9    def detect(filename: str):
    10        return os.path.splitext(filename)[1] == ".ExampleFormat"
    11
    12    def scan(self, filename: str, register_frame: Callable[..., None]):
    13        with open(filename, "r") as f:
    14            for line_number, line in enumerate(f):
    15                if line.startswith("Timestep "):
    16                    register_frame(frame_info=line_number, label=int(line[9:]))
    17
    18    def parse(self, data: DataCollection, filename: str, frame_info: Any, **kwargs: Any):
    19        ...

## The `parse` method

The `parse` method has multiple tasks. The goal is to set the `SimulationCell`, `particle positions`, and `ParticleType`. The current timestep will also be stored as `attributes` for easier access inside the OVITO application window.

First we skip through the file until we reach the requested frame (lines 21-23). The starting line of the frame can be obtained from the `frame_info` object set in the `scan` method.

The next line in the file now contains the Timestep information which we would like to save as an OVITO attribute. The information is read in line 26 and stored in the attributes dictionary in line 27. This dictionary can be filled with custom fields which will all be accessible to the user from within OVITO.

In line 30 an “empty” simulation `SimulationCell` with all cell vectors set to 0 is created and its periodic boundary conditions are set. The cell vectors are subsequently read from the input file and placed in the simulation cell matrix (code lines 32-33).

The next line after the simulation cell block contains the number of atoms stored in the file for the current frame. It is read in line 36. This information is use to create the `Particles` `PropertyContainer` (line 38). Once this container is constructed it can be filled with particle properties. In this case we want to read the particle type and the position from the file. These properties are defined in lines 40 and 42, respectively.

After skipping the reduced coordinates line we read the next N lines from the file, where N is the number of particles (lines 48-53). Each line contains the data for one particle. The first column contains the chemical symbol which can be automatically converted into OVITO particle types using the `type_property.add_type_name(tokens[0], particles).id` method, where `tokens[0]` is the chemical symbol. After parsing, all Na atoms will have the numeric type 1, all S atoms will have type 2, and so on. The result is stored in the `type_property` property array at the correct index. Lastly, positions for each particle are read from the file and converted into cartesian coordinates by multiplication with the simulation cell matrix. The resulting position vector is stored in the `position_property` array.

     1from ovito.data import DataCollection
     2from ovito.io import FileReaderInterface
     3from typing import Callable, Any
     4import os
     5
     6class ExampleFileFormatReader(FileReaderInterface):
     7
     8    @staticmethod
     9    def detect(filename: str):
    10        return os.path.splitext(filename)[1] == ".ExampleFormat"
    11
    12    def scan(self, filename: str, register_frame: Callable[..., None]):
    13        with open(filename, "r") as f:
    14            for line_number, line in enumerate(f):
    15                if line.startswith("Timestep"):
    16                    register_frame(frame_info=line_number, label=line.strip())
    17
    18    def parse(self, data: DataCollection, filename: str, frame_info: int, **kwargs: Any):
    19        with open(filename, "r") as f:
    20            # Seek to the beginning of the requested trajectory frame
    21            starting_line_number = frame_info
    22            for _ in range(starting_line_number):
    23                f.readline()
    24
    25            # Parse the timestep number as a global attribute
    26            timestep = int(f.readline().split()[1])
    27            data.attributes["Timestep"] = timestep
    28
    29            # Create 3D periodic cell
    30            cell = data.create_cell([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], pbc=[True, True, True])
    31            # Parse cell vectors
    32            for i in range(3):
    33                cell[:, i] = [float(c) for c in f.readline().split()]
    34
    35            # Parse particle count
    36            particle_count = int(f.readline().split()[1])
    37            # Create new particles container object
    38            particles = data.create_particles(count=particle_count)
    39            # Create particles type property
    40            type_property = particles.create_property("Particle Type")
    41            # Create position property
    42            position_property = particles.create_property("Position")
    43
    44            # Skip "Reduced coordinates" line
    45            f.readline()
    46
    47            # Parse particle types and coordinates
    48            for i in range(particle_count):
    49                tokens = f.readline().split()
    50                # Register named particle type and get its numeric ID assigned by OVITO
    51                type_property[i] = type_property.add_type_name(tokens[0], particles).id
    52                # Convert reduced to cartesian particle coordinates
    53                position_property[i] = cell @ ([float(c) for c in tokens[1:]] + [1.0])

## A note on performance

This custom file reader exhibits two potential performance issues:

1. For long trajectories, seeking to a requested frame by reading all lines preceding that frame will become slower and slower for later frames.

2. For simulations containing many atoms, performing many property assignments such as `type_property[i] = ...` can become a bottleneck.

These issues will be addressed in the next Example FR2: Optimizing the performance of Example FR1.