Orthogonal structure generation

In this notebook we will generate structures based on orthogonalization of the sensing matrix. This entails interatively generating structures by constructing cluster vectors orthogonal to all previous cluster vectors and finding the closest matching structure.

Enumerate supercells

First, we enumerate all supercells up to 25 repetitions of the primitive cell which we will randomly choose from when matching cluster vector to structure.

[1]:
%%time
import numpy as np
from ase.db import connect
from ase.io import read
from icet.tools import enumerate_supercells

# read primitive structure
primitive_structure = read('../structures/MoC_rocksalt_prim.xyz')

# matrix representation of primitive cell
P = primitive_structure.get_cell().array

# Enumerate supercells
db_sc = connect('supercells.db', append=False)
for supercell in enumerate_supercells(
    primitive_structure,
    sizes=list(range(1, 26)),
    niggli_reduce=True,
):
    # matrix representation of supercell
    C = supercell.get_cell().array

    # check that the width of the supercell is less than three
    # times the width of the primitive cell to avoid skewed cells
    H = C.dot(np.linalg.inv(P)) # Transformation matrix H P = C
    if np.amax(abs(H)) <= 3.001:
        db_sc.write(supercell)

print(f'Number of supercells: {len(db_sc) + 1}')
Number of supercells: 340
CPU times: user 2min 23s, sys: 3min 24s, total: 5min 48s
Wall time: 1min 30s

Set up cluster space

Next, we setup a cluster space with cutoffs that will be used for orthogonalization.

Note: Below “Be” is used to represent a vacant site.

[2]:
from icet import ClusterSpace

cutoffs = [9.0, 5.0]
chemical_symbols = [['C', 'Be'], ['Mo', 'V']]
cs = ClusterSpace(primitive_structure, cutoffs, chemical_symbols)

cs
[2]:

Cluster Space

FieldValue
Space groupFm-3m (225)
Sublattice A('Be', 'C')
Sublattice B('Mo', 'V')
Cutoffs[9.0, 5.0]
Total number of parameters52
Number of parameters of order 01
Number of parameters of order 12
Number of parameters of order 225
Number of parameters of order 324
fractional_position_tolerance2e-06
position_tolerance1e-05
symprec1e-05

Generate orthogonal structures

Then we are ready to start iteratively generating structures. The algorithm consists of the following steps:

  1. Generate a random cluster vector

  2. Make cluster vector orthogonal to all previous cluster vectors

  3. From the cluster vector, calculate the corresponding concentrations and impose a maximum of 30% vacancies (=Be)

  4. Select a random supercell

  5. Find the structure with the target concentration and supercell that closest matches the cluster vector and add it to the data set

  6. Repeat until the number of structures reaches the size of the cluster space (minus 1 since the zerolet is not included in the orthogonalization)

A database with a set of final DFT relaxed structures generated using this procedure is available at ../dft-databases/rocksalt_MoVCvac_orthogonal.db.

[3]:
# Helper functions
def orthogonalize_cv(cv, cv_basis):
    for cv_prev in cv_basis:
        cv = cv - np.dot(cv, cv_prev) / np.dot(cv_prev, cv_prev) * cv_prev
    return cv

def concentrations_from_singlets(cv):
    """
    Get the concentration for sublattices A (C, Be) and B (Mo, V) from
    the values of the singlets (cv[0] and cv[1] for A and B, respectively).

    The singlet has a value between 1 and -1 where 1 corresponds to 100% of the
    first species and -1 to 100% of the second species.
    """
    conc_Be = 1 - (cv[0] + 1) / 2
    conc_V = 1 - (cv[1] + 1) / 2
    return conc_Be, conc_V
[4]:
%%time
from icet.tools.structure_generation import generate_target_structure_from_supercells

db = connect('orthogonal-structures.db')

# List of basis vectors for the cluster space
cv_basis = []
count = 0

while count < len(cs) - 1:
    print('No. ', count + 1)

    # generate random cluster vector (without zerolet)
    cv = np.array([2 * i - 1 for i in np.random.rand(len(cs) - 1)])

    # orthogonalize against cluster vector basis
    cv = orthogonalize_cv(cv, cv_basis)

    # randomly select a supercell
    supercell = db_sc.get(id=np.random.randint(1, len(db_sc)+1)).toatoms()
    n_atoms_per_lattice = len(supercell) / 2

    # convert singlets to concentrations
    conc_Be, conc_V = concentrations_from_singlets(cv)

    # scale vacancy (=Be) concentration with 0.3 since
    # we only allow for a maximum of 30% vacancies
    conc_Be *= 0.3

    # round concentrations to be commensurate with supercell
    conc_Be = np.round(conc_Be*n_atoms_per_lattice)/n_atoms_per_lattice
    if conc_Be >= 0.3:  # decrease by 1 Be atom
        conc_Be = (np.round(conc_Be * n_atoms_per_lattice) - 1.0) / n_atoms_per_lattice
    conc_V = np.round(conc_V*n_atoms_per_lattice)/n_atoms_per_lattice

    conc_dict = {'A': {'C': 1.0 - conc_Be, 'Be': conc_Be},
                 'B': {'Mo': 1.0 - conc_V, 'V': conc_V}}

    # add initial cluster vector element 1.0 for zerolet
    cv = np.insert(cv, 0, 1.0)

    # find structure that matches cluster vector
    structure = generate_target_structure_from_supercells(
        cs, [supercell], conc_dict, cv)

    # get cluster vector for the identified structure
    cv = cs.get_cluster_vector(structure)

    # remove zerolet
    cv = np.delete(cv, 0)

    # calculate final concentrations
    conc_Be = structure.get_chemical_symbols().count('Be') / n_atoms_per_lattice
    conc_V = structure.get_chemical_symbols().count('V') / n_atoms_per_lattice

    # orthogonalize updated cluster vector and add to basis
    cv = orthogonalize_cv(cv, cv_basis)
    cv_basis.append(cv)

    # sanity check
    if np.linalg.norm(cv) < 1e-6:
        print('Norm of cv basis vector is 0, something probably went wrong')
        raise

    # write to database
    db.write(structure)

    count += 1
No.  1
No.  2
No.  3
No.  4
No.  5
No.  6
No.  7
No.  8
No.  9
No.  10
No.  11
No.  12
No.  13
No.  14
No.  15
No.  16
No.  17
No.  18
No.  19
No.  20
No.  21
No.  22
No.  23
No.  24
No.  25
No.  26
No.  27
No.  28
No.  29
No.  30
No.  31
No.  32
No.  33
No.  34
No.  35
No.  36
No.  37
No.  38
No.  39
No.  40
No.  41
No.  42
No.  43
No.  44
No.  45
No.  46
No.  47
No.  48
No.  49
No.  50
No.  51
CPU times: user 14min 12s, sys: 0 ns, total: 14min 12s
Wall time: 22min 44s