Coverage for openxps/bounds/base.py: 96%

50 statements  

« prev     ^ index     » next       coverage.py v7.11.3, created at 2025-11-13 22:08 +0000

1""" 

2Base class for boundary conditions. 

3 

4.. module:: openxps.bounds.base 

5 :platform: Linux, MacOS, Windows 

6 :synopsis: Base class for boundary conditions 

7 

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

9 

10""" 

11 

12import typing as t 

13from dataclasses import dataclass 

14 

15import cvpack 

16from cvpack.serialization import Serializable 

17from cvpack.units import Quantity 

18from openmm import unit as mmunit 

19 

20from ..utils import preprocess_args 

21 

22 

23@dataclass(frozen=True, eq=False) 

24class Bounds(Serializable): 

25 """Boundary condition for a dynamical variable. 

26 

27 Parameters 

28 ---------- 

29 lower 

30 The lower bound for the dynamical variable. 

31 upper 

32 The upper bound for the dynamical variable. 

33 unit 

34 The unity of measurement of the bounds. If the bounds do not have a unit, use 

35 ``dimensionless``. 

36 """ 

37 

38 lower: float 

39 upper: float 

40 unit: cvpack.units.Unit 

41 

42 def __post_init__(self) -> None: 

43 for kind in ("lower", "upper"): 

44 if not isinstance(getattr(self, kind), (int, float)): 

45 raise TypeError(f"The {kind} bound must be a real number.") 

46 if self.lower >= self.upper: 

47 raise ValueError("The upper bound must be greater than the lower bound.") 

48 if not mmunit.is_unit(self.unit): 

49 raise TypeError("The unit must be a valid OpenMM unit.") 

50 object.__setattr__(self, "unit", cvpack.units.Unit(self.unit)) 

51 object.__setattr__(self, "length", self.upper - self.lower) 

52 

53 def __getstate__(self) -> dict[str, t.Any]: 

54 return {"lower": self.lower, "upper": self.upper, "unit": self.unit} 

55 

56 def __setstate__(self, keywords: dict[str, t.Any]) -> None: 

57 self.__init__(keywords["lower"], keywords["upper"], keywords["unit"]) 

58 

59 def __eq__(self, other: t.Any) -> bool: 

60 return ( 

61 isinstance(other, Bounds) 

62 and self.lower * self.unit == other.lower * other.unit 

63 and self.upper * self.unit == other.upper * other.unit 

64 ) 

65 

66 def __hash__(self) -> int: 

67 unit, factor = self._md_unit_and_conversion_factor() 

68 return hash((self.lower * factor, self.upper * factor, unit)) 

69 

70 def _md_unit_and_conversion_factor(self) -> tuple[mmunit.Unit, float]: 

71 """ 

72 Return the MD unit and conversion factor for the bounds. 

73 """ 

74 unit = self.unit.in_unit_system(mmunit.md_unit_system) 

75 factor = self.unit.conversion_factor_to(unit) 

76 return unit, factor 

77 

78 def in_md_units(self) -> "Bounds": 

79 """ 

80 Return the bounds in the MD unit system. 

81 

82 Example 

83 ------- 

84 >>> import openxps as xps 

85 >>> from openmm import unit 

86 >>> bounds = xps.bounds.PeriodicBounds(-1.0, 1.0, unit.kilocalories_per_mole) 

87 >>> bounds.in_md_units() 

88 PeriodicBounds(lower=-4.184, upper=4.184, unit=nm**2 Da/(ps**2)) 

89 """ 

90 unit, factor = self._md_unit_and_conversion_factor() 

91 return self.__class__(self.lower * factor, self.upper * factor, unit) 

92 

93 def convert(self, unit: mmunit.Unit) -> "Bounds": 

94 """ 

95 Convert the bounds to a different unit of measurement. 

96 

97 Parameters 

98 ---------- 

99 unit 

100 The new unit of measurement for the bounds. 

101 

102 Returns 

103 ------- 

104 Bounds 

105 The bounds in the new unit of measurement. 

106 

107 Example 

108 ------- 

109 >>> import openxps as xps 

110 >>> from openmm import unit 

111 >>> bounds = xps.bounds.PeriodicBounds(-180, 180, unit.degree) 

112 >>> bounds.convert(unit.radian) 

113 PeriodicBounds(lower=-3.14159..., upper=3.14159..., unit=rad) 

114 """ 

115 factor = 1.0 * self.unit / unit 

116 if not isinstance(factor, float): 

117 raise ValueError("The unit must be compatible with the bounds unit.") 

118 return self.__class__(factor * self.lower, factor * self.upper, unit) 

119 

120 def asQuantity(self) -> mmunit.Quantity: 

121 """ 

122 Return the bounds as a Quantity object. 

123 

124 Returns 

125 ------- 

126 Quantity 

127 The bounds as a Quantity object. 

128 

129 Example 

130 ------- 

131 >>> import openxps as xps 

132 >>> from openmm import unit 

133 >>> bounds = xps.bounds.PeriodicBounds(-180, 180, unit.degree) 

134 >>> bounds.asQuantity() 

135 (-180, 180) deg 

136 """ 

137 return Quantity((self.lower, self.upper), self.unit) 

138 

139 def leptonExpression(self, variable: str) -> str: 

140 """ 

141 Return a lepton expression representing the transformation from an unwrapped 

142 variable to a wrapped value under the boundary conditions. 

143 

144 Parameters 

145 ---------- 

146 variable 

147 The name of the variable in the expression. 

148 Returns 

149 ------- 

150 str 

151 A string representing the transformation. 

152 

153 Example 

154 ------- 

155 >>> import openxps as xps 

156 >>> from openmm import unit 

157 >>> periodic = xps.bounds.PeriodicBounds(-180, 180, unit.degree) 

158 >>> print(periodic.leptonExpression("x")) 

159 (scaled_x-floor(scaled_x))*360-180; 

160 scaled_x=(x+180)/360 

161 >>> reflective = xps.bounds.ReflectiveBounds(1, 10, unit.angstrom) 

162 >>> print(reflective.leptonExpression("y")) 

163 min(wrapped_y,1-wrapped_y)*18+1; 

164 wrapped_y=scaled_y-floor(scaled_y); 

165 scaled_y=(y-1)/18 

166 """ 

167 raise NotImplementedError( 

168 "The method transformation must be implemented in subclasses." 

169 ) 

170 

171 def wrap(self, value: float, rate: float) -> tuple[float, float]: 

172 """ 

173 Wrap a value around the bounds and adjust its rate of change. 

174 

175 Parameters 

176 ---------- 

177 value 

178 The unwrapped value, in the same unit of measurement as the bounds. 

179 rate 

180 The rate of change of the unwrapped value, in the same unit of measurement 

181 as the bounds divided by a time unit. 

182 

183 Returns 

184 ------- 

185 float 

186 The wrapped value, in the same unit of measurement as the bounds. 

187 float 

188 The adjusted rate of change of the wrapped value, in the same unit of 

189 measurement as the original rate. 

190 """ 

191 raise NotImplementedError("The method wrap must be implemented in subclasses.") 

192 

193 

194Bounds.__init__ = preprocess_args(Bounds.__init__)