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:
Topology: Stores chemical information, such as connectivity and formal charges, independently of force field
Collections: Maps the chemical information to force field parameters. The Force Field itself is not stored in the
Interchange
Positions: Cartesian co-ordinates of atoms
Box vectors: Periodicity information
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.]])