diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6970d24
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+build/
+ThermoLIB.egg-info/
+thermolib/ext.c
diff --git a/docs/_modules/thermolib/thermodynamics/condprob.html b/docs/_modules/thermolib/thermodynamics/condprob.html
index 7047c02..225684c 100644
--- a/docs/_modules/thermolib/thermodynamics/condprob.html
+++ b/docs/_modules/thermolib/thermodynamics/condprob.html
@@ -1213,8 +1213,8 @@
# Construct 1D FEP
def transform(fs, pconds):
mask = ~np.isnan(fs)
-
ps = np.trapz(pconds[:,mask]*np.exp(-fep.beta*fs[mask]), x=cvs[mask])
-
ps /= np.trapz(ps, x=qs)
+
ps = np.trapezoid(pconds[:,mask]*np.exp(-fep.beta*fs[mask]), x=cvs[mask])
+
ps /= np.trapezoid(ps, x=qs)
fs_new = np.zeros([len(qs)], float)*np.nan
fs_new[ps>0] = -np.log(ps[ps>0])/fep.beta
return fs_new
@@ -1435,7 +1435,7 @@
Source code for thermolib.thermodynamics.condprob
#construct 2D FES
def transform(fs, pconds):
mask = ~np.isnan(fs)
- ps = np.trapz(pconds[...,mask]*np.exp(-fep.beta*fs[mask]), x=fep.cvs[mask])
+ ps = np.trapezoid(pconds[...,mask]*np.exp(-fep.beta*fs[mask]), x=fep.cvs[mask])
ps /= integrate2d(ps, x=self.qs[0], y=self.qs[1])
fs_new = np.zeros(ps.shape, float)*np.nan
fs_new[ps>0] = -np.log(ps[ps>0])/fep.beta
diff --git a/thermolib/thermodynamics/condprob.py b/thermolib/thermodynamics/condprob.py
index e50a493..4412c6f 100644
--- a/thermolib/thermodynamics/condprob.py
+++ b/thermolib/thermodynamics/condprob.py
@@ -784,8 +784,8 @@ def transform(self, fep, f_output_unit=None, f_label=None, f_output_class=BaseFr
# Construct 1D FEP
def transform(fs, pconds):
mask = ~np.isnan(fs)
- ps = np.trapz(pconds[:,mask]*np.exp(-fep.beta*fs[mask]), x=cvs[mask])
- ps /= np.trapz(ps, x=qs)
+ ps = np.trapezoid(pconds[:,mask]*np.exp(-fep.beta*fs[mask]), x=cvs[mask])
+ ps /= np.trapezoid(ps, x=qs)
fs_new = np.zeros([len(qs)], float)*np.nan
fs_new[ps>0] = -np.log(ps[ps>0])/fep.beta
return fs_new
@@ -991,7 +991,7 @@ def deproject(self, fep, f_output_unit=None, f_label=None, f_output_class=FreeEn
#construct 2D FES
def transform(fs, pconds):
mask = ~np.isnan(fs)
- ps = np.trapz(pconds[...,mask]*np.exp(-fep.beta*fs[mask]), x=fep.cvs[mask])
+ ps = np.trapezoid(pconds[...,mask]*np.exp(-fep.beta*fs[mask]), x=fep.cvs[mask])
ps /= integrate2d(ps, x=self.qs[0], y=self.qs[1])
fs_new = np.zeros(ps.shape, float)*np.nan
fs_new[ps>0] = -np.log(ps[ps>0])/fep.beta
diff --git a/thermolib/thermodynamics/cv.py b/thermolib/thermodynamics/cv.py
index 586490b..43a60f9 100644
--- a/thermolib/thermodynamics/cv.py
+++ b/thermolib/thermodynamics/cv.py
@@ -19,7 +19,7 @@
__all__ = [
'CenterOfMass', 'CenterOfPosition', 'NormalizedAxis', 'NormalToPlane', 'Distance', 'DistanceCOP', 'CoordinationNumber', 'OrthogonalDistanceToPore',
- 'Average', 'Difference', 'Minimum', 'LinearCombination', 'DotProduct', 'DistOrthProjOrig'
+ 'Average', 'Difference', 'Minimum', 'Maximum', 'LinearCombination', 'DotProduct', 'DistOrthProjOrig'
]
class CollectiveVariable(object):
@@ -759,34 +759,38 @@ def compute(self, atoms, deriv=True):
class Minimum(CollectiveVariable):
'''
- Class to implement a collective variable representing the minimum of two other collective variables:
+ Class to implement a collective variable representing the minimum of a set of collective variables. To make its definition continuous, it is implemented as:
- .. math:: CV &= \\min\\left(CV_1,CV_2\\right)
+ .. math:: \\min(\\mathbf{s}) = -\\frac{1}{\\beta} \\log \\left( \\sum_i \\exp(-\\beta s_i) \\right)
+
+ This definition is consistent with the ALT_MIN function of the MULTICOLVAR Plumed module.
'''
type = 'scalar'
- def __init__(self, cv1, cv2, name=None):
+ def __init__(self, cvs, beta=50.0, name=None):
'''
- :param cv1: first collective variable in the minimum
- :type cv1: any child class of :py:class:`CollectiveVariable `
+ :param cv_list: list of instances of child classes of :py:class:`CollectiveVariable `
+ :type cv1: List
- :param cv2: second collective variable in the minimum
- :type cv2: any child class of :py:class:`CollectiveVariable `
+ :param beta: parameter to regulate the softness of the minimum.
+ :type beta: float | optional, default=50.0
:param name: Name of CV for printing/logging purposes. If None, the default implemented in the ``_default_name`` routine will be used.
:type name: str | None, optional, default=None
'''
- self.cv1 = cv1
- self.cv2 = cv2
+ self.cvs = cvs
+ for cv in cvs:
+ assert cv.type == 'scalar', "The Minimum function is only implemented for scalar collective variables."
+ self.beta = beta
CollectiveVariable.__init__(self, name=name)
def _default_name(self):
- return 'Min(%s,%s)' %(self.cv1.name, self.cv2.name)
+ return 'Min(' + ",".join([cv.name for cv in self.cvs]) + ")"
def compute(self, atoms, deriv=True):
'''
- Compute the minimum (and optionally gradient) of the two CVs for the given atomic coordinates
+ Compute the minimum (and optionally gradient) of the CV list for the given atomic coordinates
:param atoms: ASE Atoms object on which the CV needs to be computed
:type atoms: ase.Atoms
@@ -797,19 +801,76 @@ def compute(self, atoms, deriv=True):
:return: CV value and potentially the gradient
:rtype: np.ndarray(3) or float,np.ndarray([3,Natoms,3])
'''
+
if not deriv:
- cv1 = self.cv1.compute(atoms, deriv=False)
- cv2 = self.cv2.compute(atoms, deriv=False)
- return min(cv1,cv2)
+ values = [cv.compute(atoms, deriv=False) for cv in self.cvs]
+ S = sum(np.exp(-self.beta * v) for v in values)
+ return - (1.0 / self.beta) * np.log(S)
else:
- cv1, grad1 = self.cv1.compute(atoms, deriv=True)
- cv2, grad2 = self.cv2.compute(atoms, deriv=True)
- value = min(cv1,cv2)
- if cv1`
+ :type cv1: List
+
+ :param beta: parameter to regulate the softness of the maximum.
+ :type beta: float | optional, default=50.0
+
+ :param name: Name of CV for printing/logging purposes. If None, the default implemented in the ``_default_name`` routine will be used.
+ :type name: str | None, optional, default=None
+ '''
+ self.cvs = cvs
+ for cv in cvs:
+ assert cv.type == 'scalar', "The Maximum function is only implemented for scalar collective variables."
+ self.beta = beta
+ CollectiveVariable.__init__(self, name=name)
+
+ def _default_name(self):
+ return 'Max(' + ",".join([cv.name for cv in self.cvs]) + ")"
+
+ def compute(self, atoms, deriv=True):
+ '''
+ Compute the maximum (and optionally gradient) of the CV list for the given atomic coordinates
+
+ :param atoms: ASE Atoms object on which the CV needs to be computed
+ :type atoms: ase.Atoms
+
+ :param deriv: if True, also compute and return the gradient of the CV towards all atomic coordinates
+ :type deriv: bool, optional, default=True
+
+ :return: CV value and potentially the gradient
+ :rtype: np.ndarray(3) or float,np.ndarray([3,Natoms,3])
+ '''
+
+ if not deriv:
+ values = [cv.compute(atoms, deriv=False) for cv in self.cvs]
+ S = sum(np.exp(v / self.beta) for v in values)
+ return self.beta * np.log(S)
+ else:
+ vs, gs = zip(*(cv.compute(atoms, deriv=True) for cv in self.cvs))
+ weights = [np.exp(v / self.beta) for v in vs]
+ S = sum(weights)
+ grad = sum(w * g for w, g in zip(weights, gs)) / S
+ value = self.beta * np.log(S)
+ return value, grad
class LinearCombination(CollectiveVariable):