Basic Substitutions

This guide demonstrates how to use bsym to generate symmetry-inequivalent structures by substituting atoms in a parent structure.

Overview

The bsym.interface.pymatgen.unique_structure_substitutions function takes a parent structure and generates all symmetry-unique configurations when substituting specific sites with different species.

Key parameters:

  • structure: The parent pymatgen Structure

  • to_substitute: The atomic species label to be replaced

  • site_distribution: Dictionary specifying how many of each substituting species (e.g., {'Na': 1, 'Li': 15})

Returns: A list of Structure objects, each representing a symmetry-inequivalent configuration.

Simple Example: Single Substitution

Let’s start with a simple 4×4 square lattice of lithium atoms and substitute one Li with Na.

Setting Up the Parent Structure

import numpy as np
from pymatgen.core import Structure, Lattice
from bsym.interface.pymatgen import unique_structure_substitutions

# Create a simple square lattice
coords = np.array([[0.0, 0.0, 0.0]])
atom_list = ['Li']
lattice = Lattice.from_parameters(a=1.0, b=1.0, c=1.0, alpha=90, beta=90, gamma=90)
parent_structure = Structure(lattice, atom_list, coords) * [4, 4, 1]
/home/docs/checkouts/readthedocs.org/user_builds/bsym/envs/stable/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm

This creates a 4×4 supercell with 16 lithium atoms.

print(parent_structure)
Full Formula (Li16)
Reduced Formula: Li
abc   :   4.000000   4.000000   1.000000
angles:  90.000000  90.000000  90.000000
pbc   :       True       True       True
Sites (16)
  #  SP       a     b    c
---  ----  ----  ----  ---
  0  Li    0     0       0
  1  Li    0     0.25    0
  2  Li    0     0.5     0
  3  Li    0     0.75    0
  4  Li    0.25  0       0
  5  Li    0.25  0.25    0
  6  Li    0.25  0.5     0
  7  Li    0.25  0.75    0
  8  Li    0.5   0       0
  9  Li    0.5   0.25    0
 10  Li    0.5   0.5     0
 11  Li    0.5   0.75    0
 12  Li    0.75  0       0
 13  Li    0.75  0.25    0
 14  Li    0.75  0.5     1
 15  Li    0.75  0.75    0

Performing the Substitution

Now we substitute one Li atom with Na:

unique_structures = unique_structure_substitutions(
    parent_structure, 
    'Li',
    {'Na': 1, 'Li': 15}
)

print(f"Number of unique structures: {len(unique_structures)}")
Number of unique structures: 1

Due to the high symmetry of the square lattice, all single-site substitutions are symmetry-equivalent, so we get only one unique structure.

print(unique_structures[0])
Full Formula (Na1 Li15)
Reduced Formula: NaLi15
abc   :   4.000000   4.000000   1.000000
angles:  90.000000  90.000000  90.000000
pbc   :       True       True       True
Sites (16)
  #  SP       a     b    c
---  ----  ----  ----  ---
  0  Na    0     0       0
  1  Li    0     0.25    0
  2  Li    0     0.5     0
  3  Li    0     0.75    0
  4  Li    0.25  0       0
  5  Li    0.25  0.25    0
  6  Li    0.25  0.5     0
  7  Li    0.25  0.75    0
  8  Li    0.5   0       0
  9  Li    0.5   0.25    0
 10  Li    0.5   0.5     0
 11  Li    0.5   0.75    0
 12  Li    0.75  0       0
 13  Li    0.75  0.25    0
 14  Li    0.75  0.5     1
 15  Li    0.75  0.75    0

Understanding the Output

Each structure in the returned list has a special attribute that tracks degeneracy:

na_substituted = unique_structures[0]
print(f"Number of equivalent configurations: {na_substituted.number_of_equivalent_configurations}")
Number of equivalent configurations: 16

The number_of_equivalent_configurations attribute tells us that this unique configuration has 16 symmetry-equivalent variants (one for substituting at each of the 16 sites).

More Complex Example: TiOF2 in ReO₃ Structure

Let’s look at a more realistic example: finding unique O/F orderings in TiOF2 with a pseudo-ReO3 structure.

Setting Up the Structure

a = 3.798  # lattice parameter in Ångströms

coords = np.array([[0.0, 0.0, 0.0],
                   [0.5, 0.0, 0.0],
                   [0.0, 0.5, 0.0],
                   [0.0, 0.0, 0.5]])
atom_list = ['Ti', 'X', 'X', 'X']
lattice = Lattice.from_parameters(a=a, b=a, c=a, alpha=90, beta=90, gamma=90)
unit_cell = Structure(lattice, atom_list, coords)

# Create a 2×2×2 supercell
parent_structure = unit_cell * [2, 2, 2]
print(f"Created supercell with {len(parent_structure)} atoms")
print(f"Number of X sites to substitute: {len([s for s in parent_structure if s.species_string == 'X'])}")
Created supercell with 32 atoms
Number of X sites to substitute: 0

The ‘X’ atoms represent the anion sites that we’ll substitute with O and F.

Finding Unique O/F Orderings

A 2 × 2 × 2 supercell has 24 anion sites. To get the right stoichiometry for TiOF2, we need 8 oxygen atoms and 16 fluorine atoms:

unique_structures = unique_structure_substitutions(
    parent_structure, 
    'X', 
    {'O': 8, 'F': 16}
)

print(f"Number of symmetry-inequivalent configurations: {len(unique_structures)}")
Number of symmetry-inequivalent configurations: 2664

Each structure in unique_structures represents a distinct arrangement of O and F atoms that cannot be transformed into each other by the symmetry operations of the parent structure.

Checking the Degeneracy

You can check how many equivalent configurations each unique structure represents:

for i, structure in enumerate(unique_structures[:5]): # check the first 5 structures
    n_equiv = structure.number_of_equivalent_configurations
    print(f"Structure {i}: {n_equiv} equivalent configurations")

total_configs = sum(s.number_of_equivalent_configurations for s in unique_structures)
print(f"\nTotal configurations (including equivalent): {total_configs}")
Structure 0: 3 equivalent configurations
Structure 1: 192 equivalent configurations
Structure 2: 192 equivalent configurations
Structure 3: 96 equivalent configurations
Structure 4: 96 equivalent configurations

Total configurations (including equivalent): 735471

The total number of unique structures multiplied by their degeneracies should equal the total number of ways to choose the O/F distribution without considering symmetry.

Key Points

  • unique_structure_substitutions() automatically detects the symmetry operations of your parent structure

  • The function only returns symmetry-inequivalent structures, dramatically reducing the number of configurations you need to consider

  • The number_of_equivalent_configurations attribute tracks degeneracy for statistical mechanics calculations

  • All sites labelled with to_substitute are considered as potential substitution sites

Next Steps