Random structure generationΒΆ

In this notebook we will generate a large pool of randomized structures.
Random here refers to a random supercell sizes with a random occupation of the Mo-V and C-vacancy sublattices.

First some helper functions are defined to make generating randomized supercells easier.

[1]:
import numpy as np
from ase.build import make_supercell


def find_sites(atoms, sym1, sym2):
    """
    Find the atomic indices corresponding to, e.g., metal sites in a structure.
    """
    mask_A = atoms.symbols == sym1
    mask_B = atoms.symbols == sym2
    mask = np.logical_or(mask_A, mask_B)
    sites = np.where(mask)[0]
    return sites


def generate_random_diagonal_P_matrix(n_atoms_in_prim, max_atoms, max_repeat_val):
    while True:
        nx = np.random.randint(1, max_repeat_val + 1)
        ny = np.random.randint(1, max_repeat_val + 1)
        nz = np.random.randint(1, max_repeat_val + 1)
        if nx * ny * nz * n_atoms_in_prim < max_atoms:
            break
    return np.diag([nx, ny, nz])


def randomly_occupy_supercell(atoms):
    """
    Randomly occupy a supercell with Mo-V on metal sites and
    C-vacancies on carbon sites.
    """

    # Find sites
    metal_sites = find_sites(atoms, *metal_symbols)
    carbon_sites = find_sites(atoms, 'C', 'Be')

    assert len(metal_sites) + len(carbon_sites) == len(atoms)
    assert len(set(metal_sites.tolist() + carbon_sites.tolist())) == len(atoms)

    # Metal occupation
    n_sites = len(metal_sites)
    sym_A, sym_B = metal_symbols
    n_A = np.random.randint(0, n_sites + 1)
    n_B = n_sites - n_A

    metal_occupations = [sym_A] * n_A + [sym_B] * n_B
    np.random.shuffle(metal_occupations)

    # Carbon occupations
    n_vacancy = np.random.randint(0, int(n_sites * x_vacancy_max) + 1)
    n_carbon = n_sites - n_vacancy

    carbon_occupations = ['C'] * n_carbon + ['Be'] * n_vacancy
    np.random.shuffle(carbon_occupations)

    # occupy supercell
    atoms.symbols[metal_sites] = metal_occupations
    atoms.symbols[carbon_sites] = carbon_occupations


def generate_random_supercell(prim, max_atoms, max_repeat_val):
    """ Generate a random (Mo, V)-(C, vacancy) supercell. """
    n_atoms_in_prim = len(prim)
    P = generate_random_diagonal_P_matrix(n_atoms_in_prim, max_atoms, max_repeat_val)
    atoms = make_supercell(prim, P)
    randomly_occupy_supercell(atoms)
    atoms.wrap()

    return atoms

We still need to define some parameters.

[2]:
# number of structures to generate
n_structures = 5000

# maximum number of repetitions of the primitive unit cell per direction
max_repeat_val = 10

# maximum number of atoms
max_atoms = 50

# allowed species on the metal sublattice
metal_symbols = ['Mo', 'V']

# maximum carbon vacancy concentration
x_vacancy_max = 0.3

Now we are set up to generate configurations.

[3]:
from ase.io import read

prim = read('../structures/MoC_rocksalt_prim.xyz')
n_atoms_in_prim = len(prim)

structures = []
for it in range(n_structures):
    atoms = generate_random_supercell(prim, max_atoms, max_repeat_val)
    structures.append(atoms)

We can generate histograms over the obtained concentrations.

[4]:
from matplotlib import pyplot as plt

fig, axes = plt.subplots(
    figsize=(4.5, 4.5),
    dpi=120,
    nrows=2,
)

x_Mo = [atoms.symbols.count('Mo') / (atoms.symbols.count('Mo') + atoms.symbols.count('V')) for atoms in structures]
x_C = [atoms.symbols.count('C') / (atoms.symbols.count('C') + atoms.symbols.count('Be')) for atoms in structures]

ax = axes[0]
ax.hist(x_Mo, bins=50)
ax.set_xlabel('Mo concentration')
ax.set_ylabel('Number of structures')

ax = axes[1]
ax.hist(x_C, bins=50)
ax.set_xlabel('C concentration')
ax.set_ylabel('Number of structures')

fig.tight_layout()
fig.align_ylabels()
../_images/part-1_random-structures_8_0.png