|
| 1 | +# Quick Introduction to TBLite - a Practical Guide |
| 2 | + |
| 3 | +## Installation |
| 4 | +Under a conda environment, install tblite and tblite-python through the conda-forge channel. |
| 5 | +`conda install -c conda-forge tblite` |
| 6 | +`conda install -c conda-forge tblite-python` |
| 7 | +Make sure you also have ASE installed as well as the basic scientific programming packages. |
| 8 | + |
| 9 | +## Python API and Usage in ASE |
| 10 | +### Single-Point Calculations |
| 11 | +The skeleton of the script is very simple: the calculator is initialised, and attached to your atom object. Then, most functionality of ASE is readily available. |
| 12 | +```python |
| 13 | +from tblite.ase import TBLite |
| 14 | +from ase.build import fcc111, molecule, add_adsorbate |
| 15 | + |
| 16 | +# Create or import your atoms object |
| 17 | +slab = fcc111('Ni', size=(2,2,3)) |
| 18 | +add_adsorbate(slab, 'H', height=1.5, 'ontop') |
| 19 | +slab.center(vacuum=10.0, axis=2) |
| 20 | + |
| 21 | +# Set up calculator |
| 22 | +calc = TBLite(method="GFN1-xTB") |
| 23 | + |
| 24 | +# Attach to atoms object |
| 25 | +slab.calc = calc |
| 26 | +slab.get_potential_energy() |
| 27 | +``` |
| 28 | + |
| 29 | +### Structure Relaxation |
| 30 | +In order to perform a relaxation, we need an external optimizer that allows for convergence. |
| 31 | +ASE has several optimizers that can be used: https://wiki.fysik.dtu.dk/ase/ase/optimize.html |
| 32 | +However, the following are recommended: |
| 33 | +- Fast Internal Relaxation Engine: FIRE |
| 34 | +- Non-linear (Polak-Ribiere) conjugate gradient algorithm: SciPyFminCG |
| 35 | +- Broyden–Fletcher–Goldfarb–Shanno algorithm: BFGS |
| 36 | + |
| 37 | +Let's carry on with the previous example: |
| 38 | +```python |
| 39 | +# Import optimizer from ASE |
| 40 | +from ase.optimize.sciopt import SciPyFminCG |
| 41 | + |
| 42 | +# Set up optimizer and run |
| 43 | +optimizer = SciPyFminCG(slab, trajectory='path/to/directory/system_relaxation.traj') |
| 44 | +optimizer.run(fmax=0.05) |
| 45 | +``` |
| 46 | + |
| 47 | +#### Trajectory Files |
| 48 | +A trajectory file will be created in the specifier directory. This file contains all the ionic steps and can be visualised with ase gui. Each frame of this "movie" is an atoms object that can be imported as a vasp file. |
| 49 | + |
| 50 | +First, let's start by storing the optimized structure: |
| 51 | +```python |
| 52 | +from ase.io import write, read |
| 53 | + |
| 54 | +# Write the relaxed CO2 structure to a VASP file |
| 55 | +write('path/to/directory/system_relaxed.vasp', slab, format='vasp') |
| 56 | +``` |
| 57 | +We can also read/write specific frames: |
| 58 | +```python |
| 59 | +# Extract specific frames |
| 60 | +frames = read('path/to/directory/system_relaxation.traj', index='0,3,5') |
| 61 | + |
| 62 | +# Write extracted frames as individual VASP files |
| 63 | +for i, atoms in enumerate(frames): |
| 64 | + write(f'path/to/directory/frame_{i}.vasp', atoms, format='vasp') |
| 65 | +``` |
| 66 | +In the command line: |
| 67 | +- Specify images to load: ase gui filename.traj -i 0,3,5 |
| 68 | +- A continuous range of frames: ase gui filename.traj -i 1:5 |
| 69 | + |
| 70 | +### Calculation of Vibrational Modes - Entropic Correction |
| 71 | +After the `optimizer.run(fmax=0.05)` line has completed, the ASE atoms object will be in its optimized state. Calling `get_potential_energy()` on it will return the total energy of this optimized state. |
| 72 | +```python |
| 73 | +latest_total_energy = slab.get_potential_energy() |
| 74 | +print("Latest total energy:", latest_total_energy, "eV") |
| 75 | +``` |
| 76 | +TBLite provides the electronic free energy (that is why we set an electronic temperature). We can compute the nuclear contribution associated with the vibration degrees of freedom with TBlite. For a better explanation of such entropic contribution, check out the Notion page. |
| 77 | +```python |
| 78 | +from ase.vibrations import Vibrations |
| 79 | +from ase.thermochemistry import HarmonicThermo |
| 80 | + |
| 81 | +vib = Vibrations(slab, name='path/to/directory/CO2_vibrations') # Specify the name or path where you want the data saved |
| 82 | + |
| 83 | +# Calculate vibrations |
| 84 | +vib.run() |
| 85 | + |
| 86 | +# Once the calculations are done, you can get the vibrational frequencies/energies, etc |
| 87 | +frequencies = vib.get_frequencies() |
| 88 | +energies = vib.get_energies() |
| 89 | + |
| 90 | +# Calculate the Helmholtz energy for each hydrogen and store it in the list |
| 91 | +thermo_properties = HarmonicThermo(vib_energies=energies) |
| 92 | +F = thermo_properties.get_helmholtz_energy(temperature=298.0) |
| 93 | +print("Helmholtz Energy:", F, "eV") |
| 94 | + |
| 95 | +# Total final energy: |
| 96 | +print("Total final energy:", latest_total_energy - F, "eV") |
| 97 | +``` |
| 98 | +For more information, check https://wiki.fysik.dtu.dk/ase/ase/thermochemistry/thermochemistry.html#id3 |
| 99 | + |
| 100 | +## Parallelizing the script |
| 101 | +So far, we have a script that performs various calculations on a H/Ni(111) system, namely geometry optimization followed by vibrational frequency calculations, and then computes thermodynamic properties based on those frequencies. |
| 102 | + |
| 103 | +To parallelize this script we are going to use `joblib`. Let’s consider a case where we want to run calculations on different molecules in parallel. Let's assume we have a list of molecules for which you want to calculate the "Total final energy." |
| 104 | +#### Dummy Example: |
| 105 | +```python |
| 106 | +from joblib import Parallel, delayed |
| 107 | + |
| 108 | +def compute(x): |
| 109 | + return result |
| 110 | + |
| 111 | +slow_results = [compute(x) for x in data] # Run in series |
| 112 | +faster_results = Parallel(n_jobs=4)(delayed(compute(x) for x in data) # Run in parallel with joblib |
| 113 | +``` |
| 114 | +#### Practical Example: CO2, H2O, NH3 - Full Relaxation |
| 115 | +```python |
| 116 | +from tblite.ase import TBLite |
| 117 | +from ase.build import molecule |
| 118 | +from ase.optimize import FIRE |
| 119 | +from joblib import Parallel, delayed |
| 120 | + |
| 121 | +def calculate_final_energy(molecule_name): |
| 122 | + atoms = molecule(molecule_name) |
| 123 | + |
| 124 | + # Set up calculator |
| 125 | + calc = TBLite( |
| 126 | + method="GFN1-xTB", |
| 127 | + accuracy=1.0, |
| 128 | + max_iterations=250, |
| 129 | + electronic_temperature=300, |
| 130 | + ) |
| 131 | + |
| 132 | + # Attach to atoms object |
| 133 | + atoms.calc = calc |
| 134 | + atoms.get_potential_energy() |
| 135 | + |
| 136 | + # Set up optimizer and run |
| 137 | + optimizer = FIRE(atoms, trajectory=f'path/to/directory/{molecule_name}_relaxation.traj') |
| 138 | + optimizer.run(fmax=0.2) |
| 139 | + latest_total_energy = atoms.get_potential_energy() |
| 140 | + |
| 141 | + return molecule_name, latest_total_energy |
| 142 | + |
| 143 | +# Let's assume a list of molecules |
| 144 | +molecules = ['CO2', 'H2O', 'NH3'] |
| 145 | + |
| 146 | +results = Parallel(n_jobs=-1)(delayed(calculate_final_energy)(molecule_name) for molecule_name in molecules) |
| 147 | + |
| 148 | +for molecule_name, energy in results: |
| 149 | + print(f"Total final energy for {molecule_name}:", energy, "eV") |
| 150 | +``` |
| 151 | +1. We've encapsulated the calculations inside a function `calculate_final_energy` which takes a `molecule_name` as an argument. |
| 152 | +2. We use `Parallel` and `delayed` from `joblib` to parallelize the calculations across the list of molecules. |
| 153 | +3. This script will run the calculations in parallel using all available CPU cores (`n_jobs=-1`). This is not recommended if you try it for the first time. |
0 commit comments