Getting Started#
The main object of dxtb is the Calculator class, which is used
to perform calculations on a given system.
Note that all quantities are in atomic units.
Creating a Calculator#
The constructor always requires the atomic numbers of the system(s) and a
tight-binding parametrization.
Currently, we provide the GFN1_XTB and GFN2_XTB
parametrizations out of the box.
If you directly use the corresponding
GFN1Calculator, only the atomic numbers are required.
import torch
import dxtb
numbers = torch.tensor([3, 1]) # LiH
calc1 = dxtb.calculators.GFN1Calculator(numbers)
# equivalent
calc2 = dxtb.Calculator(numbers, dxtb.GFN1_XTB)
We recommend to always pass the (floating point) dtype and
device arguments to the constructor to ensure consistency.
import torch
import dxtb
dd = {"dtype": torch.double, "device": torch.device("cpu")}
numbers = torch.tensor([3, 1], device=dd["device"])
calc = dxtb.calculators.GFN1Calculator(numbers, **dd)
Using the Calculator#
Now, you can request various properties of the system. The most common one is the total energy.
import torch
import dxtb
dd = {"dtype": torch.double, "device": torch.device("cpu")}
numbers = torch.tensor([3, 1], device=dd["device"])
calc = dxtb.calculators.GFN1Calculator(numbers, **dd)
positions = torch.tensor([[0.0, 0.0, 0.0], [0.0, 0.0, 1.0]], **dd)
energy = calc.energy(positions)
print(energy)
If your system is charged or has unpaired electrons, you need to supply both
quantities as optional keyword arguments to energy().
energy = calc.energy(positions, chrg=0, spin=0)
Instead of calling the energy() method, you can also
use corresponding getters get_energy():
energy = calc.get_energy(positions, chrg=0, spin=0)
We recommend using the getters, as they provide the familiar ASE-like interface.
Gradients#
To calculate the gradients of the energy with respect to the atomic positions,
you can use the standard torch.autograd.grad() function.
Remember to set the requires_grad attribute of the positions tensor to
True.
import torch
import dxtb
dd = {"dtype": torch.double, "device": torch.device("cpu")}
numbers = torch.tensor([3, 1], device=dd["device"])
calc = dxtb.calculators.GFN1Calculator(numbers, **dd)
positions = torch.tensor([[0.0, 0.0, 0.0], [0.0, 0.0, 1.0]], **dd)
positions.requires_grad_(True)
energy = calc.energy(positions)
(g,) = torch.autograd.grad(energy, positions)
print(g)
For convenience, you can use the forces() or
get_forces() method directly.
forces = calc.forces(positions)
forces = calc.get_forces(positions)
The equivalency of the two methods (except for the sign) can be verified by the example here.
Warning
If you supply the same inputs to the calculator multiple times with
gradient tracking enabled, you have to reset the calculator in between with
reset(). Otherwise, the gradients will be wrong.
Example
import torch
import dxtb
dd = {"dtype": torch.double, "device": torch.device("cpu")}
numbers = torch.tensor([3, 1], device=dd["device"])
positions = torch.tensor([[0.0, 0.0, 0.0], [0.0, 0.0, 1.0]], **dd)
calc = dxtb.calculators.GFN1Calculator(numbers, **dd)
pos = positions.clone().requires_grad_(True)
energy = calc.energy(pos)
(g1,) = torch.autograd.grad(energy, pos)
# wrong gradients without reset here
calc.reset()
pos = positions.clone().requires_grad_(True)
energy = calc.energy(pos)
(g2,) = torch.autograd.grad(energy, pos)
assert torch.allclose(g1, g2)
More Properties#
Besides get_energy() / energy()
and get_forces() / forces(),
the Calculator class provides methods to calculate various other
quantities. The full list is given below:
energy(): Total energy.forces(): Nuclear forces (negative gradient).dipole(): Electric dipole moment.dipole_deriv(): Derivative of electric dipole moment w.r.t. nuclear positions.polarizability(): Electric dipole polarizability.pol_deriv(): Derivative of electric dipole polarizability w.r.t. nuclear positions.hyperpolarizability(): Electric hyperpolarizability.hessian(): Hessian matrix.vibration(): Vibrational frequencies and normal modes.ir(): Infrared intensities.raman(): Raman intensities.
Each method has a corresponding getter and some additional properties are also accessible via getters:
get_normal_modes(): Normal modes from vibrational analysis.get_frequencies(): Vibrational frequencies.get_ir_intensities(): Infrared intensities.get_raman_intensities(): Raman intensities.get_raman_depol(): Raman depolarization ratios.get_charges()/get_mulliken_charges(): Mulliken charges from SCF.get_iterations(): Number of SCF iterations.
Note that all methods (except energy()) utilize
automatic derivatives. For comparison, each method also has a numerical
counterpart, e.g., forces_numerical().
Note
Caching
These methods only calculate the requested property. To also store
associated properties, turn on caching by passing
{"cache_enabled": True} to the calculator options. This avoids
redundant calculations. For example, with caching,
get_hessian() also stores the forces and the energy.
Hence, a subsequent get_forces() does not
necessitate an additional calculation.
For more details, please see here.