Note
Click here to download the full example code
Vibrational Analysis¶
The module nnp.vib contains tools to compute analytical hessian
and do vibrational analysis.
Let’s first import all the packages we will use:
import torch
from torch import Tensor
from typing import NamedTuple, Optional
With torch.autograd automatic differentiation engine, computing analytical
hessian is very simple: Just compute the gradient of energies with respect
to forces, and then compute the gradient of each element of forces with respect
to coordinates again.
The torch.autograd.grad returns a list of Optional[Tensor]. To be
compatible with torch.jit, we need to use assert statement to allow
torch.jit to do type refinement.
def _get_derivatives_not_none(x: Tensor, y: Tensor, retain_graph: Optional[bool] = None,
                              create_graph: bool = False) -> Tensor:
    ret = torch.autograd.grad(
        [y.sum()], [x], retain_graph=retain_graph, create_graph=create_graph)[0]
    assert ret is not None
    return ret
def hessian(coordinates: Tensor, energies: Optional[Tensor] = None,
            forces: Optional[Tensor] = None) -> Tensor:
    """Compute analytical hessian from the energy graph or force graph.
    Arguments:
        coordinates: Tensor of shape `(molecules, atoms, 3)` or `(atoms, 3)`
        energies: Tensor of shape `(molecules,)`, or scalar, if specified,
            then `forces` must be `None`. This energies must be computed
            from `coordinates` in a graph.
        forces: Tensor of shape `(molecules, atoms, 3)` or `(atoms, 3)`,
            if specified, then `energies` must be `None`. This forces must
            be computed from `coordinates` in a graph.
    Returns:
        Tensor of shape `(molecules, 3 * atoms, 3 * atoms)` or `(3 * atoms, 3 * atoms)`
    """
    if energies is None and forces is None:
        raise ValueError('Energies or forces must be specified')
    if energies is not None and forces is not None:
        raise ValueError('Energies or forces can not be specified at the same time')
    if forces is None:
        assert energies is not None
        forces = -_get_derivatives_not_none(coordinates, energies, create_graph=True)
    flattened_force = forces.flatten(start_dim=-2)
    force_components = flattened_force.unbind(dim=-1)
    return -torch.stack([
        _get_derivatives_not_none(coordinates, f, retain_graph=True).flatten(start_dim=-2)
        for f in force_components
    ], dim=-1)
Below are helper functions to compute vibrational frequencies and normal modes. The normal modes and vibrational frquencies satisfies the following equation.
where \(H\) is the Hessian matrix, \(q\) is the normal coordinates, and
is the mass for each coordinate.
This is a generalized eigen problem, which is not immediately supported by PyTorch So we solve this problem through Lowdin diagnolization:
Let \(q' = T^{\frac{1}{2}} q\), we then have
this is a regular eigen problem
class FreqsModes(NamedTuple):
    angular_frequencies: Tensor
    modes: Tensor
def vibrational_analysis(masses: Tensor, hessian: Tensor) -> FreqsModes:
    """Computing the vibrational wavenumbers from hessian.
    Arguments:
        masses: Tensor of shape `(molecules, atoms)` or `(atoms,)`.
        hessian: Tensor of shape `(molecules, 3 * atoms, 3 * atoms)` or
            `(3 * atoms, 3 * atoms)`.
    Returns:
        A namedtuple `(angular_frequencies, modes)` where
        angular_frequencies:
            Tensor of shape `(molecules, 3 * atoms)` or `(3 * atoms,)`
        modes:
            Tensor of shape `(molecules, modes, atoms, 3)` or `(modes, atoms, 3)`
            where `modes = 3 * atoms` is the number of normal modes.
    """
    inv_sqrt_mass = masses.rsqrt().repeat_interleave(3, dim=-1)
    mass_scaled_hessian = hessian * inv_sqrt_mass.unsqueeze(-2) * inv_sqrt_mass.unsqueeze(-1)
    eigenvalues, eigenvectors = torch.symeig(mass_scaled_hessian, eigenvectors=True)
    angular_frequencies = eigenvalues.sqrt()
    modes = (eigenvectors.transpose(-1, -2) * inv_sqrt_mass.unsqueeze(-2))
    new_shape = modes.shape[:-1] + (-1, 3)
    modes = modes.reshape(new_shape)
    return FreqsModes(angular_frequencies, modes)
Total running time of the script: ( 0 minutes 0.002 seconds)