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

1""" 

2.. class:: BasePathCV 

3 :platform: Linux, MacOS, Windows 

4 :synopsis: A base class for path-related collective variables 

5 

6.. classauthor:: Charlles Abreu <craabreu@gmail.com> 

7 

8""" 

9 

10import typing as t 

11from collections import OrderedDict 

12 

13import openmm 

14 

15from .collective_variable import CollectiveVariable 

16from .path import Metric, deviation, progress 

17 

18 

19class BasePathCV(CollectiveVariable, openmm.CustomCVForce): 

20 """ 

21 A base class for path-related collective variables 

22 

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 """ 

35 

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) 

69 

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