Constructing an Interchange

An Interchange is most commonly constructed via the Interchange.from_smirnoff() class method. from_smirnoff() takes an OpenFF Toolkit ForceField and applies it to a molecular topology to construct an Interchange. ForceField implements SMIRNOFF, the Open Force Field Initiative’s next-generation force field format. It uses SMIRKS codes instead of atom types to describe the chemistry of each parameter, which both reduces the number of parameters needed to accurately describe biomolecular systems and makes augmenting and extending the force field much simpler.

The molecular topology is simply a description of the system that should be parameterized. It is usually given as a list of OpenFF Toolkit Molecule objects, though specialist users can pass in a Topology instead. A Molecule describes a single molecule, including its atoms, formal charges, and full connectivity information. Molecule objects can be constructed in a variety of ways from most popular molecular data formats. A Molecule describes chemistry directly, rather than in terms of a molecular mechanics model; the Interchange and ForceField are responsible for applying simulation parameters.

Positions and velocities can be applied to an Interchange directly via the .positions and .velocities attributes. Likewise, box vectors can be applied to the .box attribute. Positions and velocities for a system of n atoms are taken as n×3 Numpy arrays, and boxes are described as 3×3 arrays of box vectors. As with all numerical inputs to OpenFF software, OpenFF recommends assigning units; see Units in Interchange.

--- alt: "Flowchart describing the construction and use of an Interchange (See textual description below)" align: center --- flowchart LR OFFXML SMILES/SDF/PDB subgraph np [numpy] BoxVecs[Box vectors] Positions Velocities end subgraph tk [openff.toolkit] Molecule([Molecule]) ForceField([ForceField]) end subgraph int [openff.interchange] Interchange[(Interchange)] FromSmirnoff[["from_smirnoff()"]] end style tk fill:#2f9ed2,color:#fff,stroke:#555; style int fill:#ee4266,color:#fff,stroke:#555; style np fill:#eee,color:#000,stroke:#555; classDef default stroke:#555; classDef code font-family:cousine,font-size:10pt,font-weight:bold; class FromSmirnoff,Molecule,ForceField,Interchange,tk,int,np code OFFXML --> ForceField --> FromSmirnoff SMILES/SDF/PDB --> Molecule --> FromSmirnoff FromSmirnoff --> Interchange Positions -.-> Molecule Positions -.-> Interchange BoxVecs -.-> FromSmirnoff BoxVecs -.-> Interchange Velocities -...-> Interchange

from_smirnoff() also includes some convenience tools for assigning box vectors and positions. If every Molecule passed to from_smirnoff() has at least one conformer defined, atom positions for the new Interchange are taken from the zeroth conformer of each Molecule. This allows conformers generated by the OpenFF Toolkit or taken from a structure file to be used with Interchange. Box vectors can also be supplied to the from_smirnoff() method via the box argument, or taken from the Topology. Thus, constructing a system consisting of a single ethane molecule in a 10×10×10 Angstrom box with the Sage force field can be done very simply:

from openff.interchange import Interchange
from openff.toolkit import Molecule, ForceField
from openff.units import unit
import numpy as np

mol = Molecule.from_smiles("CC")
mol.generate_conformers()
sage = ForceField("openff-2.0.0.offxml")
cubic_box = unit.Quantity(30 * np.eye(3), unit.angstrom)

interchange = Interchange.from_smirnoff(topology=[mol], force_field=sage, box=cubic_box)

Content of an Interchange object

An Interchange object stores all the information known about a system; this includes its chemistry, how that chemistry is represented by a force field, and how the system is organized in 3D space. An Interchange object has four components:

  1. Topology: Stores chemical information, such as connectivity and formal charges, independently of force field

  2. Collections: Maps the chemical information to force field parameters. The Force Field itself is not stored in the Interchange

  3. Positions: Cartesian co-ordinates of atoms

  4. Box vectors: Periodicity information

  5. Velocities: Cartesian velocities of atoms

None are strictly required; an Interchange object can be constructed containing none of the above components:

from openff.interchange import Interchange

empty_interchange = Interchange()
assert empty_interchange.topology is None
assert empty_interchange.collections is {}
assert empty_interchange.positions is None
assert empty_interchange.box is None
assert empty_interchange.velocities is None

An empty Interchange is not useful in itself, but a meaningful object can be constructed programmatically by building and assigning each component piecemeal. Interchange provides a broad API for automated construction which makes construction easier and less error prone in common cases.

Topology

The molecular topology of an Interchange is stored as an OpenFF Toolkit Topology object in the Interchange.topology property. This Topology model is able to store rich chemical information (atoms connected by bonds with fractional bond orders, bond and atom stereochemistry, aromaticity, etc.) or minimal, molecular mechanics-focused representations (particles with masses that are connected to each other) as needed.

A topology is technically optional, and any conversions requiring topological information will fail if any required data is missing. However, most conversions do require some information from the topology.

Collections

Collections are classes that store force field information in a format that associates assigned parameters with their source and allows parameters to be inspected and even modified with the full power of SMIRKS-based direct chemical perception. They are discussed further in Tweaking and Inspecting Parameters

Collections are optional, though any conversions requiring force field parameters will fail if the necessary data are missing.

Positions and velocities

Particle positions and velocities are each stored as a unit-tagged \(N×3\) matrix, where \(N\) is the number of particles in the topology. For systems with no virtual sites, \(N\) is the number of atoms; for systems with virtual sites, \(N\) is the number of atoms plus the number of virtual sites.

Positions and velocities are represented in nanometers and nanometers per femtosecond respectively both internally and when reported. However, they can be set with positions of other units, and can also be converted as desired. Internally, Interchange uses openff-units to store units, but units from the openmm.units or unyt packages are also supported. See [] (interchange_units) for more details.

Interchange assumes positions and velocities without units are expressed in the default units above. The setter will immediately tag the unitless vectors with units; the internal state always has units explicitly associated with the values.

Positions and velocities are optional, though any conversions requiring them will fail if they are missing.

>>> from openff.interchange import Interchange
>>> from openff.units import unit
>>> from openff.toolkit.topology import Molecule
>>> molecule = Molecule.from_smiles("CCO")
>>> molecule.generate_conformers(n_conformers=1)
>>> molecule.conformers[0]
Quantity(value=array([[ 0.88165321, -0.04478118, -0.01474324],
       [-0.58171004, -0.37572459,  0.05098497],
       [-1.35004062,  0.75806983,  0.17615782],
       [ 1.26504668,  0.17421359,  1.01224746],
       [ 1.01649295,  0.87054063, -0.60898906],
       [ 1.47635802, -0.89454965, -0.39185017],
       [-0.78535559, -0.99682774,  0.96832828],
       [-0.83550563, -1.00354494, -0.81588946],
       [-1.08693898,  1.51260405, -0.3762466 ]]), unit=angstrom)
>>> model = Interchange()
>>> model.positions is None
True
>>> model.positions = molecule.conformers[0]
>>> model.positions
<Quantity([[ 0.08816532 -0.00447812 -0.00147432]
 [-0.058171   -0.03757246  0.0050985 ]
 [-0.13500406  0.07580698  0.01761578]
 [ 0.12650467  0.01742136  0.10122475]
 [ 0.10164929  0.08705406 -0.06089891]
 [ 0.1476358  -0.08945496 -0.03918502]
 [-0.07853556 -0.09968277  0.09683283]
 [-0.08355056 -0.10035449 -0.08158895]
 [-0.1086939   0.1512604  -0.03762466]], 'nanometer')>
>>> model.positions.m_as(unit.angstrom)
array([[ 0.88165321, -0.04478118, -0.01474324],
       [-0.58171004, -0.37572459,  0.05098497],
       [-1.35004062,  0.75806983,  0.17615782],
       [ 1.26504668,  0.17421359,  1.01224746],
       [ 1.01649295,  0.87054063, -0.60898906],
       [ 1.47635802, -0.89454965, -0.39185017],
       [-0.78535559, -0.99682774,  0.96832828],
       [-0.83550563, -1.00354494, -0.81588946],
       [-1.08693898,  1.51260405, -0.3762466 ]])

Box vectors

Information about the periodic box of a system is stored as a unit-tagged \(3×3\) matrix, following conventional periodic box vectors and the implementation in OpenMM.

Box vectors are represented with nanometers internally and when reported. However, they can be set with box vectors of other units, and can also be converted as desired.

If box vectors are passed to the setter without tagged units, nanometers will be assumed. The setter will immediately tag the unitless vectors with units; the internal state always has units explicitly associated with with the values.

If a \(1×3\) matrix (array) is passed to the setter, it is assumed that these values correspond to the lengths of a rectangular unit cell (\(a_x\), \(b_y\), \(c_z\)).

Box vectors are optional; if it is None it is implied that the Interchange object represents a non-periodic system. However, note that passing None to the box argument of Interchange.from_smirnoff() may produce a periodic Interchange if box vectors are present in the Topology.


>>> from openff.interchange import Interchange
>>> from openff.units import unit
>>> import numpy as np
>>> model = Interchange()
>>> model.box is None
True
>>> model.box = np.eye(3) * 4 * unit.nanometer
>>> model.box
<Quantity([[4. 0. 0.]
 [0. 4. 0.]
 [0. 0. 4.]], 'nanometer')>
>>> model.box = [3, 4, 5]
<Quantity([[3. 0. 0.]
 [0. 4. 0.]
 [0. 0. 5.]], 'nanometer')>
>>> model.box = [28, 28, 28] * unit.angstrom
<Quantity([[2.8 0. 0.]
 [0. 2.8 0.]
 [0. 0. 2.8]], 'nanometer')>
>>> model.box.m_as(unit.angstrom)
array([[28.,  0.,  0.],
       [ 0., 28.,  0.],
       [ 0.,  0., 28.]])