Quickstart Tutorial

This tutorial builds a cube-like WulffPack nanoparticle, converts it into a voxel coordination-surface mask, samples trial positions from that mask, and runs a minimal Monte Carlo loop. The default example uses ASE EMT as a small, local potential-energy scorer. ORB-V3 can be selected when the optional ORB dependencies are installed.

The complete script is available at examples/mc/orb_v3_wulff_mc.py.

Install Tutorial Dependencies

AtomVoxelizer provides the grid machinery. This tutorial also uses ASE and WulffPack to build the nanoparticle:

pip install AtomVoxelizer ase wulffpack

For ORB-V3 scoring, install ORB and PyTorch in the environment you use for simulation. The example keeps that import optional because loading ORB can download weights and initialize accelerator libraries.

Build A Cube-Like WulffPack Nanoparticle

WulffPack creates a finite fcc particle from relative surface energies. The natoms argument is a target; the final atom count can differ because the particle is built from symmetry-compatible atomic shells.

from ase.build import bulk
from wulffpack import SingleCrystal

primitive = bulk("Pt", "fcc", a=3.92)
surface_energies = {(1, 0, 0): 1.0}
particle = SingleCrystal(surface_energies, primitive_structure=primitive, natoms=201)
atoms = particle.atoms

Using only the (100) facet creates a deliberately cube-like starting point. That makes the MC demonstration visible: accepted moves can reduce the spread in atom distances from the nanoparticle center.

WulffPack returns a finite cluster without a periodic simulation cell. A voxel grid needs an invertible cell, so the example translates the cluster into a padded cubic cell:

import numpy as np

padding = 6.0
positions = atoms.positions
span = positions.max(axis=0) - positions.min(axis=0)
cell_length = span.max() + 2.0 * padding
atoms.positions = positions - positions.min(axis=0) + padding
atoms.set_cell(np.eye(3) * cell_length)

The padding should be larger than the largest mask radius so periodic wrapping does not make opposite sides of the finite particle interact.

Build The Voxel Surface Mask

The coordination-surface mask is built with two sphere passes:

  1. Add a larger sphere around every atom. This gives each voxel a count of how many atom-centered shells overlap it.

  2. Set a smaller sphere around every atom back to zero. This removes atomic cores from the trial region.

import numpy as np
from ase.data import covalent_radii
from atomvoxelizer import VoxelGrid

radii = covalent_radii[atoms.numbers]
grid = VoxelGrid(atoms.cell.array, resolution=0.35, dtype=np.float32)
grid.add_spheres(atoms.positions, 1.4 * radii, value=1.0)
grid.set_spheres(atoms.positions, 1.1 * radii, value=0.0)

This is the same stencil-based operation described in Concepts. AtomVoxelizer visits the local sphere stencil around each atom instead of scanning every grid point against every atom.

Sample Trial Sites

For a surface trial region, sample voxels with values near three. The range 2.5 to 3.5 avoids depending on exact floating-point equality after repeated additions.

trial_sites = []
for position in grid.sample_voxels_in_range(2.5, 3.5, min_dist=0.6, seed=7):
    trial_sites.append(np.asarray(position))
    if len(trial_sites) >= 500:
        break
trial_sites = np.array(trial_sites)

Those positions are voxel centers in real space. They are useful as trial destinations or directions for Monte Carlo moves near the nanoparticle surface.

Wulff nanoparticle atoms and voxel-sampled MC trial sites

Minimal MC Loop

The example chooses likely surface atoms by radial distance, picks a local sampled voxel trial site, and moves the atom a short distance toward that site. Before scoring, the structure is relaxed with the selected ASE calculator. Each trial move is also relaxed before applying the Metropolis criterion. This makes the acceptance depend on relaxed local minima rather than on the raw trial guess. The default score is ASE EMT; ORB-V3 can be selected with --score orb-v3.

import math

rng = np.random.default_rng(11)
center = atoms.positions.mean(axis=0)
distances = np.linalg.norm(atoms.positions - center, axis=1)
movable = np.flatnonzero(distances >= np.quantile(distances, 0.65))

from ase.calculators.emt import EMT

atoms.calc = EMT()

# The full example relaxes the initial and trial structures before scoring.
current_score = atoms.get_potential_energy()
beta = 1.0 / (8.617333262145e-5 * 1500.0)

atom_index = int(rng.choice(movable))
target = trial_sites[int(rng.integers(len(trial_sites)))]
old_position = atoms.positions[atom_index].copy()
direction = target - old_position
direction *= min(1.0, 0.35 / np.linalg.norm(direction))
atoms.positions[atom_index] = old_position + direction

trial_score = atoms.get_potential_energy()
delta = trial_score - current_score
accept = delta <= 0.0 or rng.random() < math.exp(-beta * delta)
if not accept:
    atoms.positions[atom_index] = old_position

In an ORB-V3 MC workflow, the voxel part stays the same. Only the scoring function changes: evaluate the old and trial structures with ORB-V3, then apply the usual Metropolis criterion to the energy difference.

Run The Example

Run the EMT tutorial example with:

python examples/mc/orb_v3_wulff_mc.py --natoms 201 --resolution 0.35 \
    --shell-scale 1.25 --core-scale 1.05 \
    --steps 250 --score emt --temperature 1500 \
    --relax-fmax 0.05 --relax-steps 50 \
    --plot quickstart_wulff_mc_sites.png \
    --state-plot docs/source/_static/quickstart_wulff_mc_initial_final.png

The script prints the accepted move count, acceptance ratio, initial/final radial variance, and mean/max displacement from the starting structure so you can confirm that atoms actually moved during the run. It also writes an ASE trajectory by default:

examples/mc/orb_v3_wulff_mc.traj

The image below shows the final relaxed nanoparticle state from the MC run.

Final ASE-rendered nanoparticle state after relaxed voxel-guided MC steps

Open it with ASE to inspect the MC path:

ase gui examples/mc/orb_v3_wulff_mc.traj

The first frame is the starting cube-like particle. Each following frame is the relaxed structure after one MC step; rejected steps repeat the previous relaxed coordinates but still carry updated Atoms.info metadata such as mc_accepted and mc_score. Pass --trajectory "" to skip writing frames, or --trajectory path/to/file.traj to choose a different output path.

To try the optional ORB-V3 scorer after installing ORB and its model dependencies:

python examples/mc/orb_v3_wulff_mc.py --score orb-v3 --device cpu --steps 10

The ORB helper is intentionally isolated in the example script. ORB package APIs, weights, and accelerator setup can change independently of the voxel-grid workflow.