#!/usr/bin/env python
"""
Utility subroutines
"""
__all__ = [
'MessageException',
'IncompatibleUnitError',
'inherit_docstrings',
'all_subclasses',
'temporary_cd',
'temporary_directory',
'get_data_file_path',
'unit_to_string',
'quantity_to_string',
'string_to_unit',
'string_to_quantity',
'object_to_quantity',
'check_units_are_compatible',
'extract_serialized_units_from_dict',
'attach_units',
'detach_units',
'serialize_numpy',
'deserialize_numpy',
'convert_all_quantities_to_string',
'convert_all_strings_to_quantity',
'convert_0_1_smirnoff_to_0_2',
'convert_0_2_smirnoff_to_0_3'
]
#=============================================================================================
# GLOBAL IMPORTS
#=============================================================================================
import contextlib
import functools
from simtk import unit
import logging
#=============================================================================================
# CONFIGURE LOGGER
#=============================================================================================
logger = logging.getLogger(__name__)
#=============================================================================================
# COMMON EXCEPTION TYPES
#=============================================================================================
class MessageException(Exception):
"""A base class for exceptions that print out a string given in their constructor"""
def __init__(self, msg):
super().__init__(self, msg)
self.msg = msg
def __str__(self):
return self.msg
class IncompatibleUnitError(MessageException):
"""
Exception for when a parameter is in the wrong units for a ParameterHandler's unit system
"""
pass
# =============================================================================================
# UTILITY SUBROUTINES
# =============================================================================================
[docs]def inherit_docstrings(cls):
"""Inherit docstrings from parent class"""
from inspect import getmembers, isfunction
for name, func in getmembers(cls, isfunction):
if func.__doc__: continue
for parent in cls.__mro__[1:]:
if hasattr(parent, name):
func.__doc__ = getattr(parent, name).__doc__
return cls
[docs]def all_subclasses(cls):
"""Recursively retrieve all subclasses of the specified class"""
return cls.__subclasses__() + [
g for s in cls.__subclasses__() for g in all_subclasses(s)
]
[docs]@contextlib.contextmanager
def temporary_cd(dir_path):
"""Context to temporary change the working directory.
Parameters
----------
dir_path : str
The directory path to enter within the context
Examples
--------
>>> dir_path = '/tmp'
>>> with temporary_cd(dir_path):
... pass # do something in dir_path
"""
import os
prev_dir = os.getcwd()
os.chdir(os.path.abspath(dir_path))
try:
yield
finally:
os.chdir(prev_dir)
[docs]@contextlib.contextmanager
def temporary_directory():
"""Context for safe creation of temporary directories."""
import tempfile
tmp_dir = tempfile.mkdtemp()
try:
yield tmp_dir
finally:
import shutil
shutil.rmtree(tmp_dir)
[docs]def get_data_file_path(relative_path):
"""Get the full path to one of the reference files in testsystems.
In the source distribution, these files are in ``openforcefield/data/``,
but on installation, they're moved to somewhere in the user's python
site-packages directory.
Parameters
----------
name : str
Name of the file to load (with respect to the repex folder).
"""
from pkg_resources import resource_filename
import os
fn = resource_filename('openforcefield', os.path.join(
'data', relative_path))
if not os.path.exists(fn):
raise ValueError(
"Sorry! %s does not exist. If you just added it, you'll have to re-install"
% fn)
return fn
def unit_to_string(input_unit):
"""
Serialize a simtk.unit.Unit and return it as a string.
Parameters
----------
input_unit : A simtk.unit
The unit to serialize
Returns
-------
unit_string : str
The serialized unit.
"""
# Decompose output_unit into a tuples of (base_dimension_unit, exponent)
unit_string = None
for unit_component in input_unit.iter_base_or_scaled_units():
unit_component_name = unit_component[0].name
# Convert, for example "elementary charge" --> "elementary_charge"
unit_component_name = unit_component_name.replace(' ', '_')
if unit_component[1] == 1:
contribution = '{}'.format(unit_component_name)
else:
contribution = '{}**{}'.format(unit_component_name, int(unit_component[1]))
if unit_string is None:
unit_string = contribution
else:
unit_string += ' * {}'.format(contribution)
return unit_string
def quantity_to_string(input_quantity):
"""
Serialize a simtk.unit.Quantity to a string.
Parameters
----------
input_quantity : simtk.unit.Quantity
The quantity to serialize
Returns
-------
output_string : str
The serialized quantity
"""
import numpy as np
if input_quantity is None:
return None
unitless_value = input_quantity.value_in_unit(input_quantity.unit)
# The string representation of a numpy array doesn't have commas and breaks the
# parser, thus we convert any arrays to list here
if isinstance(unitless_value, np.ndarray):
unitless_value = list(unitless_value)
unit_string = unit_to_string(input_quantity.unit)
output_string = '{} * {}'.format(unitless_value, unit_string)
return output_string
def _ast_eval(node):
"""
Performs an algebraic syntax tree evaluation of a unit.
Parameters
----------
node : An ast parsing tree node
"""
import ast
import operator as op
operators = {ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul,
ast.Div: op.truediv, ast.Pow: op.pow, ast.BitXor: op.xor,
ast.USub: op.neg}
if isinstance(node, ast.Num): # <number>
return node.n
elif isinstance(node, ast.BinOp): # <left> <operator> <right>
return operators[type(node.op)](_ast_eval(node.left), _ast_eval(node.right))
elif isinstance(node, ast.UnaryOp): # <operator> <operand> e.g., -1
return operators[type(node.op)](_ast_eval(node.operand))
elif isinstance(node, ast.Name):
# see if this is a simtk unit
b = getattr(unit, node.id)
return b
# TODO: This was a quick hack that surprisingly worked. We should validate this further.
elif isinstance(node, ast.List):
return ast.literal_eval(node)
else:
raise TypeError(node)
def string_to_unit(unit_string):
"""
Deserializes a simtk.unit.Quantity from a string representation, for
example: "kilocalories_per_mole / angstrom ** 2"
Parameters
----------
unit_string : dict
Serialized representation of a simtk.unit.Quantity.
Returns
-------
output_unit: simtk.unit.Quantity
The deserialized unit from the string
"""
import ast
output_unit = _ast_eval(ast.parse(unit_string, mode='eval').body)
return output_unit
#if (serialized['unitless_value'] is None) and (serialized['unit'] is None):
# return None
#quantity_unit = None
#for unit_name, power in serialized['unit']:
# unit_name = unit_name.replace(
# ' ', '_') # Convert eg. 'elementary charge' to 'elementary_charge'
# if quantity_unit is None:
# quantity_unit = (getattr(unit, unit_name)**power)
# else:
# quantity_unit *= (getattr(unit, unit_name)**power)
#quantity = unit.Quantity(serialized['unitless_value'], quantity_unit)
#return quantity
def string_to_quantity(quantity_string):
"""
Takes a string representation of a quantity and returns a simtk.unit.Quantity
Parameters
----------
quantity_string : str
The quantity to deserialize
Returns
-------
output_quantity : simtk.unit.Quantity
The deserialized quantity
"""
if quantity_string is None:
return None
# This can be the exact same as string_to_unit
import ast
output_quantity = _ast_eval(ast.parse(quantity_string, mode='eval').body)
return output_quantity
def convert_all_strings_to_quantity(smirnoff_data):
"""
Traverses a SMIRNOFF data structure, attempting to convert all
quantity-defining strings into simtk.unit.Quantity objects.
Integers and floats are ignored and not converted into a dimensionless
``simtk.unit.Quantity`` object.
Parameters
----------
smirnoff_data : dict
A hierarchical dict structured in compliance with the SMIRNOFF spec
Returns
-------
converted_smirnoff_data : dict
A hierarchical dict structured in compliance with the SMIRNOFF spec,
with quantity-defining strings converted to simtk.unit.Quantity objects
"""
if isinstance(smirnoff_data, dict):
for key, value in smirnoff_data.items():
smirnoff_data[key] = convert_all_strings_to_quantity(value)
obj_to_return = smirnoff_data
elif isinstance(smirnoff_data, list):
for index, item in enumerate(smirnoff_data):
smirnoff_data[index] = convert_all_strings_to_quantity(item)
obj_to_return = smirnoff_data
elif isinstance(smirnoff_data, int) or isinstance(smirnoff_data, float):
obj_to_return = smirnoff_data
else:
try:
obj_to_return = object_to_quantity(smirnoff_data)
except (AttributeError, TypeError, SyntaxError):
obj_to_return = smirnoff_data
return obj_to_return
def convert_all_quantities_to_string(smirnoff_data):
"""
Traverses a SMIRNOFF data structure, attempting to convert all
quantities into strings.
Parameters
----------
smirnoff_data : dict
A hierarchical dict structured in compliance with the SMIRNOFF spec
Returns
-------
converted_smirnoff_data : dict
A hierarchical dict structured in compliance with the SMIRNOFF spec,
with simtk.unit.Quantitys converted to string
"""
if isinstance(smirnoff_data, dict):
for key, value in smirnoff_data.items():
smirnoff_data[key] = convert_all_quantities_to_string(value)
obj_to_return = smirnoff_data
elif isinstance(smirnoff_data, list):
for index, item in enumerate(smirnoff_data):
smirnoff_data[index] = convert_all_quantities_to_string(item)
obj_to_return = smirnoff_data
elif isinstance(smirnoff_data, unit.Quantity):
obj_to_return = quantity_to_string(smirnoff_data)
else:
obj_to_return = smirnoff_data
return obj_to_return
@functools.singledispatch
def object_to_quantity(object):
"""
Attempts to turn the provided object into simtk.unit.Quantity(s).
Can handle float, int, strings, quantities, or iterators over
the same. Raises an exception if unable to convert all inputs.
Parameters
----------
object : int, float, string, quantity, or iterator of strings of quantities
The object to convert to a ``simtk.unit.Quantity`` object.
Returns
-------
converted_object : simtk.unit.Quantity or List[simtk.unit.Quantity]
"""
# If we can't find a custom type, we treat this as a generic iterator.
return [object_to_quantity(sub_obj) for sub_obj in object]
@object_to_quantity.register(unit.Quantity)
def _(obj):
return obj
@object_to_quantity.register(str)
def _(obj):
return string_to_quantity(obj)
@object_to_quantity.register(int)
@object_to_quantity.register(float)
def _(obj):
return unit.Quantity(obj)
def check_units_are_compatible(object_name, object, unit_to_check, context=None):
"""
Checks whether a simtk.unit.Quantity or list of simtk.unit.Quantitys is compatible with given unit.
Parameters
----------
object_name : string
Name of object, used in printing exception.
object : A simtk.unit.Quantity or list of simtk.unit.Quantitys
unit_to_check : A simtk.unit.Unit
context : string, optional. Default=None
Additional information to provide at the beginning of the exception message if raised
Raises
------
IncompatibleUnitError
"""
from simtk import unit
if isinstance(object, list):
for sub_object in object:
check_units_are_compatible(object_name, sub_object, unit_to_check, context=context)
elif isinstance(object, unit.Quantity):
if not object.unit.is_compatible(unit_to_check):
msg = f"{context} {object_name} with " \
f"value {object}, is incompatible with expected unit {unit_to_check}"
raise IncompatibleUnitError(msg)
else:
msg = f"{context} {object_name} with " \
f"value {object}, is incompatible with expected unit {unit_to_check}"
raise IncompatibleUnitError(msg)
def extract_serialized_units_from_dict(input_dict):
"""
Create a mapping of (potentially unit-bearing) quantities from a dictionary, where some keys exist in pairs like
{'length': 8, 'length_unit':'angstrom'}.
Parameters
----------
input_dict : dict
Dictionary where some keys are paired like {'X': 1.0, 'X_unit': angstrom}.
Returns
-------
unitless_dict : dict
input_dict, but with keys ending in ``_unit`` removed.
attached_units : dict str : simtk.unit.Unit
``attached_units[parameter_name]`` is the simtk.unit.Unit combination that should be attached to corresponding
parameter ``parameter_name``. For example ``attached_units['X'] = simtk.unit.angstrom.
"""
# TODO: Should this scheme also convert "1" to int(1) and "8.0" to float(8.0)?
from collections import OrderedDict
attached_units = OrderedDict()
unitless_dict = input_dict.copy()
keys_to_delete = []
for key in input_dict.keys():
if key.endswith('_unit'):
parameter_name = key[:-5]
parameter_units_string = input_dict[key]
try:
parameter_units = string_to_unit(parameter_units_string)
except Exception as e:
e.msg = "Could not parse units {}\n".format(
parameter_units_string) + e.msg
raise e
attached_units[parameter_name] = parameter_units
# Remember this key and delete it later (we break the dict if we delete a key in the loop)
keys_to_delete.append(key)
# Clean out the '*_unit' keys that we processed
for key in keys_to_delete:
del unitless_dict[key]
return unitless_dict, attached_units
def attach_units(unitless_dict, attached_units):
"""
Attach units to dict entries for which units are specified.
Parameters
----------
unitless_dict : dict
Dictionary, where some items are to have units applied.
attached_units : dict [str : simtk.unit.Unit]
``attached_units[parameter_name]`` is the simtk.unit.Unit combination that should be attached to corresponding
parameter ``parameter_name``
Returns
-------
unit_bearing_dict : dict
Updated dict with simtk.unit.Unit units attached to values for which units were specified for their keys
"""
temp_dict = unitless_dict.copy()
for parameter_name, units_to_attach in attached_units.items():
if parameter_name in temp_dict.keys():
parameter_attrib_string = temp_dict[parameter_name]
try:
temp_dict[parameter_name] = float(parameter_attrib_string) * units_to_attach
except ValueError as e:
e.msg = (
"Expected numeric value for parameter '{}',"
"instead found '{}' when trying to attach units '{}'\n"
).format(parameter_name, parameter_attrib_string, units_to_attach)
raise e
# Now check for matches like "phase1", "phase2"
c = 1
while (parameter_name + str(c)) in temp_dict.keys():
indexed_parameter_name = parameter_name + str(c)
parameter_attrib_string = temp_dict[indexed_parameter_name]
try:
temp_dict[indexed_parameter_name] = float(
parameter_attrib_string) * units_to_attach
except ValueError as e:
e.msg = "Expected numeric value for parameter '{}', instead found '{}' when trying to attach units '{}'\n".format(
indexed_parameter_name, parameter_attrib_string,
units_to_attach)
raise e
c += 1
return temp_dict
def detach_units(unit_bearing_dict, output_units=None):
"""
Given a dict which may contain some simtk.unit.Quantity objects, return the same dict with the Quantities
replaced with unitless values, and a new dict containing entries with the suffix "_unit" added, containing
the units.
Parameters
----------
unit_bearing_dict : dict
A dictionary potentially containing simtk.unit.Quantity objects as values.
output_units : dict[str : simtk.unit.Unit], optional. Default = None
A mapping from parameter fields to the output unit its value should be converted to.
For example, {'length_unit': unit.angstrom}. If no output_unit is defined for a key:value pair in which
the value is a simtk.unit.Quantity, the output unit will be the Quantity's unit, and this information
will be included in the unit_dict return value.
Returns
-------
unitless_dict : dict
The input smirnoff_dict object, with all simtk.unit.Quantity values converted to unitless values.
unit_dict : dict
A dictionary in which keys are keys of simtk.unit.Quantity values in unit_bearing_dict,
but suffixed with "_unit". Values are simtk.unit.Unit .
"""
from simtk import unit
if output_units is None:
output_units = {}
# initialize dictionaries for outputs
unit_dict = {}
unitless_dict = unit_bearing_dict.copy()
for key, value in unit_bearing_dict.items():
# If no conversion is needed, skip this item
if not isinstance(value, unit.Quantity):
continue
# If conversion is needed, see if the user has requested an output unit
unit_key = key + '_unit'
if unit_key in output_units:
output_unit = output_units[unit_key]
else:
output_unit = value.unit
if not (output_unit.is_compatible(value.unit)):
raise ValueError("Requested output unit {} is not compatible with "
"quantity unit {} .".format(output_unit, value.unit))
unitless_dict[key] = value.value_in_unit(output_unit)
unit_dict[unit_key] = output_unit
return unitless_dict, unit_dict
def serialize_numpy(np_array):
"""
Serializes a numpy array into a JSON-compatible string. Leverages the numpy.save function,
thereby preserving the shape of the input array
from https://stackoverflow.com/questions/30698004/how-can-i-serialize-a-numpy-array-while-preserving-matrix-dimensions#30699208
Parameters
----------
np_array : A numpy array
Input numpy array
Returns
-------
serialized : str
A serialized representation of the numpy array.
shape : tuple of ints
The shape of the serialized array
"""
bigendian_array = np_array.newbyteorder('>')
serialized = bigendian_array.tobytes()
shape = np_array.shape
return serialized, shape
def deserialize_numpy(serialized_np, shape):
"""
Deserializes a numpy array from a JSON-compatible string.
from https://stackoverflow.com/questions/30698004/how-can-i-serialize-a-numpy-array-while-preserving-matrix-dimensions#30699208
Parameters
----------
serialized_np : str
A serialized numpy array
shape : tuple of ints
The shape of the serialized array
Returns
-------
np_array : numpy.ndarray
The deserialized numpy array
"""
import numpy as np
dt = np.dtype('float')
dt.newbyteorder('>') # set to big-endian
np_array = np.frombuffer(serialized_np, dtype=dt)
np_array = np_array.reshape(shape)
return np_array
[docs]def convert_0_2_smirnoff_to_0_3(smirnoff_data_0_2):
"""
Convert an 0.2-compliant SMIRNOFF dict to an 0.3-compliant one.
This involves removing units from header tags and adding them
to attributes of child elements.
It also requires converting ProperTorsions and ImproperTorsions
potentials from "charmm" to "fourier".
Parameters
----------
smirnoff_data_0_2 : dict
Hierarchical dict representing a SMIRNOFF data structure according the the 0.2 spec
Returns
-------
smirnoff_data_0_3
Hierarchical dict representing a SMIRNOFF data structure according the the 0.3 spec
"""
# Legacy forcefields sometimes specify the NonbondedForce's sigma_unit value, but then provide
# atom size as rmin_half. Here we correct for this behavior by explicitly defining both as
# the same unit if either one is defined.
if 'vdW' in smirnoff_data_0_2['SMIRNOFF'].keys():
rmh_unit = smirnoff_data_0_2['SMIRNOFF']['vdW'].get('rmin_half_unit', None)
sig_unit = smirnoff_data_0_2['SMIRNOFF']['vdW'].get('sigma_unit', None)
if (rmh_unit is not None) and (sig_unit is None):
smirnoff_data_0_2['SMIRNOFF']['vdW']['sigma_unit'] = rmh_unit
elif (sig_unit is not None) and (rmh_unit is None):
smirnoff_data_0_2['SMIRNOFF']['vdW']['rmin_half_unit'] = sig_unit
# If both are None, or both are defined, don't overwrite anything
else:
pass
# Recursively attach unit strings
smirnoff_data = recursive_attach_unit_strings(smirnoff_data_0_2, {})
# Change TorsionHandler potential from "charmm" to "k*(1+cos(periodicity*theta-phase))". Note that, scientifically,
# we should have used "k*(1+cos(periodicity*theta-phase))" all along, since "charmm" technically
# implies that we would support a harmonic potential for torsion terms with periodicity 0
# More at: https://github.com/openforcefield/openforcefield/issues/303#issuecomment-490156779
if 'ProperTorsions' in smirnoff_data['SMIRNOFF']:
if 'potential' in smirnoff_data['SMIRNOFF']['ProperTorsions']:
if smirnoff_data['SMIRNOFF']['ProperTorsions']['potential'] == 'charmm':
smirnoff_data['SMIRNOFF']['ProperTorsions']['potential'] = 'k*(1+cos(periodicity*theta-phase))'
if 'ImproperTorsions' in smirnoff_data['SMIRNOFF']:
if 'potential' in smirnoff_data['SMIRNOFF']['ImproperTorsions']:
if smirnoff_data['SMIRNOFF']['ImproperTorsions']['potential'] == 'charmm':
smirnoff_data['SMIRNOFF']['ImproperTorsions']['potential'] = 'k*(1+cos(periodicity*theta-phase))'
# Add per-section tag
sections_not_to_version_0_3 = ['Author', 'Date', 'version', 'aromaticity_model']
for l1_tag in smirnoff_data['SMIRNOFF'].keys():
if l1_tag not in sections_not_to_version_0_3:
if smirnoff_data['SMIRNOFF'][l1_tag] is None:
# Handle empty entries, such as the ToolkitAM1BCC handler.
smirnoff_data['SMIRNOFF'][l1_tag] = {}
smirnoff_data['SMIRNOFF'][l1_tag]['version'] = 0.3
# Update top-level tag
smirnoff_data['SMIRNOFF']['version'] = 0.3
return smirnoff_data
[docs]def convert_0_1_smirnoff_to_0_2(smirnoff_data_0_1):
"""
Convert an 0.1-compliant SMIRNOFF dict to an 0.2-compliant one.
This involves renaming several tags, adding Electrostatics and ToolkitAM1BCC tags, and
separating improper torsions into their own section.
Parameters
----------
smirnoff_data_0_1 : dict
Hierarchical dict representing a SMIRNOFF data structure according the the 0.1 spec
Returns
-------
smirnoff_data_0_2
Hierarchical dict representing a SMIRNOFF data structure according the the 0.2 spec
"""
smirnoff_data = smirnoff_data_0_1.copy()
l0_replacement_dict = {'SMIRFF': 'SMIRNOFF'}
l1_replacement_dict = {'HarmonicBondForce': 'Bonds',
'HarmonicAngleForce': 'Angles',
'PeriodicTorsionForce': 'ProperTorsions',
'NonbondedForce': 'vdW'
}
for old_l0_tag, new_l0_tag in l0_replacement_dict.items():
# Convert first-level smirnoff_data tags.
# Right now this just changes the SMIRFF tag to SMIRNOFF
if old_l0_tag in smirnoff_data.keys():
smirnoff_data[new_l0_tag] = smirnoff_data[old_l0_tag]
del smirnoff_data[old_l0_tag]
# SMIRFF tag will have been converted to SMIRNOFF here
# Convert second-level tags here
for old_l1_tag, new_l1_tag in l1_replacement_dict.items():
if old_l1_tag in smirnoff_data['SMIRNOFF'].keys():
smirnoff_data['SMIRNOFF'][new_l1_tag] = smirnoff_data['SMIRNOFF'][old_l1_tag]
del smirnoff_data['SMIRNOFF'][old_l1_tag]
# Add 'potential' field to each l1 tag
default_potential = {'Bonds': 'harmonic',
'Angles': 'harmonic',
'ProperTorsions': 'charmm',
# Note that "charmm" isn't actually correct, and was later changed
# in the 0.3 spec. More info at
# https://github.com/openforcefield/openforcefield/pull/311#commitcomment-33494506
'vdW': 'Lennard-Jones-12-6'
}
for l1_tag in smirnoff_data['SMIRNOFF'].keys():
if l1_tag in default_potential.keys():
# Ensure that it isn't there already (shouldn't happen, but better to be safe)
if 'potential' in smirnoff_data['SMIRNOFF'][l1_tag].keys():
assert smirnoff_data[l1_tag].keys == default_potential[l1_tag]
continue
# Issue an informative warning about assumptions made during conversion.
logger.warning(f"0.1 SMIRNOFF spec file does not contain 'potential' attribute for '{l1_tag}' tag. "
f"The SMIRNOFF spec converter is assuming it has a value of '{default_potential[l1_tag]}'")
smirnoff_data['SMIRNOFF'][l1_tag]['potential'] = default_potential[l1_tag]
# Separate improper torsions from propers
if 'ProperTorsions' in smirnoff_data['SMIRNOFF']:
if 'Improper' in smirnoff_data['SMIRNOFF']['ProperTorsions']:
# First generate an ImproperTorsions header, taking the relevant values from the ProperTorsions header
improper_section = {'k_unit': smirnoff_data['SMIRNOFF']['ProperTorsions']['k_unit'],
'phase_unit': smirnoff_data['SMIRNOFF']['ProperTorsions']['phase_unit'],
'potential': smirnoff_data['SMIRNOFF']['ProperTorsions']['potential'],
'Improper': smirnoff_data['SMIRNOFF']['ProperTorsions']['Improper']
}
# Then, attach the newly-made ImproperTorsions section
smirnoff_data['SMIRNOFF']['ImproperTorsions'] = improper_section
del smirnoff_data['SMIRNOFF']['ProperTorsions']['Improper']
# Add Electrostatics tag, setting several values to their defaults and
# warning about assumptions that are being made
electrostatics_section = {'method': 'PME',
'scale12': 0.0,
'scale13': 0.0,
'scale15': 1.0,
'cutoff': 9.0,
'cutoff_unit': 'angstrom'
}
logger.warning("0.1 SMIRNOFF spec did not allow the 'Electrostatics' tag. Adding it in 0.2 spec conversion, and "
"assuming the following values:")
for key, val in electrostatics_section.items():
logger.warning(f"\t{key}: {val}")
# Take electrostatics 1-4 scaling term from 0.1 spec's NonBondedForce tag
electrostatics_section['scale14'] = smirnoff_data['SMIRNOFF']['vdW']['coulomb14scale']
del smirnoff_data['SMIRNOFF']['vdW']['coulomb14scale']
smirnoff_data['SMIRNOFF']['Electrostatics'] = electrostatics_section
# Change vdW's lj14scale to 14scale, add other scaling terms
vdw_section_additions = {'method': "cutoff",
'combining_rules': "Lorentz-Berthelot",
'scale12': "0.0",
'scale13': "0.0",
'scale15': "1",
'switch_width': "1.0",
'switch_width_unit': "angstrom",
'cutoff': "9.0",
'cutoff_unit': "angstrom"
}
for key, val in vdw_section_additions.items():
if not key in smirnoff_data['SMIRNOFF']['vdW'].keys():
logger.warning(f"0.1 SMIRNOFF spec file does not contain '{key}' attribute for 'NonBondedMethod/vdW'' tag. "
f"The SMIRNOFF spec converter is assuming it has a value of '{val}'")
smirnoff_data['SMIRNOFF']['vdW'][key] = val
# Rename L-J 1-4 scaling term from 0.1 spec's NonBondedForce tag to vdW's scale14
smirnoff_data['SMIRNOFF']['vdW']['scale14'] = smirnoff_data['SMIRNOFF']['vdW']['lj14scale']
del smirnoff_data['SMIRNOFF']['vdW']['lj14scale']
# Add <ToolkitAM1BCC/> tag
smirnoff_data['SMIRNOFF']['ToolkitAM1BCC'] = {}
# Update top-level tag
smirnoff_data['SMIRNOFF']['version'] = 0.2
return smirnoff_data
def recursive_attach_unit_strings(smirnoff_data, units_to_attach):
"""
Recursively traverse a SMIRNOFF data structure, appending "* {unit}" to values in key:value pairs
where "key_unit":"unit_string" is present at a higher level in the hierarchy.
This function expects all items in smirnoff_data to be formatted as strings.
Parameters
----------
smirnoff_data : dict
Any level of hierarchy that is part of a SMIRNOFF dict, with all data members
formatted as string.
units_to_attach : dict
Dict of the form {key:unit_string}
Returns
-------
unit_appended_smirnoff_data: dict
"""
import re
# Make a copy of units_to_attach so we don't modify the original (otherwise things like k_unit could
# leak between sections)
units_to_attach = units_to_attach.copy()
#smirnoff_data = smirnoff_data.copy()
# If we're working with a dict, see if there are any new unit entries and store them,
# then operate recursively on the values in the dict.
if isinstance(smirnoff_data, dict):
# Go over all key:value pairs once to see if there are new units to attach.
# Note that units to be attached can be defined in the same dict as the
# key:value pair they will be attached to, so we need to complete this check
# before we are able to check other items in the dict.
for key, value in list(smirnoff_data.items()):
if key[-5:] == '_unit':
units_to_attach[key[:-5]] = value
del smirnoff_data[key]
# Go through once more to attach units as appropriate
for key in smirnoff_data.keys():
# We use regular expressions to catch possible indexed attributes
attach_unit = None
for unit_key, unit_string in units_to_attach.items():
if re.match(f'{unit_key}[0-9]*', key):
attach_unit = unit_string
if attach_unit is not None:
smirnoff_data[key] = str(smirnoff_data[key]) + " * " + attach_unit
# And recursively act on value, in case it's a deeper level of hierarchy
smirnoff_data[key] = recursive_attach_unit_strings(smirnoff_data[key], units_to_attach)
# If it's a list, operate on each member of the list
elif isinstance(smirnoff_data, list):
for index, value in enumerate(smirnoff_data):
smirnoff_data[index] = recursive_attach_unit_strings(value, units_to_attach)
# Otherwise, just return smirnoff_data unchanged
else:
pass
return smirnoff_data