11. 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.
11.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 9).
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.
11.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 9).
11.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 11-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 11-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.
11.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 allocatedOpenMM_State
object. You must then explicitly destroy thisState
object when you are done with it, by callingOpenMM_State_destroy()
.OpenMM::Platform::loadPluginsFromDirectory() The C version
OpenMM_Platform_loadPluginsFromDirectory()
returns a heap-allocatedOpenMM_StringArray
object containing a list of all the file names that were successfully loaded. You must then explicitly destroy thisStringArray
object when you are done with it. Do not ignore the return value; if you do you’ll have a memory leak since theStringArray
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.)
11.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);
11.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 11-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 |
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 |
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 |
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 11-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);
11.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 9). 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.
11.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 9).
11.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 11-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 11-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.
11.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.
11.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 11-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 |
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 |
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 |
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 11-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
11.3. Python API¶
11.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:
The
getState()
method in theContext
class takes Pythonic-type arguments to indicate which state variables should be made available. For example:myContext.getState(getEnergy=True, getForce=False, …)
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)
Unlike C++, the Python API accepts and returns quantities with units attached to most values (see Section 11.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.
11.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.
11.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.