Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Abstract potential part of AtomType class into a new class #96

Merged
merged 15 commits into from
Mar 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
227 changes: 49 additions & 178 deletions topology/core/atom_type.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,61 @@
import warnings
import numpy as np
import sympy
import unyt as u
from topology.testing.utils import allclose


class AtomType(object):
"""An atom type."""
from topology.testing.utils import allclose
from topology.core.potential import Potential


class AtomType(Potential):
"""An atom type, inheriting from the Potential class.

AtomType represents an atom type and includes the functional form
describing its interactions and, optionally, other properties such as mass
and charge. This class inhereits from Potential, which stores the
non-bonded interaction between atoms or sites. The functional form of the
potential is stored as a `sympy` expression and the parameters, with units,
are stored explicitly.

Parameters
----------
name : str, default="AtomType"
The name of the potential.
mass : unyt.unyt_quantity, optional, default=0.0 * unyt.g / u.mol
The mass of the atom type.
charge : unyt.unyt_quantity, optional, default=0.0 * unyt.elementary_charge
The charge of the atom type.
expression : str or sympy.Expr,
default='4*epsilon*((sigma/r)**12 - (sigma/r)**6)',
The mathematical expression describing the functional form of the
justinGilmer marked this conversation as resolved.
Show resolved Hide resolved
potential describing this atom type, i.e. a Lennard-Jones potential.
The default value is a 12-6 Lennard-Jones potential.
parameters : dict of str : unyt.unyt_quantity pairs,
justinGilmer marked this conversation as resolved.
Show resolved Hide resolved
default={'sigma': 0.3 * u.nm, 'epsilon': 0.3 * u.Unit('kJ')},
The parameters of the potential describing this atom type and their
values, as unyt quantities.
independent_variables : str, sympy.Symbol, or list-like of str, sympy.Symbol
The independent variables of the functional form previously described.

"""

def __init__(self,
name="AtomType",
name='AtomType',
mass=0.0 * u.gram / u.mol,
charge=0.0 * u.elementary_charge,
nb_function='4*epsilon*((sigma/r)**12 - (sigma/r)**6)',
expression='4*epsilon*((sigma/r)**12 - (sigma/r)**6)',
parameters={
'sigma': 0.3*u.nm,
'epsilon': 0.3*u.Unit('kJ')},
independent_variables={'r'},
):

self._name = name
'sigma': 0.3 * u.nm,
'epsilon': 0.3 * u.Unit('kJ')},
independent_variables={'r'}):
justinGilmer marked this conversation as resolved.
Show resolved Hide resolved

super(AtomType, self).__init__(
name=name,
expression=expression,
parameters=parameters,
independent_variables=independent_variables)
self._mass = _validate_mass(mass)
self._charge = _validate_charge(charge)
self._parameters = _validate_parameters(parameters)
self._independent_variables = _validate_independent_variables(independent_variables)
self._nb_function = _validate_nb_function(nb_function)

self._validate_function_parameters()

@property
def name(self):
return self._name

@name.setter
def name(self, val):
self._name = val
self._validate_expression_parameters()

@property
def charge(self):
Expand All @@ -52,109 +73,6 @@ def mass(self):
def mass(self, val):
self._mass = _validate_mass(val)

@property
def parameters(self):
return self._parameters

@parameters.setter
def parameters(self, newparams):
newparams = _validate_parameters(newparams)

self._parameters.update(newparams)
self._validate_function_parameters()

@property
def independent_variables(self):
return self._independent_variables

@independent_variables.setter
def independent_variables(self, indep_vars):
self._independent_variables = _validate_independent_variables(indep_vars)

@property
def nb_function(self):
return self._nb_function

@nb_function.setter
def nb_function(self, function):
# Check valid function type (string or sympy expression)
# If func is undefined, just keep the old one
if isinstance(function, str):
self._nb_function = sympy.sympify(function)
elif isinstance(function, sympy.Expr):
self._nb_function = function
else:
raise ValueError("Please enter a string or sympy expression")

self._validate_function_parameters()

def set_nb_function(self, function=None, parameters=None, independent_variables=None):
""" Set the nonbonded function and paramters for this atomtype

Parameters
----------
function: sympy.Expression or string
The mathematical expression corresponding to the nonbonded potential
If None, the function remains unchanged
parameters: dict
{parameter: value} in the function
If None, the parameters remain unchanged

Notes
-----
Be aware of the symbols used in the `function` and `parameters`.
If unnecessary parameters are supplied, an error is thrown.
If only a subset of the parameters are supplied, they are updated
while the non-passed parameters default to the existing values
"""
if function is not None:
self._nb_function = _validate_nb_function(function)

if parameters is None:
parameters = self._parameters
else:
parameters = _validate_parameters(parameters)
if not set(self._parameters).intersection(set(parameters)):
if function is None:
raise ValueError('`parameters` argument includes no '
'variables found in function. Expected '
'at least one of {}'.format(
self._parameters.keys()))
self._parameters.update(parameters)

if independent_variables is not None:
self._independent_variables = _validate_independent_variables(independent_variables)

if not set(parameters.keys()).isdisjoint(self._nb_function.free_symbols):
raise ValueError('Mismatch between parameters and nbfunction symbols')


self._validate_function_parameters()

def _validate_function_parameters(self):
# Check for unused symbols
parameter_symbols = sympy.symbols(set(self._parameters.keys()))
independent_variable_symbols = self._independent_variables
used_symbols = parameter_symbols.union(independent_variable_symbols)
unused_symbols = self.nb_function.free_symbols - used_symbols
if len(unused_symbols) > 0:
warnings.warn('You supplied parameters with '
'unused symbols {}'.format(unused_symbols))

if used_symbols != self.nb_function.free_symbols:
symbols = sympy.symbols(set(self.parameters.keys()))
if symbols != self.nb_function.free_symbols:
missing_syms = self.nb_function.free_symbols - symbols - self._independent_variables
if missing_syms:
raise ValueError("Missing necessary parameters to evaluate "
"NB function. Missing symbols: {}"
"".format(missing_syms))
extra_syms = symbols ^ self.nb_function.free_symbols
warnings.warn("NB function and parameter"
" symbols do not agree,"
" extraneous symbols:"
" {}".format(extra_syms))

def __eq__(self, other):
name_match = (self.name == other.name)
charge_match = allclose(
Expand All @@ -168,11 +86,11 @@ def __eq__(self, other):
atol=1e-6 * u.gram / u.mol,
rtol=1e-5 * u.gram / u.mol)
parameter_match = (self.parameters == other.parameters)
nb_function_match = (self.nb_function == other.nb_function)
expression_match = (self.expression == other.expression)

return all([
name_match, charge_match, mass_match, parameter_match,
nb_function_match
expression_match
])

def __repr__(self):
Expand Down Expand Up @@ -204,50 +122,3 @@ def _validate_mass(mass):
pass

return mass


def _validate_parameters(parameters):
if not isinstance(parameters, dict):
raise ValueError("Please enter dictionary for parameters")
for key, val in parameters.items():
if not isinstance(val, u.unyt_array):
raise ValueError('Paramter value {} lacks a unyt'.format(val))
if not isinstance(key, str):
raise ValueError('Parameter key {} is not a str'.format(key))

return parameters

def _validate_independent_variables(indep_vars):
if isinstance(indep_vars, str):
indep_vars = {sympy.symbols(indep_vars)}
elif isinstance(indep_vars, sympy.symbol.Symbol):
indep_vars = {indep_vars}
elif isinstance(indep_vars, (list, set)):
if all([isinstance(val, sympy.symbol.Symbol) for val in indep_vars]):
pass
elif all([isinstance(val, str) for val in indep_vars]):
indep_vars = set([sympy.symbols(val) for val in indep_vars])
else:
raise ValueError('`independent_variables` argument was a list '
'or set of mixed variables. Please enter a '
'list or set of either only strings or only '
'sympy symbols')
else:
raise ValueError("Please enter a string, sympy expression, "
"or list or set thereof for independent_variables")

return indep_vars


def _validate_nb_function(nb_function):
if nb_function is None:
pass
elif isinstance(nb_function, str):
nb_function = sympy.sympify(nb_function)
elif isinstance(nb_function, sympy.Expr):
nb_function = nb_function
else:
raise ValueError("Please enter a string, sympy expression, "
"or None for nb_function")

return nb_function
Loading