Skip to content

Commit

Permalink
Includes PathCV that uses reference structures as milestones (#93)
Browse files Browse the repository at this point in the history
* Defined base class for path-related CVs

* Fixed doctest

* Initial implementation of PathInRMSDSpace

* Included doctest

* Improved doctest

* Fixed base path cv usage

* Added unit test

* Included missing package

* Fixed tests

* Fixed dependencies

* Using absolute error in assertion

* Updated README.md
  • Loading branch information
craabreu committed Apr 20, 2024
1 parent 907d7ca commit 9775ac3
Show file tree
Hide file tree
Showing 12 changed files with 313 additions and 48 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ The CVs implemented in CVPack are listed in the table below.
| [Meta CV] | a function of other collective variables |
| [Number of contacts] | number of contacts between two groups of atoms |
| [OpenMM Force wrapper] | converts an OpenMM Force object into a CVPack CV |
| [Path in CV space] | progress along (or deviation from) a predefined path in CV space |
| [Path in CV space] | progress along (or deviation from) a path in CV space |
| [Path in RMSD space] | progress along (or deviation from) a path in RMSD space |
| [Radius of gyration] | radius of gyration of a group of atoms |
| [(Radius of gyration)^2]| square of the radius of gyration of a group of atoms |
| [Residue coordination] | number of contacts between two disjoint groups of residues |
Expand Down Expand Up @@ -117,6 +118,7 @@ Initial project based on the [CMS Cookiecutter] version 1.1.
[Number of contacts]: https://redesignscience.github.io/cvpack/latest/api/NumberOfContacts.html
[OpenMM Force wrapper]: https://redesignscience.github.io/cvpack/latest/api/OpenMMForceWrapper.html
[Path in CV space]: https://redesignscience.github.io/cvpack/latest/api/PathInCVSpace.html
[Path in RMSD space]: https://redesignscience.github.io/cvpack/latest/api/PathInRMSDSpace.html
[Radius of gyration]: https://redesignscience.github.io/cvpack/latest/api/RadiusOfGyration.html
[(Radius of gyration)^2]: https://redesignscience.github.io/cvpack/latest/api/RadiusOfGyrationSq.html
[Residue coordination]: https://redesignscience.github.io/cvpack/latest/api/ResidueCoordination.html
Expand Down
2 changes: 2 additions & 0 deletions cvpack/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from .number_of_contacts import NumberOfContacts # noqa: F401
from .openmm_force_wrapper import OpenMMForceWrapper # noqa: F401
from .path_in_cv_space import PathInCVSpace # noqa: F401
from .path_in_rmsd_space import PathInRMSDSpace # noqa: F401
from .radius_of_gyration import RadiusOfGyration # noqa: F401
from .radius_of_gyration_sq import RadiusOfGyrationSq # noqa: F401
from .residue_coordination import ResidueCoordination # noqa: F401
Expand All @@ -44,6 +45,7 @@
"NumberOfContacts",
"OpenMMForceWrapper",
"PathInCVSpace",
"PathInRMSDSpace",
"RadiusOfGyration",
"RadiusOfGyrationSq",
"ResidueCoordination",
Expand Down
71 changes: 71 additions & 0 deletions cvpack/base_path_cv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""
.. class:: BasePathCV
:platform: Linux, MacOS, Windows
:synopsis: A base class for path-related collective variables
.. classauthor:: Charlles Abreu <craabreu@gmail.com>
"""

import typing as t
from collections import OrderedDict

import openmm

from .collective_variable import CollectiveVariable
from .path import Metric, deviation, progress


class BasePathCV(openmm.CustomCVForce, CollectiveVariable):
"""
A base class for path-related collective variables
Parameters
----------
metric
A measure of progress or deviation with respect to a path in CV space
sigma
The width of the Gaussian kernels
squared_distances
Expressions for the squared distance to each milestone
variables
A dictionary of collective variables used in the expressions for the squared
distances
"""

def __init__(
self,
metric: Metric,
sigma: float,
squared_distances: t.Sequence[str],
variables: t.Dict[str, CollectiveVariable],
) -> None:
n = len(squared_distances)
definitions = OrderedDict(
{f"x{i}": sqdist for i, sqdist in enumerate(squared_distances)}
)
definitions["lambda"] = 1 / (2 * sigma**2)
definitions["xmin0"] = "min(x0,x1)"
for i in range(n - 2):
definitions[f"xmin{i+1}"] = f"min(xmin{i},x{i+2})"
for i in range(n):
definitions[f"w{i}"] = f"exp(lambda*(xmin{n - 2}-x{i}))"
definitions["wsum"] = "+".join(f"w{i}" for i in range(n))
expressions = [f"{key}={value}" for key, value in definitions.items()]
if metric == progress:
numerator = "+".join(f"{i}*w{i}" for i in range(1, n))
expressions.append(f"({numerator})/({n - 1}*wsum)")
else:
expressions.append(f"xmin{n - 2}-log(wsum)/lambda")
super().__init__("; ".join(reversed(expressions)))
for name, variable in variables.items():
self.addCollectiveVariable(name, variable)

def _generateName(self, metric: Metric, name: str, kind: str) -> str:
if metric not in (progress, deviation):
raise ValueError(
"Invalid metric. Use 'cvpack.path.progress' or 'cvpack.path.deviation'."
)
if name is None:
return f"path_{metric.name}_in_{kind}_space"
return name
50 changes: 18 additions & 32 deletions cvpack/path_in_cv_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,18 @@
"""

import typing as t
from collections import OrderedDict
from copy import deepcopy

import openmm
from openmm import unit as mmunit

from .base_path_cv import BasePathCV
from .collective_variable import CollectiveVariable
from .path import Metric, deviation, progress
from .path import Metric
from .units.units import MatrixQuantity, ScalarQuantity, value_in_md_units
from .utils import convert_to_matrix


class PathInCVSpace(openmm.CustomCVForce, CollectiveVariable):
class PathInCVSpace(BasePathCV):
r"""
A metric of the system's progress (:math:`s`) or deviation (:math:`z`) with
respect to a path defined by a sequence of :math:`n` milestones positioned in a
Expand Down Expand Up @@ -97,14 +96,15 @@ class PathInCVSpace(openmm.CustomCVForce, CollectiveVariable):
Examples
--------
>>> import cvpack
>>> import openmm
>>> from openmmtools import testsystems
>>> import numpy as np
>>> model = testsystems.AlanineDipeptideVacuum()
>>> phi_atoms = ["ACE-C", "ALA-N", "ALA-CA", "ALA-C"]
>>> psi_atoms = ["ALA-N", "ALA-CA", "ALA-C", "NME-N"]
>>> atoms = [f"{a.residue.name}-{a.name}" for a in model.topology.atoms()]
>>> milestones = np.array(
... [[1.3, -0.2], [1.2, 3.1], [-2.7, 2.9], [-1.3, 2.7], [-1.3, -0.4]]
... [[1.3, -0.2], [1.2, 3.1], [-2.7, 2.9], [-1.3, 2.7]]
... )
>>> phi = cvpack.Torsion(*[atoms.index(atom) for atom in phi_atoms])
>>> psi = cvpack.Torsion(*[atoms.index(atom) for atom in psi_atoms])
Expand All @@ -116,9 +116,9 @@ class PathInCVSpace(openmm.CustomCVForce, CollectiveVariable):
>>> context = openmm.Context(model.system, openmm.VerletIntegrator(1.0))
>>> context.setPositions(model.positions)
>>> path_vars[0].getValue(context)
0.50... dimensionless
0.6... dimensionless
>>> path_vars[1].getValue(context)
0.25... dimensionless
0.2... dimensionless
"""

def __init__( # pylint: disable=too-many-branches
Expand All @@ -130,20 +130,15 @@ def __init__( # pylint: disable=too-many-branches
scales: t.Optional[t.Iterable[ScalarQuantity]] = None,
name: t.Optional[str] = None,
) -> None:
if metric not in (progress, deviation):
raise ValueError(
"Invalid metric. Use 'cvpack.path.progress' or 'cvpack.path.deviation'."
)
if name is None:
name = f"path_{metric.name}_in_cv_space"
name = self._generateName(metric, name, "cv")
variables = list(variables)
cv_scales = [1.0] * len(variables) if scales is None else list(scales)
milestones, n, numvars = convert_to_matrix(milestones)
if numvars != len(variables):
raise ValueError("Wrong number of columns in the milestones matrix.")
if n < 2:
raise ValueError("At least two rows are required in the milestones matrix.")
definitions = OrderedDict({"lambda": 1 / (2 * sigma**2)})
squared_distances = []
periods = {}
for i, variable in enumerate(variables):
values = variable.getPeriodicBounds()
Expand All @@ -153,25 +148,16 @@ def __init__( # pylint: disable=too-many-branches
deltas = [f"({value}-cv{j})" for j, value in enumerate(values)]
for j, period in periods.items():
deltas[j] = f"min(abs{deltas[j]},{period}-abs{deltas[j]})"
definitions[f"x{i}"] = "+".join(
f"{delta}^2" if scale == 1.0 else f"({delta}/{scale})^2"
for delta, scale in zip(deltas, cv_scales)
squared_distances.append(
"+".join(
f"{delta}^2" if scale == 1.0 else f"({delta}/{scale})^2"
for delta, scale in zip(deltas, cv_scales)
)
)
definitions["xmin0"] = "min(x0,x1)"
for i in range(n - 2):
definitions[f"xmin{i+1}"] = f"min(xmin{i},x{i+2})"
for i in range(n):
definitions[f"w{i}"] = f"exp(lambda*(xmin{n - 2}-x{i}))"
definitions["wsum"] = "+".join(f"w{i}" for i in range(n))
expressions = [f"{key}={value}" for key, value in definitions.items()]
if metric == progress:
numerator = "+".join(f"{i}*w{i}" for i in range(1, n))
expressions.append(f"({numerator})/({n - 1}*wsum)")
else:
expressions.append(f"xmin{n - 2}-log(wsum)/lambda")
super().__init__("; ".join(reversed(expressions)))
for i, variable in enumerate(variables):
self.addCollectiveVariable(f"cv{i}", deepcopy(variable))
collective_variables = {
f"cv{i}": deepcopy(variable) for i, variable in enumerate(variables)
}
super().__init__(metric, sigma, squared_distances, collective_variables)
self._registerCV(
name,
mmunit.dimensionless,
Expand Down
153 changes: 153 additions & 0 deletions cvpack/path_in_rmsd_space.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
"""
.. class:: PathInRMSDSpace
:platform: Linux, MacOS, Windows
:synopsis: A metric of progress or deviation with respect to a path in RMSD space
.. classauthor:: Charlles Abreu <craabreu@gmail.com>
"""

import typing as t

from openmm import unit as mmunit

from .base_path_cv import BasePathCV
from .path import Metric, progress
from .rmsd import RMSD
from .units.units import VectorQuantity


class PathInRMSDSpace(BasePathCV):
r"""
A metric of the system's progress (:math:`s`) or deviation (:math:`z`) with
respect to a path defined by a sequence of :math:`n` milestones defined as
reference structures :cite:`Branduardi_2007`:
.. math::
s({\bf r}) = \frac{
\dfrac{\sum_{i=1}^n i w_i({\bf r})}{\sum_{i=1}^n w_i({\bf r})} - 1
}{n-1}
\quad \text{or} \quad
z({\bf r}) = - 2 \sigma ^2 \ln \sum_{i=1}^n w_i({\bf r})
with :math:`w_i({\bf r})` being a Gaussian kernel centered at the :math:`i`-th
milestone, i.e.,
.. math::
w_i({\bf r}) = \exp\left(\
-\frac{d^2_{\rm rms}({\bf r},{\bf r}^{\rm ref}_i)}{2 \sigma^2}
\right)
where :math:`d_{\rm rms}({\bf r},{\bf r}^{\rm ref}_i)` is the root-mean-square
distance between the current system state and the :math:`i`-th reference structure
and :math:`\sigma` sets the width of the kernels.
.. note::
The kernel width :math:`\sigma` is related to the parameter :math:`\lambda` of
Ref. :cite:`Branduardi_2007` by :math:`\sigma = \frac{1}{\sqrt{2\lambda}}`.
Parameters
----------
metric
The path-related metric to compute. Use ``cvpack.path.progress`` for
computing :math:`s({\bf r})` or ``cvpack.path.deviation`` for computing
:math:`z({\bf r})`.
milestones
A sequence of reference structures, each represented as a dictionary mapping
atom indices to coordinate vectors.
numAtoms
The total number of atoms in the system, including those that are not in
any of the reference structures.
sigma
The width of the Gaussian kernels in nanometers
name
The name of the collective variable. If not provided, it is set to
"path_progress_in_rmsd_space" or "path_deviation_in_rmsd_space" depending
on the metric.
Raises
------
ValueError
The number of milestones is less than 2
ValueError
If the metric is not `cvpack.path.progress` or `cvpack.path.deviation`
Examples
--------
>>> import cvpack
>>> import networkx as nx
>>> import numpy as np
>>> import openmm
>>> from openmm import unit
>>> from openmmtools import testsystems
>>> from scipy.spatial.transform import Rotation
>>> model = testsystems.AlanineDipeptideVacuum()
>>> atom1, atom2 = 8, 14
>>> graph = model.mdtraj_topology.to_bondgraph()
>>> nodes = list(graph.nodes)
>>> graph.remove_edge(nodes[atom1], nodes[atom2])
>>> movable = list(nx.connected_components(graph))[1]
>>> x = model.positions / model.positions.unit
>>> x0 = x[atom1, :]
>>> vector = x[atom2, :] - x0
>>> vector /= np.linalg.norm(vector)
>>> rotation = Rotation.from_rotvec((np.pi / 6) * vector)
>>> atoms = [nodes.index(atom) for atom in movable]
>>> frames = [x.copy()]
>>> for _ in range(6):
... x[atoms, :] = x0 + rotation.apply(x[atoms, :] - x0)
... frames.append(x.copy())
>>> milestones = [
... {i: row for i, row in enumerate(frame)}
... for frame in frames
... ]
>>> s, z = [
... cvpack.PathInRMSDSpace(
... metric, milestones, len(x), 0.5 * unit.angstrom
... )
... for metric in (cvpack.path.progress, cvpack.path.deviation)
... ]
>>> s.addToSystem(model.system)
>>> z.addToSystem(model.system)
>>> context = openmm.Context(model.system, openmm.VerletIntegrator(0.001))
>>> context.setPositions(model.positions)
>>> s.getValue(context)
0.172... dimensionless
>>> z.getValue(context)
-0.004... nm**2
"""

def __init__( # pylint: disable=too-many-branches
self,
metric: Metric,
milestones: t.Sequence[t.Dict[int, VectorQuantity]],
numAtoms: int,
sigma: mmunit.Quantity,
name: t.Optional[str] = None,
) -> None:
name = self._generateName(metric, name, "rmsd")
if mmunit.is_quantity(sigma):
sigma = sigma.value_in_unit(mmunit.nanometers)
n = len(milestones)
if n < 2:
raise ValueError("At least two reference structures are required.")
collective_variables = {
f"rmsd{i}": RMSD(reference, reference.keys(), numAtoms, name=f"rmsd{i}")
for i, reference in enumerate(milestones)
}
squared_distances = [f"rmsd{i}^2" for i in range(n)]
super().__init__(metric, sigma, squared_distances, collective_variables)
self._registerCV(
name,
mmunit.dimensionless if metric == progress else mmunit.nanometers**2,
metric=metric,
milestones=[{k: list(v) for k, v in m.items()} for m in milestones],
numAtoms=numAtoms,
sigma=sigma,
)


PathInRMSDSpace.registerTag("!cvpack.PathInRMSDSpace")
Loading

0 comments on commit 9775ac3

Please sign in to comment.