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 identify the file format by the file name suffix .ExampleFormat:

@staticmethod
def detect(filename: str):
    return os.path.splitext(filename)[1] == ".ExampleFormat"

The scan method

Frames are delimited by the phrase Timestep N, where N is the simulation timestep of the frame. The scan method reads the whole file line by line. Each time it encounters the Timestep tag, it registers a new frame with OVITO using the register_frame callback. The line number serves as frame_info, which lets the parse method quickly find the frame in the file later, and the line text is used as a human-readable label of the frame in the timeline of OVITO Pro:

def scan(self, filename: str, register_frame: Callable[..., None]):
    with open(filename, "r") as f:
        for line_number, line in enumerate(f):
            if line.startswith("Timestep "):
                register_frame(frame_info=line_number, label=line.strip())

The parse method

The parse method loads the simulation cell, the particle types, and the particle positions of the requested frame. It also stores the timestep number as a global attribute in the attributes dictionary, which makes it accessible to the user in OVITO:

def parse(self, data: DataCollection, filename: str, frame_info: int, **kwargs: Any):
    with open(filename, "r") as f:
        # Seek to the beginning of the requested trajectory frame
        for _ in range(frame_info):
            f.readline()

        # Parse the timestep number and store it as a global attribute
        data.attributes["Timestep"] = int(f.readline().split()[1])

        # Create a 3D periodic simulation cell and parse the cell vectors
        cell = data.create_cell([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], pbc=(True, True, True))
        for i in range(3):
            cell[:, i] = [float(c) for c in f.readline().split()]

        # Parse the particle count and create the particles container
        particle_count = int(f.readline().split()[1])
        particles = data.create_particles(count=particle_count)
        type_property = particles.create_property("Particle Type")
        position_property = particles.create_property("Position")

        # Skip the "Reduced coordinates" line
        f.readline()

        # Parse particle types and coordinates
        for i in range(particle_count):
            tokens = f.readline().split()
            # Register the named particle type and store its numeric ID assigned by OVITO
            type_property[i] = type_property.add_type_name(tokens[0], particles).id
            # Convert reduced to Cartesian coordinates
            position_property[i] = cell @ ([float(c) for c in tokens[1:]] + [1.0])

The method performs the following steps:

  1. It skips through the file until it reaches the line where the requested frame starts, which was recorded by the scan method.

  2. It reads the timestep number from the frame’s first line and stores it as the global attribute Timestep.

  3. It creates a SimulationCell with periodic boundary conditions and fills in the three cell vectors read from the file.

  4. It reads the number of atoms and creates a Particles container with the Particle Type and Position properties.

  5. For each atom, it converts the chemical symbol into a numeric type ID using add_type_name(), which defines a new named particle type when a symbol occurs for the first time, and converts the reduced coordinates to Cartesian coordinates by multiplying them with the cell matrix. After parsing, all Na atoms have the numeric type ID 1, all S atoms type ID 2, and so on.

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.