Coverage for calorine/tools/entropy.py: 100%

71 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-14 16:36 +0000

1from warnings import warn 

2import numpy as np 

3 

4try: 

5 import torch 

6 torch_available = True 

7except ImportError: # pragma: no cover 

8 torch_available = False 

9 # If pytorch is not available, we still need a torch object 

10 # to satisfy type hints etc. Otherwise the file will not run. 

11 

12 class _DummyTensor: 

13 """Fallback Tensor placeholder.""" 

14 pass 

15 

16 class _DummyDevice: 

17 """Fallback device placeholder.""" 

18 def __init__(self, *args, **kwargs): 

19 pass 

20 

21 class _DummyDType: 

22 """Fallback dtype placeholder.""" 

23 pass 

24 

25 class _DummyTorchModule: 

26 Tensor = _DummyTensor 

27 device = _DummyDevice 

28 dtype = _DummyDType 

29 

30 # Provide common dtype names you might reference 

31 float32 = _DummyDType() 

32 float64 = _DummyDType() 

33 

34 torch = _DummyTorchModule() 

35 

36try: 

37 import warnings as _warnings 

38 with _warnings.catch_warnings(): 

39 _warnings.filterwarnings('ignore', message='IProgress not found') 

40 from tqdm.auto import tqdm 

41except ImportError: 

42 def tqdm(iterable, **kwargs): 

43 """Fallback for when tqdm is not installed; accepts and ignores the 

44 keyword arguments that tqdm takes.""" 

45 return iterable 

46 

47 

48def get_entropy( 

49 descriptors: np.ndarray | torch.Tensor, 

50 width: float, 

51 use_tqdm: bool = True, 

52 device: str | torch.device = 'cuda', 

53 block: int = 1024, 

54 dtype: torch.dtype = torch.float64, 

55 eps: float = 1e-12, 

56 cumulative: bool = False, 

57) -> tuple[float, np.ndarray] | tuple[np.ndarray, np.ndarray]: 

58 r""" 

59 Computes an estimate for the information entropy :math:`H(\mathbf{X})` of a 

60 set of descriptors :math:`\mathbf{X}`. The estimate is described in 

61 [Nat. Comm. **16**, 4014 (2025)](https://doi.org/10.1038/s41467-025-59232-0) 

62 and given by 

63 

64 .. math:: 

65 

66 \mathcal{H}(\{\mathbf{X}\}) = -\frac{1}{n} \sum_{i=1}^{n} p_i 

67 

68 where 

69 

70 .. math:: 

71 

72 p_i 

73 = \log \left[ 

74 \frac{1}{n} \sum_{j=1}^{n} 

75 K_h(\mathbf{X}_i, \mathbf{X}_j) 

76 \right] 

77 

78 with a Gaussian kernel 

79 

80 .. math:: 

81 

82 K_h(\mathbf{X}_i, \mathbf{X}_j) 

83 = \exp\!\left( 

84 -\frac{\lVert \mathbf{X}_i - \mathbf{X}_j \rVert^2}{2h^2}. 

85 \right) 

86 

87 The calculation is done via torch if the library has been installed, 

88 and numpy otherwise. When using torch the calculation is run via CUDA. 

89 The latter behavior can be controlled using the :attr:`device` argument. 

90 

91 Parameters 

92 ---------- 

93 descriptors 

94 The set of descriptors :math:`\mathbf{X}` for which to evaluate to 

95 the entropy. Typically each row corresponds to one atom and the 

96 columns correspond to the different descriptor components. 

97 width 

98 Width :math:`h` of the Gaussian kernel. 

99 use_tqdm 

100 Use `tqdm <https://tqdm.github.io/>`_ to show a progress bar. 

101 Note that this requires tqdm to be installed. 

102 block 

103 In order to limit the memory needs, the kernel density estimate 

104 matrix is handled in blocks. This parameter controls the size of 

105 each block. Smaller numbers imply a smaller memory footprint. 

106 eps 

107 Smallest (absolute) permissible value. 

108 device 

109 Device to use for calculation. The documentation of 

110 [`torch.device`](https://docs.pytorch.org/docs/stable/tensor_attributes.html#torch.device) 

111 provides more information. 

112 Only used when pytorch is available. 

113 dtype 

114 Floating point precision used for the computation. The documentation of 

115 [`torch.dtype`](https://docs.pytorch.org/docs/stable/tensor_attributes.html) 

116 provides an overview. 

117 Only used when pytorch is available. 

118 cumulative 

119 If True, returns the cumulative entropy of each row in the descriptor. 

120 

121 

122 Returns 

123 ------- 

124 A tuple comprising the total entropy :math:`H(\mathbf{X})` and the entropy contributions 

125 :math:`p_i` from each row in the input descriptor matrix. 

126 """ 

127 if torch_available: 

128 res = _get_entropy_torch(descriptors, 

129 width, 

130 use_tqdm, 

131 block, 

132 eps, 

133 cumulative, 

134 device, 

135 dtype) 

136 return res 

137 else: # pragma: no cover 

138 warn('Using the numpy implementation.' 

139 ' Install torch in order to use GPUs and achieve a considerable speed-up.') 

140 res = _get_entropy_numpy(descriptors, width, use_tqdm, block, eps, cumulative) 

141 return res 

142 

143 

144def _get_entropy_numpy( 

145 descriptors: np.ndarray, 

146 width: float, 

147 use_tqdm: bool, 

148 block: int, 

149 eps: float, 

150 cumulative: bool, 

151) -> tuple[float, np.ndarray] | tuple[np.ndarray, np.ndarray]: 

152 """Compute the informational entropy using numpy. 

153 See get_entropy for documentation. 

154 """ 

155 X = np.asarray(descriptors, dtype=np.float64) 

156 N, d = X.shape 

157 s = np.sum(X * X, axis=1) # (N,) 

158 inv_two_sigma2 = 1.0 / (2.0 * width * width) 

159 

160 if not cumulative: 

161 N_mat = N 

162 row_sums = np.zeros(N, dtype=np.float64) 

163 else: 

164 N_mat = np.repeat(np.arange(1, N+1, dtype=np.float64)[:, None], N, axis=1).T 

165 row_sums = np.tril(N_mat, -1) 

166 

167 for i0 in tqdm(range(0, N, block), leave=False, disable=not use_tqdm): 

168 i1 = min(i0 + block, N) 

169 # D2[i0:i1, :] = s[i0:i1,None] + s[None,:] - 2*X[i0:i1]@X.T 

170 G = X[i0:i1] @ X.T # (B,N) 

171 D2_blk = (s[i0:i1, None] + s[None, :] - 2.0 * G) 

172 K = np.exp(-D2_blk * inv_two_sigma2) 

173 if not cumulative: 

174 row_sums[i0:i1] = np.sum(K, axis=1) 

175 else: 

176 K = np.cumsum(K, axis=1) 

177 row_sums[i0:i1, 0:N] += np.triu(K, k=i0) 

178 

179 subvals = -np.log(np.clip(row_sums / N_mat, min=eps))/N_mat 

180 entropy = np.sum(subvals, axis=0) 

181 

182 if not cumulative: 

183 entropy = float(entropy) 

184 

185 return entropy, subvals 

186 

187 

188def _get_entropy_torch( 

189 descriptors: np.ndarray | torch.Tensor, 

190 width: float, 

191 use_tqdm: bool, 

192 block: int, 

193 eps: float, 

194 cumulative: bool, 

195 device: str | torch.device = 'cuda', 

196 dtype: torch.dtype = torch.float64, 

197) -> tuple[float, np.ndarray] | tuple[np.ndarray, np.ndarray]: 

198 """Compute the informational entropy using torch. 

199 See get_entropy for documentation. 

200 """ 

201 with torch.no_grad(): 

202 # Move data to device 

203 if not torch.cuda.is_available() and str(device) == 'cuda': # pragma: no cover 

204 device = 'cpu' 

205 X = torch.as_tensor(descriptors, dtype=dtype, device=device) 

206 N, d = X.shape 

207 inv_two_sigma2 = 1.0 / (2.0 * width * width) 

208 

209 # Precompute norms once 

210 s = (X * X).sum(dim=1) # (N,) 

211 

212 if not cumulative: 

213 N_mat = N 

214 row_sums = torch.zeros(N, dtype=dtype, device=device) 

215 else: 

216 N_mat = torch.arange(1, N+1, dtype=dtype, device=device) 

217 N_mat = N_mat.repeat(N).reshape((N, N)) 

218 row_sums = torch.tril(N_mat, diagonal=-1) 

219 

220 # Block over rows i; each block computes K[i0:i1, :].sum(-1) 

221 # D2 = s[i] + s[j] - 2 * X[i] @ X[j]^T (formed in blocks to save memory) 

222 XT = X.T # reuse in matmuls 

223 for i0 in tqdm(range(0, N, block), leave=False, disable=not use_tqdm): 

224 i1 = min(i0 + block, N) 

225 try: 

226 G = X[i0:i1] @ XT # (B, N) 

227 D2_blk = s[i0:i1, None] + s[None, :] - 2.0 * G 

228 except torch.cuda.OutOfMemoryError: # pragma: no cover 

229 torch.cuda.empty_cache() 

230 raise ValueError( 

231 'Tried to allocate too much GPU memory.' 

232 f' Try to reduce the value of block, e.g., to {block//2}.') 

233 K = torch.exp(-D2_blk * inv_two_sigma2) 

234 if not cumulative: 

235 row_sums[i0:i1] = K.sum(dim=1) 

236 else: 

237 K = torch.cumsum(K, dim=1) 

238 row_sums[i0:i1, 0:N] += torch.triu(K, diagonal=i0) 

239 

240 subvals = -torch.log(torch.clamp(row_sums / N_mat, min=eps))/N_mat 

241 entropy = torch.sum(subvals, dim=0) 

242 if not cumulative: 

243 det_entropy = float(entropy.detach().cpu().item()) 

244 else: 

245 det_entropy = entropy.detach().cpu().numpy() 

246 det_subvals = subvals.detach().cpu().numpy() 

247 return det_entropy, det_subvals