Coverage for cvpack/composite_rmsd.py: 92%

38 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-16 00:10 +0000

1""" 

2.. class:: CompositeRMSD 

3 :platform: Linux, MacOS, Windows 

4 :synopsis: Deviation of multiple corotating bodies from their reference structures 

5 

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

7 

8""" 

9 

10from __future__ import annotations 

11 

12import typing as t 

13 

14from openmm import unit as mmunit 

15 

16from .base_rmsd import BaseRMSD 

17from .units import MatrixQuantity, VectorQuantity 

18 

19# Earlier versions compute wrong forces for groups of different sizes 

20MIN_OPENMM_CPP_FORCES_VERSION = (0, 2, 0) 

21 

22 

23def _stub(message: str) -> type: 

24 class Stub: 

25 def __init__(self, *_): 

26 raise ImportError(message) 

27 

28 return Stub 

29 

30 

31def _get_force_class() -> type: 

32 """Return CompositeRMSDForce, or a stub that raises an informative ImportError.""" 

33 requirement = "openmm-cpp-forces 0.2.0 or later (mdtools conda channel or PyPI)" 

34 try: 

35 import openmmcppforces # noqa: PLC0415 

36 except ImportError: 

37 return _stub(f"CompositeRMSD requires {requirement}") 

38 version = getattr(openmmcppforces, "__version__", None) 

39 numbers = tuple( 

40 int(part) for part in (version or "").split(".")[:3] if part.isdigit() 

41 ) 

42 if numbers < MIN_OPENMM_CPP_FORCES_VERSION: 

43 return _stub( 

44 f"CompositeRMSD requires {requirement}, because earlier versions compute " 

45 f"wrong forces, but version {version or 'unknown'} is installed" 

46 ) 

47 return openmmcppforces.CompositeRMSDForce 

48 

49 

50CompositeRMSDForce = _get_force_class() 

51 

52 

53class CompositeRMSD(BaseRMSD, CompositeRMSDForce): 

54 r""" 

55 The composite root-mean-square deviation (RMSD) between the current and reference 

56 coordinates of :math:`m` groups of atoms: 

57 

58 .. math:: 

59 

60 d_{\rm crms}({\bf r}) = \sqrt{ 

61 \frac{1}{n} \min_{ 

62 \bf q \in \mathbb{R}^4 \atop \|{\bf q}\| = 1 

63 } \sum_{j=1}^m \sum_{i \in {\bf g}_j} \left\| 

64 {\bf A}({\bf q})\left({\bf r}_i - {\bf c}_j\right) - 

65 {\bf r}_i^{\rm ref} + {\bf c}_j^{\rm ref} 

66 \right\|^2 

67 } 

68 

69 where each group :math:`{\bf g}_j` is a set of :math:`n_j` atom indices, 

70 :math:`n = \sum_{j=1}^m n_j` is the total number of atoms in these groups, 

71 :math:`{\bf A}(\bf q)` is the rotation matrix corresponding to a unit quaternion 

72 :math:`{\bf q}`, :math:`{\bf r}_i` and :math:`{\bf r}_i^{\rm ref}` are the 

73 positions of atom :math:`i` in the current and reference structures, respectively, 

74 :math:`{\bf c}_j` is the centroid of group :math:`j`, given by 

75 

76 .. math:: 

77 

78 {\bf c}_j = \frac{1}{n_j} \sum_{i \in {\bf g}_j} {\bf r}_i 

79 

80 and :math:`{\bf c}_j^{\rm ref}` is the centroid of the reference structure for 

81 group :math:`j`, defined analogously to :math:`{\bf c}_j`. 

82 

83 .. warning:: 

84 

85 To use this class, you must install the `openmm-cpp-forces`_ package, 

86 version 0.2.0 or later. 

87 

88 .. _openmm-cpp-forces: https://anaconda.org/mdtools/openmm-cpp-forces 

89 

90 Parameters 

91 ---------- 

92 referencePositions 

93 The reference coordinates, which can be either a coordinate matrix or a mapping 

94 from atom indices to coordinate vectors. It must contain all atoms in 

95 ``groups``, and does not need to contain all atoms in the system. See 

96 ``numAtoms`` below. 

97 groups 

98 A sequence of disjoint atom groups. Each group is a sequence of atom indices. 

99 numAtoms 

100 The total number of atoms in the system, including those that are not in 

101 ``groups``. This argument is necessary only if ``referencePositions`` does not 

102 contain all atoms in the system. 

103 name 

104 The name of the collective variable. 

105 

106 Raises 

107 ------ 

108 ImportError 

109 If the `openmm-cpp-forces`_ package (version 0.2.0 or later) is not 

110 installed. 

111 ValueError 

112 If ``groups`` is not a sequence of disjoint atom groups. 

113 ValueError 

114 If referencePositions is a dict and numAtoms is not given 

115 

116 Example 

117 ------- 

118 >>> import cvpack 

119 >>> import openmm as mm 

120 >>> import pytest 

121 >>> from openmmtools import testsystems 

122 >>> from openmm import unit 

123 >>> model = testsystems.HostGuestVacuum() 

124 >>> host_atoms, guest_atoms = ( 

125 ... [a.index for a in r.atoms()] 

126 ... for r in model.topology.residues() 

127 ... ) 

128 >>> try: 

129 ... composite_rmsd = cvpack.CompositeRMSD( 

130 ... model.positions, 

131 ... [host_atoms, guest_atoms], 

132 ... ) 

133 ... except ImportError: 

134 ... pytest.skip("openmm-cpp-forces is not installed") 

135 >>> composite_rmsd.addToSystem(model.system) 

136 >>> context = mm.Context( 

137 ... model.system, 

138 ... mm.VerletIntegrator(1.0 * unit.femtoseconds), 

139 ... mm.Platform.getPlatformByName('Reference'), 

140 ... ) 

141 >>> context.setPositions(model.positions) 

142 >>> composite_rmsd.getValue(context) 

143 0.0 nm 

144 >>> model.positions[guest_atoms] += 1.0 * unit.nanometers 

145 >>> context.setPositions(model.positions) 

146 >>> composite_rmsd.getValue(context) 

147 0.0 nm 

148 """ 

149 

150 def __init__( 

151 self, 

152 referencePositions: MatrixQuantity | dict[int, VectorQuantity], 

153 groups: t.Iterable[t.Iterable[int]], 

154 numAtoms: int | None = None, 

155 name: str = "composite_rmsd", 

156 ) -> None: 

157 if numAtoms is None and isinstance(referencePositions, dict): 

158 raise ValueError("numAtoms is required when referencePositions is a dict") 

159 num_atoms = numAtoms or len(referencePositions) 

160 groups = [[int(atom) for atom in group] for group in groups] 

161 if not all(groups): 

162 # CompositeRMSDForce would interpret an empty group as all atoms 

163 raise ValueError("The groups of atoms must not be empty") 

164 defined_coords = self._getDefinedCoords(referencePositions, sum(groups, [])) 

165 all_coords = self._getAllCoords(defined_coords, num_atoms) 

166 super().__init__(all_coords) 

167 for group in groups: 

168 self.addGroup(group) 

169 self._registerCV( 

170 name, 

171 mmunit.nanometers, 

172 referencePositions=defined_coords, 

173 groups=groups, 

174 numAtoms=num_atoms, 

175 ) 

176 

177 

178CompositeRMSD.registerTag("!cvpack.CompositeRMSD")