Quickstart¶
This tutorial shows how to convert crystal structures to Crystal Normal Form (CNF) and explore their neighbors.
Loading Crystal Structures¶
CNF works with pymatgen Structure objects. Here are several ways to create or load structures.
From pymatgen directly¶
You can create structures programmatically using pymatgen:
from pymatgen.core import Structure, Lattice
# Create a simple NaCl structure
lattice = Lattice.cubic(5.64)
nacl = Structure(
lattice,
["Na", "Cl"],
[[0, 0, 0], [0.5, 0.5, 0.5]]
)
print(nacl)
Full Formula (Na1 Cl1) Reduced Formula: NaCl abc : 5.640000 5.640000 5.640000 angles: 90.000000 90.000000 90.000000 pbc : True True True Sites (2) # SP a b c --- ---- --- --- --- 0 Na 0 0 0 1 Cl 0.5 0.5 0.5
From a CIF file¶
# Load from CIF (uncomment and provide your own path)
# struct = Structure.from_file("path/to/structure.cif")
# Or use the UnitCell helper class
# from cnf import UnitCell
# unit_cell = UnitCell.from_cif("path/to/structure.cif")
From ASE Atoms¶
from ase import Atoms
from ase.build import bulk
from pymatgen.io.ase import AseAtomsAdaptor
# Create an ASE Atoms object
si_ase = bulk('Si', 'diamond', a=5.43)
print("ASE Atoms:", si_ase)
# Convert to pymatgen Structure
si_pmg = AseAtomsAdaptor.get_structure(si_ase)
print("\npymatgen Structure:")
print(si_pmg)
ASE Atoms: Atoms(symbols='Si2', pbc=True, cell=[[0.0, 2.715, 2.715], [2.715, 0.0, 2.715], [2.715, 2.715, 0.0]]) pymatgen Structure: Full Formula (Si2) Reduced Formula: Si abc : 3.839590 3.839590 3.839590 angles: 60.000000 60.000000 60.000000 pbc : True True True Sites (2) # SP a b c --- ---- ---- ---- ---- 0 Si 0 0 0 1 Si 0.25 0.25 0.25
Converting to Crystal Normal Form¶
To convert a structure to CNF, you need to specify two discretization parameters:
- xi (ξ): Lattice resolution in Ų. Smaller values give finer resolution.
- delta (δ): Motif resolution. Atomic coordinates become multiples of 1/δ.
Typical values are xi=1.0 and delta=100.
from cnf import CNFConstructor
# Create a CNF constructor with discretization parameters
xi = 1.0 # lattice resolution (Ų)
delta = 100 # motif resolution (1/δ fractional units)
constructor = CNFConstructor(xi=xi, delta=delta)
# Convert the NaCl structure to CNF
result = constructor.from_pymatgen_structure(nacl)
cnf = result.cnf
print("CNF coordinates:", cnf.coords)
print("\nLattice Normal Form (vonorms):", cnf.lattice_normal_form.vonorms.tuple)
print("Motif Normal Form:", cnf.motif_normal_form.coord_list)
CNF coordinates: (32, 32, 33, 95, 64, 64, 64, 50, 50, 50) Lattice Normal Form (vonorms): (32, 32, 33, 95, 64, 64, 64) Motif Normal Form: (50, 50, 50)
Using the UnitCell helper¶
The UnitCell class provides a convenient interface:
from cnf import UnitCell
# Create a UnitCell from a pymatgen Structure
unit_cell = UnitCell.from_pymatgen_structure(nacl)
# Convert to CNF
cnf = unit_cell.to_cnf(xi=1.0, delta=100)
print("CNF:", cnf)
CNF: CrystalNormalForm(lattice=LatticeNormalForm(vonorms=(32, 32, 33, 95, 64, 64, 64),step_size=1.0), motif=MotifNormalForm((50, 50, 50),elements=['Cl', 'Na'],delta=100), xi=1.0, delta=100)
Reconstructing a Structure from CNF¶
You can convert a CNF back to a pymatgen Structure:
# Reconstruct the structure from CNF
reconstructed = cnf.reconstruct()
print(reconstructed)
Full Formula (Na1 Cl1) Reduced Formula: NaCl abc : 5.656854 5.656854 5.744563 angles: 90.881613 90.881613 90.000000 pbc : True True True Sites (2) # SP a b c --- ---- --- --- --- 0 Cl 0 0 0 1 Na 0.5 0.5 0.5
Finding Neighbors¶
One of the key features of CNF is that every crystal has well-defined "neighbors" - structures that differ by small strains or atomic displacements. This creates a navigable graph over all possible crystal structures.
from cnf.navigation import NeighborFinder, find_neighbors
# Method 1: Use the convenience function
neighbors = find_neighbors(cnf)
print(f"Found {len(neighbors)} neighbors")
# Method 2: Use NeighborFinder for more control
finder = NeighborFinder.from_cnf(cnf)
neighbors = finder.find_neighbors(cnf)
print(f"Found {len(neighbors)} neighbors")
Found 25 neighbors Found 25 neighbors
Lattice vs Motif Neighbors¶
Neighbors come in two types:
- Lattice neighbors: Differ by a small strain (±1 step in vonorm space)
- Motif neighbors: Differ by a small atomic displacement (±1 step in fractional coordinates)
# Find lattice and motif neighbors separately
lattice_neighbors = finder.find_lattice_neighbor_cnfs(cnf)
motif_neighbors = finder.find_motif_neighbor_cnfs(cnf)
print(f"Lattice neighbors: {len(lattice_neighbors)}")
print(f"Motif neighbors: {len(motif_neighbors)}")
Lattice neighbors: 22 Motif neighbors: 4
Examining a Neighbor¶
Let's look at how a neighbor differs from the original structure:
if neighbors:
neighbor = neighbors[0]
print("Original CNF coords:", cnf.coords)
print("Neighbor CNF coords:", neighbor.coords)
# Reconstruct and compare volumes
orig_struct = cnf.reconstruct()
neighbor_struct = neighbor.reconstruct()
print(f"\nOriginal volume: {orig_struct.volume:.3f} ų")
print(f"Neighbor volume: {neighbor_struct.volume:.3f} ų")
Original CNF coords: (32, 32, 33, 95, 64, 64, 64, 50, 50, 50) Neighbor CNF coords: (32, 32, 33, 95, 64, 63, 65, 50, 50, 50) Original volume: 183.782 ų Neighbor volume: 183.739 ų
Next Steps¶
Now that you can create CNFs and find neighbors, you can:
- Use A* search to find paths between two crystal structures
- Explore the energy landscape using the waterfill algorithm
- Calculate energy barriers between phases
See the CLI reference for command-line tools, or the Python API guide for programmatic access.