Coverage for cvpack/helix_torsion_content.py: 100%

32 statements  

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

1""" 

2.. class:: HelixTorsionContent 

3 :platform: Linux, MacOS, Windows 

4 :synopsis: Alpha-helix torsion content of a sequence of residues 

5 

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

7 

8""" 

9 

10from __future__ import annotations 

11 

12import typing as t 

13 

14import numpy as np 

15import openmm 

16from openmm import app as mmapp 

17from openmm import unit as mmunit 

18 

19from .collective_variable import CollectiveVariable 

20from .units import Quantity, ScalarQuantity 

21from .utils import check_contiguous_residues 

22 

23 

24class HelixTorsionContent(CollectiveVariable, openmm.CustomTorsionForce): 

25 r""" 

26 The alpha-helix Ramachandran content of a sequence of `n` residues: 

27 

28 .. math:: 

29 

30 \alpha_{\phi,\psi}({\bf r}) = \frac{1}{2} \sum_{i=2}^{n-1} \left[ 

31 B_m\left( 

32 \frac{\phi_i({\bf r}) - \phi_{\rm ref}}{\theta_{\rm tol}} 

33 \right) + 

34 B_m\left( 

35 \frac{\psi_i({\bf r}) - \psi_{\rm ref}}{\theta_{\rm tol}} 

36 \right) 

37 \right] 

38 

39 where :math:`\phi_i({\bf r})` and :math:`\psi_i({\bf r})` are the Ramachandran 

40 dihedral angles of residue :math:`i`, :math:`\phi_{\rm ref}` and 

41 :math:`\psi_{\rm ref}` are their reference values in an alpha helix 

42 :cite:`Hovmoller_2002`, and :math:`\theta_{\rm tol}` is the threshold tolerance 

43 around these references. :math:`B_m(x)` is a smooth boxcar function given by 

44 

45 .. math:: 

46 B_m(x) = \frac{1}{1 + x^{2m}} 

47 

48 where :math:`m` is an integer parameter that controls its steepness. Note that 

49 :math:`x` needs to be raised to an even power for :math:`B_m(x)` to be an even 

50 function. 

51 

52 Optionally, this collective variable can be normalized to the range :math:`[0, 1]`. 

53 

54 .. note:: 

55 

56 The :math:`\phi` and :math:`\psi` angles of the first and last residues are 

57 not considered. They are used to compute the dihedral angles of the second and 

58 penultimate residues, respectively. 

59 

60 The residues must be a contiguous sequence from a single chain, ordered from the 

61 N- to the C-terminus. 

62 

63 Parameters 

64 ---------- 

65 residues 

66 The residues in the sequence. 

67 pbc 

68 Whether to use periodic boundary conditions. 

69 phiReference 

70 The reference value of the phi dihedral angle in an alpha helix. 

71 psiReference 

72 The reference value of the psi dihedral angle in an alpha helix. 

73 tolerance 

74 The threshold tolerance around the reference values. 

75 halfExponent 

76 The parameter :math:`m` of the boxcar function. 

77 normalize 

78 Whether to normalize the collective variable to the range :math:`[0, 1]`. 

79 name 

80 The name of the collective variable. 

81 

82 Raises 

83 ------ 

84 ValueError 

85 If some residue does not contain a :math:`\phi` or :math:`\psi` angle. 

86 

87 Example 

88 ------- 

89 >>> import cvpack 

90 >>> import openmm 

91 >>> from openmm import app, unit 

92 >>> from openmmtools import testsystems 

93 >>> model = testsystems.LysozymeImplicit() 

94 >>> residues = list(model.topology.residues())[59:80] 

95 >>> print(*[r.name for r in residues]) 

96 LYS ASP GLU ... ILE LEU ARG 

97 >>> helix_content = cvpack.HelixTorsionContent(residues) 

98 >>> helix_content.addToSystem(model.system) 

99 >>> platform = openmm.Platform.getPlatformByName('Reference') 

100 >>> integrator = openmm.VerletIntegrator(0) 

101 >>> context = openmm.Context(model.system, integrator, platform) 

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

103 >>> helix_content.getValue(context) 

104 17.452... dimensionless 

105 """ 

106 

107 def __init__( 

108 self, 

109 residues: t.Sequence[mmapp.topology.Residue], 

110 pbc: bool = False, 

111 phiReference: ScalarQuantity = Quantity(-63.8 * mmunit.degrees), 

112 psiReference: ScalarQuantity = Quantity(-41.1 * mmunit.degrees), 

113 tolerance: ScalarQuantity = Quantity(25 * mmunit.degrees), 

114 halfExponent: int = 3, 

115 normalize: bool = False, 

116 name: str = "helix_torsion_content", 

117 ) -> None: 

118 residues = list(residues) 

119 check_contiguous_residues(residues) 

120 if normalize and len(residues) < 3: # noqa: PLR2004 

121 raise ValueError("Normalization requires at least 3 residues") 

122 

123 def find_atom(residue: mmapp.topology.Residue, name: str) -> int: 

124 for atom in residue.atoms(): 

125 if atom.name == name: 

126 return atom.index 

127 raise ValueError( 

128 f"Could not find atom {name} in residue {residue.name}{residue.id}" 

129 ) 

130 

131 numerator = 1 / (2 * (len(residues) - 2)) if normalize else 1 / 2 

132 tol = tolerance 

133 if mmunit.is_quantity(tol): 

134 tol = tol.value_in_unit_system(mmunit.md_unit_system) 

135 super().__init__( 

136 f"{numerator}/(1+x^{2 * halfExponent})" 

137 f"; x=(delta-{2 * np.pi}*floor(delta/{2 * np.pi}+0.5))/{tol}" 

138 "; delta=theta-theta_ref" 

139 ) 

140 self.addPerTorsionParameter("theta_ref") 

141 for i in range(1, len(residues) - 1): 

142 self.addTorsion( 

143 find_atom(residues[i - 1], "C"), 

144 find_atom(residues[i], "N"), 

145 find_atom(residues[i], "CA"), 

146 find_atom(residues[i], "C"), 

147 [phiReference], 

148 ) 

149 self.addTorsion( 

150 find_atom(residues[i], "N"), 

151 find_atom(residues[i], "CA"), 

152 find_atom(residues[i], "C"), 

153 find_atom(residues[i + 1], "N"), 

154 [psiReference], 

155 ) 

156 self.setUsesPeriodicBoundaryConditions(pbc) 

157 self._registerCV( 

158 name, 

159 mmunit.dimensionless, 

160 residues=residues, 

161 pbc=pbc, 

162 phiReference=phiReference, 

163 psiReference=psiReference, 

164 tolerance=tolerance, 

165 halfExponent=halfExponent, 

166 normalize=normalize, 

167 ) 

168 

169 

170HelixTorsionContent.registerTag("!cvpack.HelixTorsionContent")