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:
It skips through the file until it reaches the line where the requested frame starts, which was recorded by the
scanmethod.It reads the timestep number from the frame’s first line and stores it as the global attribute
Timestep.It creates a
SimulationCellwith periodic boundary conditions and fills in the three cell vectors read from the file.It reads the number of atoms and creates a
Particlescontainer with theParticle TypeandPositionproperties.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:
For long trajectories, seeking to a requested frame by reading all lines preceding that frame will become slower and slower for later frames.
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.