.. only:: html
.. note::
:class: sphx-glr-download-link-note
Click :ref:`here ` to download the full example code
.. rst-class:: sphx-glr-example-title
.. _sphx_glr_code_vib.py:
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:
.. code-block:: default
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`_.
.. _type refinement:
https://pytorch.org/docs/stable/jit.html#optional-type-refinement
.. code-block:: default
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.
.. math::
H q = \omega^2 T q
where :math:`H` is the Hessian matrix, :math:`q` is the normal coordinates, and
.. math::
T=\left[
\begin{array}{ccccccc}
m_{1}\\
& m_{1}\\
& & m_{1}\\
& & & m_{2}\\
& & & & m_{2}\\
& & & & & m_{2}\\
& & & & & & \ddots
\end{array}
\right]
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:
.. math::
H q = \omega^2 T q \Longrightarrow H q = \omega^2 T^{\frac{1}{2}} T^{\frac{1}{2}} q
Let :math:`q' = T^{\frac{1}{2}} q`, we then have
.. math::
T^{\frac{1}{2}} H T^{\frac{1}{2}} q' = \omega^2 q'
this is a regular eigen problem
.. code-block:: default
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)
.. rst-class:: sphx-glr-timing
**Total running time of the script:** ( 0 minutes 0.003 seconds)
.. _sphx_glr_download_code_vib.py:
.. only :: html
.. container:: sphx-glr-footer
:class: sphx-glr-footer-example
.. container:: sphx-glr-download sphx-glr-download-python
:download:`Download Python source code: vib.py `
.. container:: sphx-glr-download sphx-glr-download-jupyter
:download:`Download Jupyter notebook: vib.ipynb `
.. only:: html
.. rst-class:: sphx-glr-signature
`Gallery generated by Sphinx-Gallery `_