Skip to content

Doping/Vacancy Generator

Author: Lin Ziyue

This script consolidates the following into a single dictionary at the beginning:

  1. "Which elements to replace"
  2. "What new elements can replace each element"
  3. "Whether to delete a specific element"

Users only need to modify this dictionary; the rest of the code remains untouched.

Script Logic

  • Scans all .vasp files in the Struct/ directory
  • Performs doping/deletion operations on each file sequentially
  • Removes duplicates using ASE's SymmetryEquivalenceCheck
  • Writes each unique structure to
    Results/<original_filename_without_extension>/<scheme_ID>/unique_<number>.vasp
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Doping / vacancy generator
-------------------------------------------------
Just modify the `doping_plan` below to your needs.
Don't touch the rest; simply run:
    python dopant_generator.py
Requires ASE >= 3.22
"""

import os
import itertools
from collections import OrderedDict

from ase.io import read, write
from ase.utils.structure_comparator import SymmetryEquivalenceCheck

# ================================================================
# 1. User Section: Define doping / vacancy scheme
# ------------------------------------------------
# Key: host element to be replaced or deleted
# Value: a list
#   • Regular substitution -> element symbol directly, e.g., 'La'
#   • Create vacancy -> string 'VAC' (VAC = vacancy)
#
# Example (feel free to modify/add/remove):
#   Hf can be replaced by either 'La' or 'Y'
#   O  can be deleted to create a vacancy via 'VAC'
#   Al can be doped with 'Ga'
#
# You can have 1, 2, 3 ... up to N entries.
# ================================================================

doping_plan = OrderedDict([
    ('Hf', ['La', 'Y']),
    ('O',  ['VAC']),      # Use VAC to indicate O deletion
    # ('Al', ['Ga']),     # Uncomment if needed
])

# ================================================================
# 2. Script Section: Do not modify
# ================================================================

comp      = SymmetryEquivalenceCheck()       # Check structural equivalence
src_dir   = 'Struct'                         # Original VASP files folder
dst_root  = 'Results'                        # Output root directory
os.makedirs(dst_root, exist_ok=True)

vasp_files = [f for f in os.listdir(src_dir) if f.endswith('.vasp')]

def label_from_choice(choice_dict):
    """Convert a combination choice to folder name, e.g., Hf→La__O→VAC"""
    parts = [f"{host}{dop}" for host, dop in choice_dict.items()]
    return "__".join(parts)

for vfile in vasp_files:
    atoms0   = read(os.path.join(src_dir, vfile))
    basename = os.path.splitext(vfile)[0]

    # Collect all indices of each host element in the structure
    host_indices = {host: [i for i, a in enumerate(atoms0) if a.symbol == host]
                    for host in doping_plan}

    # Generate all Cartesian product combinations
    dopant_lists = doping_plan.values()          # e.g., (['La','Y'], ['VAC'])
    for dopant_choice in itertools.product(*dopant_lists):
        # dopant_choice looks like ('La', 'VAC')
        choice_map   = {host: dop for host, dop in zip(doping_plan, dopant_choice)}
        scheme_label = label_from_choice(choice_map)

        # For the same scheme, multiple host atoms may exist; iterate through all index combinations
        index_lists = [host_indices[host] for host in doping_plan]  # [[3,10],[7,15,20], …]
        for pick in itertools.product(*index_lists):
            # pick is the specific atom index combination to actually modify
            new_atoms = atoms0.copy()

            # To ensure correct vacancy deletion order, reverse-sort the atom indices for replacement
            for (host, dop), idx in zip(choice_map.items(), pick):
                if dop == 'VAC':
                    new_atoms.pop(idx)           # Delete host atom
                else:
                    new_atoms[idx].symbol = dop  # Direct replacement

            # Remove duplicates via symmetry check against structures already generated for current scheme
            folder = os.path.join(dst_root, basename, scheme_label)
            os.makedirs(folder, exist_ok=True)

            is_dup = False
            for exist_file in os.listdir(folder):
                ref = read(os.path.join(folder, exist_file))
                if comp.compare(new_atoms, ref):
                    is_dup = True
                    break
            if is_dup:
                continue

            # Write file
            out_name = f"unique_{len(os.listdir(folder))}.vasp"
            write(os.path.join(folder, out_name), new_atoms)

print("Done! Results written to", dst_root)

How to Modify the Script

  • Only modify doping_plan.

  • To replace all Hf with Nb only:

    doping_plan = OrderedDict([('Hf', ['Nb'])])
    

  • To try "Hf→La or Y" while also deleting one O in the same script:

    Simply modify the example dictionary as needed.

  • To add an additional "Al→Ga" doping:

    1
    2
    3
    4
    5
    doping_plan = OrderedDict([
        ('Hf', ['La', 'Y']),
        ('O',  ['VAC']),
        ('Al', ['Ga']),
    ])
    

Run:

python dopant_generator.py

Result Directory Structure

1
2
3
4
5
6
Results/
└─ <original_VASP_filename>/
   ├─ Hf→La__O→VAC/
   │  └─ unique_0.vasp …
   └─ Hf→Y__O→VAC/
      └─ unique_0.vasp …

All files undergo the same operations independently, making it convenient for subsequent calculations.