Coverage for cvpack/base_path_cv.py: 97%
31 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-16 00:10 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-16 00:10 +0000
1"""
2.. class:: BasePathCV
3 :platform: Linux, MacOS, Windows
4 :synopsis: A base class for path-related collective variables
6.. classauthor:: Charlles Abreu <craabreu@gmail.com>
8"""
10import typing as t
11from collections import OrderedDict
13import openmm
15from .collective_variable import CollectiveVariable
16from .path import Metric, deviation, progress
19class BasePathCV(CollectiveVariable, openmm.CustomCVForce):
20 """
21 A base class for path-related collective variables
23 Parameters
24 ----------
25 metric
26 A measure of progress or deviation with respect to a path in CV space
27 sigma
28 The width of the Gaussian kernels
29 squared_distances
30 Expressions for the squared distance to each milestone
31 variables
32 A dictionary of collective variables used in the expressions for the squared
33 distances
34 """
36 def __init__(
37 self,
38 metric: Metric,
39 sigma: float,
40 squared_distances: t.Sequence[str],
41 variables: dict[str, CollectiveVariable],
42 ) -> None:
43 n = len(squared_distances)
44 definitions = OrderedDict(
45 {f"x{i}": sqdist for i, sqdist in enumerate(squared_distances)}
46 )
47 definitions["lambda"] = 1 / (2 * sigma**2)
48 definitions["xmin0"] = "min(x0,x1)"
49 for i in range(n - 2):
50 definitions[f"xmin{i + 1}"] = f"min(xmin{i},x{i + 2})"
51 # Shifting all exponents by about the smallest squared distance keeps the
52 # weights between exp(-1) and 1 for the nearest milestone. The metrics do not
53 # depend on the shift, and floor() has a zero derivative, so the derivatives
54 # of the expression do not contain those of the chain of minima, which made
55 # Context creation scale steeply with the number of milestones.
56 definitions["shift"] = f"floor(lambda*xmin{n - 2})/lambda"
57 for i in range(n):
58 definitions[f"w{i}"] = f"exp(lambda*(shift-x{i}))"
59 definitions["wsum"] = "+".join(f"w{i}" for i in range(n))
60 expressions = [f"{key}={value}" for key, value in definitions.items()]
61 if metric == progress:
62 numerator = "+".join(f"{i}*w{i}" for i in range(1, n))
63 expressions.append(f"({numerator})/({n - 1}*wsum)")
64 else:
65 expressions.append("shift-log(wsum)/lambda")
66 super().__init__("; ".join(reversed(expressions)))
67 for name, variable in variables.items():
68 self.addCollectiveVariable(name, variable)
70 def _generateName(self, metric: Metric, name: str, kind: str) -> str:
71 if metric not in (progress, deviation):
72 raise ValueError(
73 "Invalid metric. Use 'cvpack.path.progress' or 'cvpack.path.deviation'."
74 )
75 if name is None:
76 return f"path_{metric.name}_in_{kind}_space"
77 return name