#!/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)