12. Using OpenMM with Software Written in Languages Other than C++

Although the native OpenMM API is object-oriented C++ code, it is possible to directly translate the interface so that it is callable from C, Fortran 95, and Python with no substantial conceptual changes. We have developed a straightforward mapping for these languages that, while perhaps not the most elegant possible, has several advantages:

  • Almost all documentation, training, forum discussions, and so on are equally useful to users of all these languages. There are syntactic differences of course, but all the important concepts remain unchanged.

  • We are able to generate the C, Fortran, and Python APIs from the C++ API. Obviously, this reduces development effort, but more importantly it means that the APIs are likely to be error-free and are always available immediately when the native API is updated.

  • Because OpenMM performs expensive operations “in bulk” there is no noticeable overhead in accessing these operations through the C, Fortran, or Python APIs.

  • All symbols introduced to a C or Fortran program begin with the prefix “OpenMM_” so will not interfere with symbols already in use.

Availability of APIs in other languages: All necessary C and Fortran bindings are built in to the main OpenMM library; no separate library is required. The Python wrappers are contained in a module that is distributed with OpenMM and that can be installed by executing its setup.py script in the standard way.

(This doesn’t apply to most users: if you are building your own OpenMM from source using CMake and want the API bindings generated, be sure to enable the OPENMM_BUILD_C_AND_FORTRAN_WRAPPERS option for C and Fortran, or OPENMM_BUILD_PYTHON_WRAPPERS option for Python. The Python module will be placed in a subdirectory of your main build directory called “python”)

Documentation for APIs in other languages: While there is extensive Doxygen documentation available for the C++ and Python APIs, there is no separate on-line documentation for the C and Fortran API. Instead, you should use the C++ documentation, employing the mappings described here to figure out the equivalent syntax in C or Fortran.

12.1. C API

Before you start writing your own C program that calls OpenMM, be sure you can build and run the two C examples that are supplied with OpenMM (see Chapter 10). These can be built from the supplied Makefile on Linux and Mac, or supplied NMakefile and Visual Studio solution files on Windows.

The example programs are HelloArgonInC and HelloSodiumChlorideInC. The argon example serves as a quick check that your installation is set up properly and you know how to build a C program that is linked with OpenMM. It will also tell you whether OpenMM is executing on the GPU or is running (slowly) on the Reference platform. However, the argon example is not a good template to follow for your own programs. The sodium chloride example, though necessarily simplified, is structured roughly in the way we recommended you set up your own programs to call OpenMM. Please be sure you have both of these programs executing successfully on your machine before continuing.

12.1.1. Mechanics of using the C API

The C API is generated automatically from the C++ API when OpenMM is built. There are two resulting components: C bindings (functions to call), and C declarations (in a header file). The C bindings are small extern (global) interface functions, one for every method of every OpenMM class, whose signatures (name and arguments) are predictable from the class name and method signatures. There are also “helper” types and functions provided for the few cases in which the C++ behavior cannot be directly mapped into C. These interface and helper functions are compiled in to the main OpenMM library so there is nothing special you have to do to get access to them.

In the include subdirectory of your OpenMM installation directory, there is a machine-generated header file OpenMMCWrapper.h that should be #included in any C program that is to make calls to OpenMM functions. That header contains declarations for all the OpenMM C interface functions and related types. Note that if you follow our suggested structure, you will not need to include this file in your main() compilation unit but can instead use it only in a local file that you write to provide a simple interface to your existing code (see Chapter 10).

12.1.2. Mapping from the C++ API to the C API

The automated generator of the C “wrappers” follows the translation strategy shown in Table 12-1. The idea is that if you see the construct on the left in the C++ API documentation, you should interpret it as the corresponding construct on the right in C. Please look at the supplied example programs to see how this is done in practice.

Construct

C++ API declaration

Equivalent in C API

namespace

OpenMM::

OpenMM_ (prefix)

class

class OpenMM::ClassName

typedef OpenMM_ClassName

constant

OpenMM::RadiansPerDeg

OpenMM_RadiansPerDeg (static constant)

class enum

OpenMM::State::Positions

OpenMM_State_Positions

constructor

new OpenMM::ClassName()

OpenMM_ClassName* OpenMM_ClassName_create()
(additional constructors are _create_2(), etc.)

destructor

OpenMM::ClassName* thing;
delete thing;
OpenMM_ClassName* thing;
OpenMM_ClassName_destroy(thing);

class method

OpenMM::ClassName* thing;
thing->method(args);
OpenMM_ClassName* thing;
OpenMM_ClassName_method(thing, args)

Boolean (type & constants)

bool
true, false
OpenMM_Boolean
OpenMM_True(1), OpenMM_False(0)

string

std::string

char*

3-vector

OpenMM::Vec3

typedef OpenMM_Vec3

arrays

std::vector<std::string>
std::vector<double>
std::vector<Vec3>
std::vector<std::pair<int,int>>
std::map<std::string,double>
typedef OpenMM_StringArray
typedef OpenMM_DoubleArray
typedef OpenMM_Vec3Array
typedef OpenMM_BondArray
typedef OpenMM_ParameterArray

Table 12-1: Default mapping of objects from the C++ API to the C API There are some exceptions to the generic translation rules shown in the table; they are enumerated in the next section. And because there are no C++ API equivalents to the array types, they are described in detail below.

12.1.3. Exceptions

These two methods are handled somewhat differently in the C API than in the C++ API:

  • OpenMM::Context::getState() The C version, OpenMM_Context_getState(), returns a pointer to a heap allocated OpenMM_State object. You must then explicitly destroy this State object when you are done with it, by calling OpenMM_State_destroy().

  • OpenMM::Platform::loadPluginsFromDirectory() The C version OpenMM_Platform_loadPluginsFromDirectory() returns a heap-allocated OpenMM_StringArray object containing a list of all the file names that were successfully loaded. You must then explicitly destroy this StringArray object when you are done with it. Do not ignore the return value; if you do you’ll have a memory leak since the StringArray will still be allocated.

(In the C++ API, the equivalent methods return references into existing memory rather than new heap-allocated memory, so the returned objects do not need to be destroyed.)

12.1.4. OpenMM_Vec3 helper type

Unlike the other OpenMM objects which are opaque and manipulated via pointers, the C API provides an explicit definition for the C OpenMM_Vec3 type that is compatible with the OpenMM::Vec3 type. The definition of OpenMM_Vec3 is:

typedef struct {double x, y, z;} OpenMM_Vec3;

You can work directly with the individual fields of this type from your C program if you want. For convenience, a scale() function is provided that creates a new OpenMM_Vec3 from an old one and a scale factor:

OpenMM_Vec3 OpenMM_Vec3_scale(const OpenMM_Vec3 vec, double scale);

12.1.5. Array helper types

C++ has built-in container types std::vector and std::map which OpenMM uses to manipulate arrays of objects. These don’t have direct equivalents in C, so we supply special array types for each kind of object for which OpenMM creates containers. These are: string, double, Vec3, bond, and parameter map. See Table 12-2 for the names of the C types for each of these object arrays. Each of the array types provides these functions (prefixed by OpenMM_ and the actual Thing name), with the syntax shown conceptually since it differs slightly for each kind of object.

Function

Operation

ThingArray* create(int size)

Create a heap-allocated array of Things, with space pre-allocated to hold size of them. You can start at size==0 if you want since these arrays are dynamically resizeable.

void destroy(ThingArray*)

Free the heap space that is currently in use for the passed-in array of Things.

int getSize(ThingArray*)

Return the current number of Things in this array. This means you can get() and set() elements up to getSize()-1.

void resize(ThingArray*, int size)

Change the size of this array to the indicated value which may be smaller or larger than the current size. Existing elements remain in their same locations as long as they still fit.

void append(ThingArray*, Thing)

Add a Thing to the end of the array, increasing the array size by one. The precise syntax depends on the actual type of Thing; see below.

void set(ThingArray*, int index, Thing)

Store a copy of Thing in the indicated element of the array (indexed from 0). The array must be of length at least index+1; you can’t grow the array with this function.

Thing get(ThingArray*, int index)

Retrieve a particular element from the array (indexed from 0). (For some Things the value is returned in arguments rather than as the function return.)

Table 12-2: Generic description of array helper types

Here are the exact declarations with deviations from the generic description noted, for each of the array types.

OpenMM_DoubleArray

OpenMM_DoubleArray*
            OpenMM_DoubleArray_create(int size);
void        OpenMM_DoubleArray_destroy(OpenMM_DoubleArray*);
int         OpenMM_DoubleArray_getSize(const OpenMM_DoubleArray*);
void        OpenMM_DoubleArray_resize(OpenMM_DoubleArray*, int size);
void        OpenMM_DoubleArray_append(OpenMM_DoubleArray*, double value);
void        OpenMM_DoubleArray_set(OpenMM_DoubleArray*, int index, double value);
double      OpenMM_DoubleArray_get(const OpenMM_DoubleArray*, int index);

OpenMM_StringArray

OpenMM_StringArray*
            OpenMM_StringArray_create(int size);
void        OpenMM_StringArray_destroy(OpenMM_StringArray*);
int         OpenMM_StringArray_getSize(const OpenMM_StringArray*);
void        OpenMM_StringArray_resize(OpenMM_StringArray*, int size);
void        OpenMM_StringArray_append(OpenMM_StringArray*, const char* string);
void        OpenMM_StringArray_set(OpenMM_StringArray*, int index, const char* string);
const char* OpenMM_StringArray_get(const OpenMM_StringArray*, int index);

OpenMM_Vec3Array

OpenMM_Vec3Array*
            OpenMM_Vec3Array_create(int size);
void        OpenMM_Vec3Array_destroy(OpenMM_Vec3Array*);
int         OpenMM_Vec3Array_getSize(const OpenMM_Vec3Array*);
void        OpenMM_Vec3Array_resize(OpenMM_Vec3Array*, int size);
void        OpenMM_Vec3Array_append(OpenMM_Vec3Array*, const OpenMM_Vec3 vec);
void        OpenMM_Vec3Array_set(OpenMM_Vec3Array*, int index, const OpenMM_Vec3 vec);
const OpenMM_Vec3*
            OpenMM_Vec3Array_get(const OpenMM_Vec3Array*, int index);

OpenMM_BondArray

Note that bonds are specified by pairs of integers (the atom indices). The get() method returns those in a pair of final arguments rather than as its functional return.

OpenMM_BondArray*
            OpenMM_BondArray_create(int size);
void        OpenMM_BondArray_destroy(OpenMM_BondArray*);
int         OpenMM_BondArray_getSize(const OpenMM_BondArray*);
void        OpenMM_BondArray_resize(OpenMM_BondArray*, int size);
void        OpenMM_BondArray_append(OpenMM_BondArray*, int particle1, int particle2);
void        OpenMM_BondArray_set(OpenMM_BondArray*, int index, int particle1, int particle2);
void        OpenMM_BondArray_get(const OpenMM_BondArray*, int index,
                                 int* particle1, int* particle2);

OpenMM_ParameterArray

OpenMM returns references to internal ParameterArrays but does not support user-created ParameterArrays, so only the get() and getSize() functions are available. Also, note that since this is actually a map rather than an array, the “index” is the name of the parameter rather than its ordinal.

int         OpenMM_ParameterArray_getSize(const OpenMM_ParameterArray*);
double      OpenMM_ParameterArray_get(const OpenMM_ParameterArray*, const char* name);

12.2. Fortran 95 API

Before you start writing your own Fortran program that calls OpenMM, be sure you can build and run the two Fortran examples that are supplied with OpenMM (see Chapter 10). These can be built from the supplied Makefile on Linux and Mac, or supplied NMakefile and Visual Studio solution files on Windows.

The example programs are HelloArgonInFortran and HelloSodiumChlorideInFortran. The argon example serves as a quick check that your installation is set up properly and you know how to build a Fortran program that is linked with OpenMM. It will also tell you whether OpenMM is executing on the GPU or is running (slowly) on the Reference platform. However, the argon example is not a good template to follow for your own programs. The sodium chloride example, though necessarily simplified, is structured roughly in the way we recommended you set up your own programs to call OpenMM. Please be sure you have both of these programs executing successfully on your machine before continuing.

12.2.1. Mechanics of using the Fortran API

The Fortran API is generated automatically from the C++ API when OpenMM is built. There are two resulting components: Fortran bindings (subroutines to call), and Fortran declarations of types and subroutines (in the form of a Fortran 95 module file). The Fortran bindings are small interface subroutines, one for every method of every OpenMM class, whose signatures (name and arguments) are predictable from the class name and method signatures. There are also “helper” types and subroutines provided for the few cases in which the C++ behavior cannot be directly mapped into Fortran. These interface and helper subroutines are compiled in to the main OpenMM library so there is nothing special you have to do to get access to them.

Because Fortran is case-insensitive, calls to Fortran subroutines (however capitalized) are mapped by the compiler into all-lowercase or all-uppercase names, and different compilers use different conventions. The automatically-generated OpenMM Fortran “wrapper” subroutines, which are generated in C and thus case-sensitive, are provided in two forms for compatibility with the majority of Fortran compilers, including Intel Fortran and gfortran. The two forms are: (1) all-lowercase with a trailing underscore, and (2) all-uppercase without a trailing underscore. So regardless of the Fortran compiler you are using, it should find a suitable subroutine to call in the main OpenMM library.

In the include subdirectory of your OpenMM installation directory, there is a machine-generated module file OpenMMFortranModule.f90 that must be compiled along with any Fortran program that is to make calls to OpenMM functions. (You can look at the Makefile or Visual Studio solution file provided with the OpenMM examples to see how to build a program that uses this module file.) This module file contains definitions for two modules: MODULE OpenMM_Types and MODULE OpenMM; however, only the OpenMM module will appear in user programs (it references the other module internally). The modules contain declarations for all the OpenMM Fortran interface subroutines, related types, and parameters (constants). Note that if you follow our suggested structure, you will not need to use the OpenMM module in your main() compilation unit but can instead use it only in a local file that you write to provide a simple interface to your existing code (see Chapter 10).

12.2.2. Mapping from the C++ API to the Fortran API

The automated generator of the Fortran “wrappers” follows the translation strategy shown in Table 12-3. The idea is that if you see the construct on the left in the C++ API documentation, you should interpret it as the corresponding construct on the right in Fortran. Please look at the supplied example programs to see how this is done in practice. Note that all subroutines and modules are declared with “implicit none”, meaning that the type of every symbol is declared explicitly and should not be inferred from the first letter of the symbol name.

Construct

C++ API declaration

Equivalent in Fortran API

namespace

OpenMM::

OpenMM_ (prefix)

class

class OpenMM::ClassName

type (OpenMM_ClassName)

constant

OpenMM::RadiansPerDeg

parameter (OpenMM_RadiansPerDeg)

class enum

OpenMM::State::Positions

parameter (OpenMM_State_Positions)

constructor

new OpenMM::ClassName()

type (OpenMM_ClassName) thing
call OpenMM_ClassName_create(thing)
(additional constructors are _create_2(), etc.)

destructor

OpenMM::ClassName* thing;
delete thing;
type (OpenMM_ClassName) thing
call OpenMM_ClassName_destroy(thing)

class method

OpenMM::ClassName* thing;
thing->method(args*)
type (OpenMM_ClassName) thing
call OpenMM_ClassName_method(thing, args)

Boolean (type & constants)

bool
true
false
integer*4
parameter (OpenMM_True=1)
parameter (OpenMM_False=0)

string

std::string

character(*)

3-vector

OpenMM::Vec3

real*8 vec(3)

arrays

std::vector<std::string> std::vector<double> std::vector<Vec3> std::vector<std::pair<int,int>> std::map<std::string, double>

type (OpenMM_StringArray)
type (OpenMM_DoubleArray)
type (OpenMM_Vec3Array)
type (OpenMM_BondArray)
type (OpenMM_ParameterArray)

Table 12-3: Default mapping of objects from the C++ API to the Fortran API

Because there are no C++ API equivalents to the array types, they are described in detail below.

12.2.3. OpenMM_Vec3 helper type

Unlike the other OpenMM objects which are opaque and manipulated via pointers, the Fortran API uses an ordinary real*8(3) array in place of the OpenMM::Vec3 type. You can work directly with the individual elements of this type from your Fortran program if you want. For convenience, a scale() function is provided that creates a new Vec3 from an old one and a scale factor:

subroutine OpenMM_Vec3_scale(vec, scale, result)
real*8 vec(3), scale, result(3)

No explicit type(OpenMM_Vec3) is provided in the Fortran API since it is not needed.

12.2.4. Array helper types

C++ has built-in container types std::vector and std::map which OpenMM uses to manipulate arrays of objects. These don’t have direct equivalents in Fortran, so we supply special array types for each kind of object for which OpenMM creates containers. These are: string, double, Vec3, bond, and parameter map. See Table 12-4 for the names of the Fortran types for each of these object arrays. Each of the array types provides these functions (prefixed by OpenMM_ and the actual Thing name), with the syntax shown conceptually since it differs slightly for each kind of object.

Function

Operation

subroutine create(array,size)
type (OpenMM_ThingArray) array
integer*4 size

Create a heap-allocated array of Things, with space pre-allocated to hold size of them. You can start at size==0 if you want since these arrays are dynamically resizeable.

subroutine destroy(array)
type (OpenMM_ThingArray) array

Free the heap space that is currently in use for the passed-in array of Things.

function getSize(array)
type (OpenMM_ThingArray) array
integer*4 size

Return the current number of Things in this array. This means you can get() and set() elements up to getSize().

subroutine resize(array,size)
type (OpenMM_ThingArray) array
integer*4 size

Change the size of this array to the indicated value which may be smaller or larger than the current size. Existing elements remain in their same locations as long as they still fit.

subroutine append(array,elt)
type (OpenMM_ThingArray) array
Thing elt

Add a Thing to the end of the array, increasing the array size by one. The precise syntax depends on the actual type of Thing; see below.

subroutine set(array,index,elt)
type (OpenMM_ThingArray) array
integer*4 size
Thing elt

Store a copy of elt in the indicated element of the array (indexed from 1). The array must be of length at least index; you can’t grow the array with this function.

subroutine get(array,index,elt)
type (OpenMM_ThingArray) array
integer*4 size
Thing elt

Retrieve a particular element from the array (indexed from 1). Some Things require more than one argument to return.

Table 12-4: Generic description of array helper types

Here are the exact declarations with deviations from the generic description noted, for each of the array types.

OpenMM_DoubleArray

subroutine OpenMM_DoubleArray_create(array, size)
    integer*4 size
    type (OpenMM_DoubleArray) array
subroutine OpenMM_DoubleArray_destroy(array)
    type (OpenMM_DoubleArray) array
function OpenMM_DoubleArray_getSize(array)
    type (OpenMM_DoubleArray) array
    integer*4 OpenMM_DoubleArray_getSize
subroutine OpenMM_DoubleArray_resize(array, size)
    type (OpenMM_DoubleArray) array
    integer*4 size
subroutine OpenMM_DoubleArray_append(array, value)
    type (OpenMM_DoubleArray) array
    real*8 value
subroutine OpenMM_DoubleArray_set(array, index, value)
    type (OpenMM_DoubleArray) array
    integer*4 index
    real*8 value
subroutine OpenMM_DoubleArray_get(array, index, value)
    type (OpenMM_DoubleArray) array
    integer*4 index
    real*8 value

OpenMM_StringArray

subroutine OpenMM_StringArray_create(array, size)
    integer*4 size
    type (OpenMM_StringArray) array
subroutine OpenMM_StringArray_destroy(array)
    type (OpenMM_StringArray) array
function OpenMM_StringArray_getSize(array)
    type (OpenMM_StringArray) array
    integer*4 OpenMM_StringArray_getSize
subroutine OpenMM_StringArray_resize(array, size)
    type (OpenMM_StringArray) array
    integer*4 size
subroutine OpenMM_StringArray_append(array, str)
    type (OpenMM_StringArray) array
    character(*) str
subroutine OpenMM_StringArray_set(array, index, str)
    type (OpenMM_StringArray) array
    integer*4 index
    character(*) str
subroutine OpenMM_StringArray_get(array, index, str)
    type (OpenMM_StringArray) array
    integer*4 index
    character(*)str

OpenMM_Vec3Array

subroutine OpenMM_Vec3Array_create(array, size)
    integer*4 size
    type (OpenMM_Vec3Array) array
subroutine OpenMM_Vec3Array_destroy(array)
    type (OpenMM_Vec3Array) array
function OpenMM_Vec3Array_getSize(array)
    type (OpenMM_Vec3Array) array
    integer*4 OpenMM_Vec3Array_getSize
subroutine OpenMM_Vec3Array_resize(array, size)
    type (OpenMM_Vec3Array) array
    integer*4 size
subroutine OpenMM_Vec3Array_append(array, vec)
    type (OpenMM_Vec3Array) array
    real*8 vec(3)
subroutine OpenMM_Vec3Array_set(array, index, vec)
    type (OpenMM_Vec3Array) array
    integer*4 index
    real*8 vec(3)
subroutine OpenMM_Vec3Array_get(array, index, vec)
    type (OpenMM_Vec3Array) array
    integer*4 index
    real*8 vec (3)

OpenMM_BondArray

Note that bonds are specified by pairs of integers (the atom indices). The get() method returns those in a pair of final arguments rather than as its functional return.

subroutine OpenMM_BondArray_create(array, size)
    integer*4 size
    type (OpenMM_BondArray) array
subroutine OpenMM_BondArray_destroy(array)
    type (OpenMM_BondArray) array
function OpenMM_BondArray_getSize(array)
    type (OpenMM_BondArray) array
    integer*4 OpenMM_BondArray_getSize
subroutine OpenMM_BondArray_resize(array, size)
    type (OpenMM_BondArray) array
    integer*4 size
subroutine OpenMM_BondArray_append(array, particle1, particle2)
    type (OpenMM_BondArray) array
    integer*4 particle1, particle2
subroutine OpenMM_BondArray_set(array, index, particle1, particle2)
    type (OpenMM_BondArray) array
    integer*4 index, particle1, particle2
subroutine OpenMM_BondArray_get(array, index, particle1, particle2)
    type (OpenMM_BondArray) array
    integer*4 index, particle1, particle2

OpenMM_ParameterArray

OpenMM returns references to internal ParameterArrays but does not support user-created ParameterArrays, so only the get() and getSize() functions are available. Also, note that since this is actually a map rather than an array, the “index” is the name of the parameter rather than its ordinal.

function OpenMM_ParameterArray_getSize(array)
    type (OpenMM_ParameterArray) array
    integer*4 OpenMM_ParameterArray_getSize
subroutine OpenMM_ParameterArray_get(array, name, param)
    type (OpenMM_ParameterArray) array
    character(*) name
    character(*) param

12.3. Python API

12.3.1. Mapping from the C++ API to the Python API

The Python API follows the C++ API as closely as possible. There are three notable differences:

  1. The getState() method in the Context class takes Pythonic-type arguments to indicate which state variables should be made available. For example:

    myContext.getState(energy=True, force=False, …)
    
  2. Wherever the C++ API uses references to return multiple values from a method, the Python API returns a tuple. For example, in C++ you would query a HarmonicBondForce for a bond’s parameters as follows:

    int particle1, particle2;
    double length, k;
    f.getBondParameters(i, particle1, particle2, length, k);
    

    In Python, the equivalent code is:

    [particle1, particle2, length, k] = f.getBondParameters(i)
    
  3. Unlike C++, the Python API accepts and returns quantities with units attached to most values (see Section 12.3.3 below for details). In short, this means that while values in C++ have implicit units, the Python API returns objects that have values and explicit units.

12.3.2. Mechanics of using the Python API

When using the Python API, be sure to include the GPU support libraries in your library path, just as you would for a C++ application. This is set with the LD_LIBRARY_PATH environment variable on Linux, DYLD_LIBRARY_PATH on Mac, or PATH on Windows. See Chapter 2.2 for details.

The Python API is contained in the openmm package, while the units code is contained in the openmm.units package. (The application layer, described in the Application Guide, is contained in the openmm.app package.) A program using it will therefore typically begin

import openmm as mm
import openmm.unit as unit

Creating and using OpenMM objects is then done exactly as in C++:

system = mm.System()
nb = mm.NonbondedForce()
nb.setNonbondedMethod(mm.NonbondedForce.CutoffNonPeriodic)
nb.setCutoffDistance(1.2*unit.nanometer)
system.addForce(nb)

Note that when setting the cutoff distance, we explicitly specify that it is in nanometers. We could just as easily specify it in different units:

nb.setCutoffDistance(12*unit.angstrom)

The use of units in OpenMM is discussed in the next section.

12.3.3. Units and dimensional analysis

Why does the Python API include units?

The C++ API for OpenMM uses an implicit set of units for physical quantities such as lengths, masses, energies, etc. These units are based on daltons, nanometers, and picoseconds for the mass, length, and time dimensions, respectively. When using the C++ API, it is very important to ensure that quantities being manipulated are always expressed in terms of these units. For example, if you read in a distance in Angstroms, you must multiply that distance by a conversion factor to turn it into nanometers before using it in the C++ API. Such conversions can be a source of tedium and errors. This is true in many areas of scientific programming. Units confusion was blamed for the loss of the Mars Climate Orbiter spacecraft in 1999, at a cost of more than $100 million. Units were introduced in the Python API to minimize the chance of such errors.

The Python API addresses the potential problem of conversion errors by using quantities with explicit units. If a particular distance is expressed in Angstroms, the Python API will know that it is in Angstroms. When the time comes to call the C++ API, it will understand that the quantity must be converted to nanometers. You, the programmer, must declare upfront that the quantity is in Angstrom units, and the API will take care of the details from then on. Using explicit units is a bit like brushing your teeth: it requires some effort upfront, but it probably saves you trouble in the long run.

Quantities, units, and dimensions

The explicit unit system is based on three concepts: Dimensions, Units, and Quantities.

Dimensions are measurable physical concepts such as mass, length, time, and energy. Energy is actually a composite dimension based on mass, length, and time.

A Unit defines a linear scale used to measure amounts of a particular physical Dimension. Examples of units include meters, seconds, joules, inches, and grams.

A Quantity is a specific amount of a physical Dimension. An example of a quantity is “0.63 kilograms”. A Quantity is expressed as a combination of a value (e.g., 0.63), and a Unit (e.g., kilogram). The same Quantity can be expressed in different Units.

The set of BaseDimensions defined in the openmm.unit module includes:

  • mass

  • length

  • time

  • temperature

  • amount

  • charge

  • luminous intensity

These are not precisely the same list of base dimensions used in the SI unit system. SI defines “current” (charge per time) as a base unit, while openmm.unit uses “charge”. And openmm.unit treats angle as a dimension, even though angle quantities are often considered dimensionless. In this case, we choose to err on the side of explicitness, particularly because interconversion of degrees and radians is a frequent source of unit headaches.

Units examples

Many common units are defined in the openmm.unit module.

from openmm.unit import nanometer, angstrom, dalton

Sometimes you don’t want to type the full unit name every time, so you can assign it a shorter name using the as functionality:

from openmm.unit import nanometer as nm

New quantities can be created from a value and a unit. You can use either the multiply operator (‘*’) or the explicit Quantity constructor:

from simk.unit import nanometer, Quantity
# construct a Quantity using the multiply operator
bond_length = 1.53 * nanometer
# equivalently using the explicit Quantity constructor
bond_length = Quantity(1.53, nanometer)
# or more verbosely
bond_length = Quantity(value=1.53, unit=nanometer)

Arithmetic with units

Addition and subtraction of quantities is only permitted between quantities that share the same dimension. It makes no sense to add a mass to a distance. If you attempt to add or subtract two quantities with different dimensions, an exception will be raised. This is a good thing; it helps you avoid errors.

x = 5.0*dalton + 4.3*nanometer; # error

Addition or subtraction of quantities with the same dimension, but different units, is fine, and results in a new quantity created using the correct conversion factor between the units used.

x = 1.3*nanometer + 5.6*angstrom; # OK, result in nanometers

Quantities can be added and subtracted. Naked Units cannot.

Multiplying or dividing two quantities creates a new quantity with a composite dimension. For example, dividing a distance by a time results in a velocity.

from openmm.unit import kilogram, meter, second
a = 9.8 * meter / second**2; # acceleration
m = 0.36 * kilogram; # mass
F = m * a; # force in kg*m/s**2::

Multiplication or division of two Units results in a composite Unit.

mps = meter / second

Unlike amount (moles), angle (radians) is arguably dimensionless.  But openmm.unit treats angle as another dimension.   Use the trigonometric functions from the openmm.unit module (not those from the Python math module!) when dealing with Units and Quantities.

from openmm.unit import sin, cos, acos
x = sin(90.0*degrees)
angle = acos(0.68); # returns an angle quantity (in radians)

The method pow() is a built-in Python method that works with Quantities and Units.

area = pow(3.0*meter, 2)
# or, equivalently
area = (3.0*meter)**2
# or
area = 9.0*(meter**2)

The method sqrt() is not as built-in as pow(). Do not use the Python math.sqrt() method with Units and Quantities. Use the openmm.unit.sqrt() method instead:

from openmm.unit import sqrt
side_length = sqrt(4.0*meter**2)

Atomic scale mass and energy units are “per amount”

Mass and energy units at the atomic scale are specified “per amount” in the openmm.unit module. Amount (mole) is one of the seven fundamental dimensions in the SI unit system.   The atomic scale mass unit, dalton, is defined as grams per mole. The dimension of dalton is therefore mass/amount, instead of simply mass. Similarly, the atomic scale energy unit, kilojoule_per_mole (and kilocalorie_per_mole) has “per amount” in its dimension. Be careful to always use “per amount” mass and energy types at the atomic scale, and your dimensional analysis should work out properly.

The energy unit kilocalories_per_mole does not have the same Dimension as the macroscopic energy unit kilocalories.  Molecular scientists sometimes use the word “kilocalories” when they mean “kilocalories per mole”. Use “kilocalories per mole” or”kilojoules per mole” for molecular energies.  Use “kilocalories” for the metabolic energy content of your lunch. The energy unit kilojoule_per_mole happens to go naturally with the units nanometer, picoseconds, and dalton. This is because 1 kilojoule/mole happens to be equal to 1 gram-nanometer2/mole-picosecond2, and is therefore consistent with the molecular dynamics unit system used in the C++ OpenMM API.

These “per mole” units are what you should be using for molecular calculations, as long as you are using SI / cgs / calorie sorts of units.

SI prefixes

Many units with SI prefixes such as “milligram” (milli) and “kilometer” (kilo) are provided in the openmm.unit module. Others can be created by multiplying a prefix symbol by a non-prefixed unit:

from openmm.unit import mega, kelvin
megakelvin = mega * kelvin
t = 8.3 * megakelvin

Only grams and meters get all of the SI prefixes (from yotto-(10-24) to yotta-(1024)) automatically.

Converting to different units

Use the Quantity.in_units_of() method to create a new Quantity with different units.

from openmm.unit import nanosecond, fortnight
x = (175000*nanosecond).in_units_of(fortnight)

When you want a plain number out of a Quantity, use the value_in_unit() method:

from openmm.unit import femtosecond, picosecond
t = 5.0*femtosecond
t_just_a_number = t.value_in_unit(picoseconds)

Using value_in_unit() puts the responsibility for unit analysis back into your hands, and it should be avoided. It is sometimes necessary, however, when you are called upon to use a non-units-aware Python API.

Lists, tuples, vectors, numpy arrays, and Units

Units can be attached to containers of numbers to create a vector quantity. The openmm.unit module overloads the __setitem__ and __getitem__ methods for these containers to ensure that Quantities go in and out.

>>> a = Vec3(1,2,3) * nanometers
>>> print(a)
(1, 2, 3) nm
>>> print(a.in_units_of(angstroms))
(10.0, 20.0, 30.0) A

>>> s2 = [[1,2,3],[4,5,6]] * centimeter
>>> print(s2)
[[1, 2, 3], [4, 5, 6]] cm
>>> print(s2/millimeter)
[[10.0, 20.0, 30.0], [40.0, 50.0, 60.0]]

>>> import numpy
>>> a = numpy.array([1,2,3]) * centimeter
>>> print(a)
[1 2 3] cm
>>> print(a/millimeter)
[ 10.  20.  30.]

Converting a whole list to different units at once is much faster than converting each element individually. For example, consider the following code that prints out the position of every particle in a State, as measured in Angstroms:

for v in state.getPositions():
    print(v.value_in_unit(angstrom))

This can be rewritten as follows:

for v in state.getPositions().value_in_unit(angstrom):
    print(v)

The two versions produce identical results, but the second one will run faster, and therefore is preferred.