Assigning unique polyhedral shapes to multiple particles

I need to assign a polyhedral shape to approximately 5000 particles, where each particle has a different shape. The only way I found to do this in Ovito is by using the “Particle types” option, changing the particle shape to “Mesh/User-defined,” and selecting the corresponding .obj file, which specifies the vertex coordinates and triangular-face connections.

This approach is manageable for a single particle but becomes impractical for all 5000 particles, as each particle has a unique shape associated with a specific .obj file.

I assume the only solution is to create a Python script to automate this process. However, I’m not sure which modifiers or API commands would allow me to achieve this. I couldn’t find relevant examples or documentation on the Ovito website.

Could someone help me with guidance or an example script for this task?

This is an interesting use case. In fact, this can only be achieved with the help of a Python modifier function, which you have to insert into the pipeline in the OVITO Pro GUI. Or you have to implement all work as a standalone program using the OVITO Python module.

It must be said, however, that OVITO is not designed for such a scenario. Both the particle type system and the rendering system are designed for only a handful of different particle shapes to exist in the system. 5000 different particle shapes is at the limit of what OVITO is capable of. You will experience significant performance bottlenecks.

Here is a Python modifier script that shows how to load the particle shapes from the OBJ files and assign them to the particles.

from ovito.data import DataCollection
from ovito.vis import ParticlesVis
from ovito.io import import_file

# First, preload the 5000 .obj files to produce a list of TriangleMesh objects
meshes = []
for i in range(5000):
    mesh = import_file(f"meshes/shape{i}.obj").compute().triangle_meshes['mesh']
    meshes.append(mesh)

# Custom modifier function, which creates the ParticleTypes for the triangle meshes and
# assigns them to the existing particles in the simulation.
def modify(frame: int, data: DataCollection):
   # Create a modifiable copy of the "Particle Type" property.
   particle_types = data.particles_.particle_types_
   for i, mesh in enumerate(meshes):
       # Create the i-th particle type
       t = particle_types.add_type_id(i, data.particles)
       t.mesh = mesh
       t.shape = ParticlesVis.Shape.Mesh
       # Assign the i-th particle type to the i-th particle
       particle_types[i] = i

I would like to invite you to provide more detailed background information on your use case. What kind of simulation model are you dealing with? Perhaps we can make improvements to OVITO in the future to better support such applications.

1 Like

Thank you for the reply! I will try using this Python script to evaluate Ovito’s performance. I typically rely on Ovito to visualize particle trajectories, including their positions and orientations, in my Monte Carlo simulations. Since I work with hard-core particles, Ovito is a great tool for verifying the accuracy of my overlap algorithm. Currently, I am working with particles with polyhedral shapes and analyzing their packing behavior.

@stukowski Sorry for the late reply. I’ve just purchased the Ovito PRO version and tested your Python script.

The script that you’ve provided is compiling and running but apparently it’s not selecting the particle types accordingly, which is probably due to the formatting of my XYZ file.

My XYZ file look like this (sorry but I can’t upload the file because I’m a new user):

5002
Lattice="2065.2926613755631 0.0000000000000000 0.0000000000000000 0.0000000000000000 2065.2926613755631 0.0000000000000000 0.0000000000000000 0.0000000000000000 7228.5243148144709" Origin="-1032.6463306877815 -1032.6463306877815 -3614.2621574072355" Properties=species:S:1:pos:R:3:orientation:R:4:aspherical_shape:R:3
                  745  -791.42972070406393       -857.38362340600213       -2448.6421545759163      -0.32503951466243725      -0.44545301693885936      -0.83143564847902929       -6.8085872589964652E-002   0.0000000000000000        0.0000000000000000        0.0000000000000000     
                  112  -479.78039338739427        911.33459893192753       -2803.0434487446478       0.21857325575177441       0.11447159576216352       0.91929148929866622       0.30663519585457172        0.0000000000000000        0.0000000000000000        0.0000000000000000     
                  166  -937.68743574384655       -907.84395322391981       -3074.5251917272381       0.46014544682664471      -0.28433375124069904      -0.46144334366403972       0.70320020353968227        0.0000000000000000        0.0000000000000000        0.0000000000000000     
                  939   951.60927728485308       -941.97298046256765       -2856.0910508970687       0.75450447441450319      -0.53234989693532531       -4.8203341118543200E-002 -0.38079262496331523        0.0000000000000000        0.0000000000000000        0.0000000000000000     
                  796  -706.63727794048145       -194.69979530234778       -2257.4714064738237      -0.57864230119196225      -0.16898924066850529       0.54901079302779765      -0.57896707414890158        0.0000000000000000        0.0000000000000000        0.0000000000000000     
                  126   238.35965222931785       -870.07266367397483       -2482.8294115594799      -0.67792793796024131       0.37863058089032942       0.34682419138766485       0.52608513989269756        0.0000000000000000        0.0000000000000000        0.0000000000000000     
                  207   309.90721233715419        507.46687237162365       -3537.5818810426426      -0.73944754134853885       -1.1928088984263476E-002  0.64453741128638775       0.19402726545262786        0.0000000000000000        0.0000000000000000        0.0000000000000000     
                  441  -491.88726644106879       -140.04531138723564       -2421.8259775950178      -0.66879417599499258       0.49568137064037227       -9.2536211221677499E-003 -0.55401146148016489        0.0000000000000000        0.0000000000000000        0.0000000000000000     
                  277  -597.99554772444674        398.45938973176874       -2030.8955354011177       0.60883420007563049       0.34519312147521974       0.58766678021041063       0.40596844845578006        0.0000000000000000        0.0000000000000000        0.0000000000000000     
                  641   276.60410744089313       -609.34162569472767       -2213.0912937125404      -0.36353194106512005       0.51324637218678593       0.39531799005749740       0.66943735778594871        0.0000000000000000        0.0000000000000000        0.0000000000000000
                    .
                    .
                    .
                    0   0.0000000000000000        0.0000000000000000        3614.2621574072355        0.0000000000000000        0.0000000000000000        0.0000000000000000        1.0000000000000000        1032.6463306877815        1032.6463306877815        0.0000000000000000     
                    0   0.0000000000000000        0.0000000000000000       -3614.2621574072355        0.0000000000000000        0.0000000000000000        0.0000000000000000        1.0000000000000000        1032.6463306877815        1032.6463306877815        0.0000000000000000

I have 5000 particles + 2 hard walls at the bottom and top of my simulation box. I reserved the index 0 to these walls. The first column of my XYZ file is the particle type, followed by XYZ positions, XYZW orientation quaternions, and XYZ aspherical shape, which is not really necessary for describing the meshes, but it is for the hard walls.

I also don’t have in this example 5000 particle types. I have only 1000 types that are distributed randomly to the particles, so you can have more than one type selected for different particles.

I couldn’t figure out what’s happening in the script and why it is not selecting the particle types accordingly. Could you help me with that?

It’s difficult for me to say from here what’s happening in the script and why this fails. I would like to have a direct look at it. Could you please send the XYZ file and your current visualisation setup to [email protected]? If you save the current program session state as an .ovito file, it should also contain the script. As for the 1000 .obj files, it may not be necessary to send them (if the data is too large). If we’re lucky, I can already identify the problem without them. Thanks.

1 Like

Hello, @stukowski

Thank you for your answer! I may have fixed the issue but I’ll send to you just in case the OVITO session file with the OBJ files (they are less than 1KB each) and the XYZ file.

What I did was:

from ovito.data import DataCollection
from ovito.vis import ParticlesVis
from ovito.io import import_file

# Number of objects
n_objects = 10

# Number of particles
n_particles = 10

# First, preload the N .obj files to produce a list of TriangleMesh objects
meshes = []
for i in range(0, n_objects):
    mesh = import_file(f"Shapes/prism_{i+1:05d}.obj").compute().triangle_meshes['mesh']
    meshes.append(mesh)
    print(f"Object #{i+1:05d} | Mesh: {meshes[i]}") # The OBJ indexes begin with 1

print(" ")

# Custom modifier function, which creates the ParticleTypes for the triangle meshes and
# assigns them to the existing particles in the simulation.
def modify(frame: int, data: DataCollection):
   # Create a modifiable copy of the "Particle Type" property.
   particle_types = data.particles_.particle_types_
   for i in range(0, n_particles): # Maybe I don't need to loop over the N particles if the number of particle types is less than N
       t = particle_types.add_type_id(particle_types[i], data.particles)
       t.mesh = meshes[particle_types[i]-1] # Access one index below
       print(f"Particle type {particle_types[i]} is receiving mesh {t.mesh}")
       t.shape = ParticlesVis.Shape.Mesh

Yes, now that you load a XYZ file that contains 1000(+1) different particle types, you don’t have to newly create these types in the script anymore. It’s sufficient to modify these existing particle types and assign a shape to each of them.

I’ve prepared an updated modifier script for you that is based on the “advanced” programming interface for user-defined modifiers. The advantage of this extended interface is that the loading of the 1000 obj files can be integrated into the modify routine and be performed asynchronously. This avoids blocking the user interface during this lengthy operation.

from ovito.pipeline import ModifierInterface
from ovito.data import DataCollection
from ovito.vis import ParticlesVis
from ovito.io import import_file

class LoadShapesModifier(ModifierInterface):
    def modify(self, data: DataCollection, *, data_cache: DataCollection, **kwargs):

        # Load the 1000 .obj files to produce a list of TriangleMesh objects
        num_files = 1000
        if not (meshes := data_cache.attributes.get('meshes', None)):
            meshes = data_cache.attributes['meshes'] = []
            yield f"Loading {num_files} mesh files..."
            for i in range(num_files):
                yield i / num_files
                mesh = import_file(f"Shapes/prism_{i+1:05d}.obj").compute().triangle_meshes['mesh']
                meshes.append(mesh)

        # Modify the types list of the "Particle Type" property.
        particle_types = data.particles_.particle_types_
        for t in particle_types.types_:
            if t.id != 0:
                t.mesh = meshes[t.id - 1]
                t.shape = ParticlesVis.Shape.Mesh

Amazing! Thank you very much for that! The data is loading much faster now and the visualization of the trajectory is not freezing at every frame.