Tutorial 2 - Generators, Properties and Calculators

Tutorial 2 - Generators, Properties and Calculators#

      _
    /|_|\   
   / / \ \  
  /_/   \_\  
  \ \   / /  
   \ \_/ /  
    \|_|/  

SOPRANO: a Python library for generation, manipulation and analysis of large batches of crystalline structures

Developed within the CCP-NC project. Copyright STFC 2022

# Basic imports
import os, sys
sys.path.insert(0, os.path.abspath('..')) # This to add the Soprano path to the PYTHONPATH
                                          # so we can load it without installing it
# Other useful imports

import numpy as np

from ase import Atoms
from ase import io as ase_io

1 - USING GENERATORS#

Soprano provides a series of generators able to create multiple structures on one go based on simple criteria. One of these, used here, is the linspaceGen, which interpolates linearly between two extreme structures. Others are the rattleGen (generating copies of a given structure with random atomic displacements) and the airssGen (binding to AIRSS’ buildcell executable to generate random structures, only available if AIRSS is installed).

from soprano.collection import AtomsCollection
from soprano.collection.generate import linspaceGen


# Let's use the ammonia molecule switching configurations as an example
nh3coords = np.array([[ 2.5,     2.5,     2.5   ],
                      [ 3.4373,  2.5,     2.1193],
                      [ 2.0314,  3.3117,  2.1193],
                      [ 2.0314,  1.6883,  2.1193]])
nh3l = Atoms('NHHH', nh3coords, cell=[5,5,5]) # The cell is just an empty box
# Now the right version
nh3coords *= [1, 1, -1]
nh3r = Atoms('NHHH', nh3coords, cell=[5,5,5])

# Now let's build a collection of 20 intermediate steps between the two structures
nh3linsp = linspaceGen(nh3l, nh3r, steps=20, periodic=True)
# Generators can be passed directly to the AtomsCollection constructor
nh3coll = AtomsCollection(nh3linsp)

2 - PROPERTIES#

Soprano Properties are classes meant to extract complex arrays of information from collections. A number of these are provided by default, but advanced users can easily create their own class inheriting from the generic AtomsProperty class to implement particular needs.

from soprano.properties.linkage import LinkageList


# As a first experiment we try using LinkageList, a property meant to return a list of all pair interatomic distances
# in a system. This can serve as a fingerprint to distinguish different structures

# The basic usage is to just call the Property's method "get". In this way the Property is calculated with
# default parameters.
# The three shortest values (varying) are N-H distances, while the constant ones are H-H distances

print("---- Linkage List for all NH3 configurations - Default parameters\n")
print('\n'.join(['{0}'.format(x) for x in LinkageList.get(nh3coll)]), "\n\n")
---- Linkage List for all NH3 configurations - Default parameters

[1.01162016 1.01162016 1.01166387 1.62339512 1.62339512 1.6234    ]
[0.99723073 0.99723073 0.99727508 1.62339512 1.62339512 1.6234    ]
[0.98426355 0.98426355 0.98430848 1.62339512 1.62339512 1.6234    ]
[0.97277548 0.97277548 0.97282094 1.62339512 1.62339512 1.6234    ]
[0.96281947 0.96281947 0.9628654  1.62339512 1.62339512 1.6234    ]
[0.95444347 0.95444347 0.95448979 1.62339512 1.62339512 1.6234    ]
[0.94768936 0.94768936 0.94773602 1.62339512 1.62339512 1.6234    ]
[0.94259203 0.94259203 0.94263894 1.62339512 1.62339512 1.6234    ]
[0.93917843 0.93917843 0.93922551 1.62339512 1.62339512 1.6234    ]
[0.93746697 0.93746697 0.93751414 1.62339512 1.62339512 1.6234    ]
[0.93746697 0.93746697 0.93751414 1.62339512 1.62339512 1.6234    ]
[0.93917843 0.93917843 0.93922551 1.62339512 1.62339512 1.6234    ]
[0.94259203 0.94259203 0.94263894 1.62339512 1.62339512 1.6234    ]
[0.94768936 0.94768936 0.94773602 1.62339512 1.62339512 1.6234    ]
[0.95444347 0.95444347 0.95448979 1.62339512 1.62339512 1.6234    ]
[0.96281947 0.96281947 0.9628654  1.62339512 1.62339512 1.6234    ]
[0.97277548 0.97277548 0.97282094 1.62339512 1.62339512 1.6234    ]
[0.98426355 0.98426355 0.98430848 1.62339512 1.62339512 1.6234    ]
[0.99723073 0.99723073 0.99727508 1.62339512 1.62339512 1.6234    ]
[1.01162016 1.01162016 1.01166387 1.62339512 1.62339512 1.6234    ] 
# If one wants to use parameters, an instance of the Property has to be created.
# For example LinkageList accepts a parameter "size" that limits the number of distances computed.
# This can then just be called on the AtomsCollection

customLL = LinkageList(size=3)

print("---- Linkage List for all NH3 configurations - Custom parameters\n")
print('\n'.join(['{0}'.format(x) for x in customLL(nh3coll)]), "\n\n")
---- Linkage List for all NH3 configurations - Custom parameters

[1.01162016 1.01162016 1.01166387]
[0.99723073 0.99723073 0.99727508]
[0.98426355 0.98426355 0.98430848]
[0.97277548 0.97277548 0.97282094]
[0.96281947 0.96281947 0.9628654 ]
[0.95444347 0.95444347 0.95448979]
[0.94768936 0.94768936 0.94773602]
[0.94259203 0.94259203 0.94263894]
[0.93917843 0.93917843 0.93922551]
[0.93746697 0.93746697 0.93751414]
[0.93746697 0.93746697 0.93751414]
[0.93917843 0.93917843 0.93922551]
[0.94259203 0.94259203 0.94263894]
[0.94768936 0.94768936 0.94773602]
[0.95444347 0.95444347 0.95448979]
[0.96281947 0.96281947 0.9628654 ]
[0.97277548 0.97277548 0.97282094]
[0.98426355 0.98426355 0.98430848]
[0.99723073 0.99723073 0.99727508]
[1.01162016 1.01162016 1.01166387] 
# Now we can try creating a custom property. This one will calculate the center of mass of all Hydrogen atoms.
from soprano.properties import AtomsProperty


class HydrogenCOM(AtomsProperty):
    
    default_name = 'hydrogen_com' # These need to be defined for any property
    default_params = {}
    
    @staticmethod
    def extract(s): # This is where the core of the calculation happens
        # s is a single Atoms object passed to this method
        
        chemsyms = s.get_chemical_symbols()
        h_inds = [i for i, sym in enumerate(chemsyms) if sym == 'H']
        h_pos = s.get_positions()[h_inds]
        com = np.average(h_pos, axis=0)
        
        return com

print("---- Hydrogen COM for all NH3 configurations\n")
print('\n'.join(['{0}'.format(x) for x in HydrogenCOM.get(nh3coll)]), "\n\n")
---- Hydrogen COM for all NH3 configurations

[2.50003333 2.5        2.1193    ]
[2.50003333 2.5        2.15937368]
[2.50003333 2.5        2.19944737]
[2.50003333 2.5        2.23952105]
[2.50003333 2.5        2.27959474]
[2.50003333 2.5        2.31966842]
[2.50003333 2.5        2.35974211]
[2.50003333 2.5        2.39981579]
[2.50003333 2.5        2.43988947]
[2.50003333 2.5        2.47996316]
[2.50003333 2.5        2.52003684]
[2.50003333 2.5        2.56011053]
[2.50003333 2.5        2.60018421]
[2.50003333 2.5        2.64025789]
[2.50003333 2.5        2.68033158]
[2.50003333 2.5        2.72040526]
[2.50003333 2.5        2.76047895]
[2.50003333 2.5        2.80055263]
[2.50003333 2.5        2.84062632]
[2.50003333 2.5        2.8807    ] 

3 - CALCULATORS#

The Atomic Simulation Environment provides bindings to many codes in the form of calculators. These include ab initio codes like CASTEP and VASP as well as empirical force fields. These calculators can be set and used in Soprano as well. Here we’re going to use the most basic one, the Lennard-Jones force field, as an example.

from ase.calculators.lj import LennardJones
from soprano.properties.basic import CalcEnergy

nh3coll.set_calculators(LennardJones) # Creates calculators of the given type for all structures

print("---- NH3 Lennard-Jones energy for all configurations ----\n")
print('\n'.join(['{0}'.format(x) for x in CalcEnergy.get(nh3coll)]), "\n\n")
---- NH3 Lennard-Jones energy for all configurations ----

-1.3375659506414495
-0.3833070450646066
0.7294291809909723
1.961978605860948
3.2557938225344025
4.534839379738149
5.711414812707825
6.695131685684334
7.403926123523162
7.775252265974936
7.775252265974961
7.403926123523162
6.695131685684357
5.711414812707825
4.534839379738149
3.2557938225344167
1.9619786058609139
0.7294291809909723
-0.3833070450645939
-1.3375659506414495 
# Now let's try a plot
%matplotlib inline
import matplotlib.pyplot as plt

comz = np.array(HydrogenCOM.get(nh3coll))[:,2]-2.5
ljE = CalcEnergy.get(nh3coll)

plt.plot(comz, ljE)
[<matplotlib.lines.Line2D at 0x7fb679d5fbd0>]
../_images/a3e251f978567566ceb0574646a75250e6f52fa976a1a1c8ba50e7c33e11c657.png