Migrating to TorchANI 2#

If you were using a previous version of TorchANI you may need to update your code to work with TorchANI 2. We strive to keep backwards compatibility, but some minor breaking changes were necessary in order to support improvements in the models, dataset management, etc. Minor versions changes attempt to be fully backwards compatible, and breaking changes are reserved for major releases.

Here we document the most important changes. In many cases code will run as is, but some warnings are emitted if the old, legacy API is being used, so we also provide recommendations to use the new functions when appropriate.

General usage of built-in ANI models#

If you were previously calling torchani models as:

import torchani

species_indices = torch.tensor([[0, 1, 1, 0]])
coords = torch.tensor(...)
model = torchani.models.ANI1x()

species_indices, energies = model((species_indices, coords))
# or
energies = model((species_indices, coords)).energies

you may prefer to do instead:

import torchani
from torchani import single_point

atomic_nums = torch.tensor([[1, 6, 6, 1]])
coords = torch.tensor(...)
model = torchani.models.ANI1x()

result = single_point(model, atomic_nums, coords)
energies = result["energies"]

Here “single-point” is typical chemistry jargon for “calculation on fixed molecule coords”. This was changed since it allows models to output more than a single scalar value, which is necessary e.g. for models that output charges. Additionally, the new allows for outputting forces and hessians without any familiarity with torch (no need to do anything with torch.Tensor.requires_grad). Calling a model directly is still possible.

To output other quantities of interest use:

result = single_point(model, atomic_nums, coords, forces=True, hessians=True)
atomic_charges = result["atomic_charges"]  # Only for models that support this
energies = result["energies"]
forces = result["forces"]
hessians = result["hessians"]

The AEVComputer, ANIModel, Ensemble, and SpeciesConverter classes#

If you were previously using these classes as:

import torchani
aevc = torchani.AEVComputer(...)
animodel = torchani.ANIModel(...)
ensemble = torchani.Ensemble(...)
converter = torchani.SpeciesConverter(...)

_, idxs = converter((species, coords), cell, pbc)
_, aevs = aevc((idxs, coords), cell, pbc)
_, energies = animodel((idxs, aevs), cell, pbc)
_, energies = ensemble((idxs, aevs), cell, pbc)

you should now do this instead:

import torchani
converter = torchani.nn.SpeciesConverter(...)
aevc = torchani.AEVComputer(...)
# Note that the following classes have different names
ani_nets = torchani.ANINetworks(...)
ensemble = torchani.Ensemble(...)

idxs = converter(atomic_nums)
aevs = aevc(idxs, coords, cell, pbc)
energies = animodel(idxs, aevs)
energies = ensemble(ixs, aevs)

Note that torchani.nn.ANIModel has been renamed to ANINetworks. The old signature is still supported for now, but it will be removed in the future

Extra notes on the AEVComputer#

It is possible now to separate the AEVComputer and Neighborlist parts of a calculation like this:

import torchani
neighborlist = torchani.neighbors.AllPairs()
aevc = torchani.AEVComputer.like_2x()
converter = torchani.SpeciesConverter(("H", "C", "N", "O"))

idxs = converter(atomic_nums)
cutoff = aevc.radial.cutoff
neighbors = neighborlist(cutoff, idxs, coords, cell, pbc)
aevc = aevc.compute_from_neighbors(idxs, neighbors)

This may be useful if you are want to use the computed neighborlist in more modules afterwards (e.g. pair potentials, or other neural networks).

Additionally, AEVComputer is now initialized with different inputs. If you prefer the old signature you can use the from_constants constructor instead.

Usage of torchani.data#

This module is deprecated, you can still access it under torchani.legacy_data, but its use is discouraged, and moving forward it will not be maintained. Use torchani.datasets instead (it is similar to torchvision.datasets which you may be familiar with).

Usage of Sequential#

The torchani.nn.Sequential class is still available, but its use is highly discouraged.

If you were previously doing:

import torchani
aev_computer = torchani.aev.AEVComputer(...)  # Lots of arguments
neural_networks = torchani.nn.ANIModel(...)  # Lots of arguments
energy_shifter = torchani.utils.EnergyShifter(...)  # More arguments
model = torchani.nn.Sequential(aev_computer, neural_networks, energy_shifter)

You should probably stop. This approach is error prone and verbose, and has multiple gotchas.

The simplest way of creating a model for training, with random initial weights, is using the factory functions in torchani.arch, such as torchani.arch.simple_ani:

from torchani.arch import simple_ani

# LoT is used for the ground state energies, returned model is ready to train
# Consult the documentation for the relevant options
model = simple_ani(("H", "C", "N", "O", "S"), lot="wb97x-631gd")

These functions are wrappers over torchani.arch.Assembler, which you can also use to create your model. For example, to create a model just like torchani.models.ANI2x, but with random initial weights and the cuAEV strategy for faster training, do:

import torchani
asm = torchani.arch.Assembler()
asm.set_symbols(("H", "C", "N", "O"))
# You can also pass your custom angular or radial terms as arguments
asm.set_aev_computer(radial="ani2x", angular="ani2x", strategy="cuaev")
# A custom class with a custom constructor is also supported
asm.set_atomic_networks(ctor="ani2x")
asm.set_gsaes_as_self_energies("wb97x-631gd")  # Add ground state atomic energies
model = asm.assemble()  # The returned model is ready to train

This takes care of all the gotchas of building a model (for instance, it ensures the AEVComputer is initialized with the the correct number of elements, that it matches the initial size of the networks, and that the internal order of the element idxs is the same for all modules). It is a pretty customizable procedure, and has good defaults. It also avoids having to return irrelevant outputs and accept irrelevant inputs in your modules.

If you want even more flexibility, we recommend you create your own torch.nn.Module, which is way easier than it sounds. As an example:

import torchani
from torch.nn import Module

class Model(Module):
    def __init__(self):
        super().__init__()
        self.converter = torchani.nn.SpeciesConverter(...)
        self.neighborlist = torchani.neighbors.AllPairs(...)
        self.aevc = torchani.aev.AEVComputer(...)
        self.nn = torchani.nn.ANINetworks(...)
        self.shifter = torchani.sae.SelfEnergy(...)

    def forward(self, atomic_nums, coords, cell, pbc):
        idxs = self.converter(atomic_nums)
        cutoff = self.aevc.radial.cutoff
        neighbors = self.neighborlist(cutoff, idxs, coords, cell, pbc)
        aevs = self.aevc.compute_from_neighbors(idxs, neighbors)
        return self.nn(idxs, aevs) + self.shifter(idxs)

model = Model()
energies = model(atomic_nums, coords, cell, pbc)  # forward is automatically called

This gives you the full flexibility of torch, at the cost of some complexity.