Source code for calorine.tools.entropy

from warnings import warn
import numpy as np

try:
    import torch
    torch_available = True
except ImportError:  # pragma: no cover
    torch_available = False
    # If pytorch is not available, we still need a torch object
    # to satisfy type hints etc. Otherwise the file will not run.

    class _DummyTensor:
        """Fallback Tensor placeholder."""
        pass

    class _DummyDevice:
        """Fallback device placeholder."""
        def __init__(self, *args, **kwargs):
            pass

    class _DummyDType:
        """Fallback dtype placeholder."""
        pass

    class _DummyTorchModule:
        Tensor = _DummyTensor
        device = _DummyDevice
        dtype = _DummyDType

        # Provide common dtype names you might reference
        float32 = _DummyDType()
        float64 = _DummyDType()

    torch = _DummyTorchModule()

try:
    import warnings as _warnings
    with _warnings.catch_warnings():
        _warnings.filterwarnings('ignore', message='IProgress not found')
        from tqdm.auto import tqdm
except ImportError:  # pragma: no cover
    def tqdm(x):
        return x


[docs] def get_entropy( descriptors: np.ndarray | torch.Tensor, width: float, use_tqdm: bool = True, device: str | torch.device = 'cuda', block: int = 1024, dtype: torch.dtype = torch.float32, eps: float = 1e-12, ) -> tuple[float, np.ndarray]: r""" Computes an estimate for the information entropy :math:`H(\mathbf{X})` of a set of descriptors :math:`\mathbf{X}`. The estimate is described in [Nat. Comm. **16**, 4014 (2025)](https://doi.org/10.1038/s41467-025-59232-0) and given by .. math:: \mathcal{H}(\{\mathbf{X}\}) = -\frac{1}{n} \sum_{i=1}^{n} p_i where .. math:: p_i = \log \left[ \frac{1}{n} \sum_{j=1}^{n} K_h(\mathbf{X}_i, \mathbf{X}_j) \right] with a Gaussian kernel .. math:: K_h(\mathbf{X}_i, \mathbf{X}_j) = \exp\!\left( -\frac{\lVert \mathbf{X}_i - \mathbf{X}_j \rVert^2}{2h^2}. \right) The calculation is done via torch if the library has been installed, and numpy otherwise. When using torch the calculation is run via CUDA. The latter behavior can be controlled using the :attr:`device` argument. Parameters ---------- descriptors The set of descriptors :math:`\mathbf{X}` for which to evaluate to the entropy. Typically each row corresponds to one atom and the columns correspond to the different descriptor components. width Width :math:`h` of the Gaussian kernel. use_tqdm Use `tqdm <https://tqdm.github.io/>`_ to show a progress bar. Note that this requires tqdm to be installed. block In order to limit the memory needs, the kernel density estimate matrix is handled in blocks. This parameter controls the size of each block. Smaller numbers imply a smaller memory footprint. eps Smallest (absolute) permissible value. device Device to use for calculation. The documentation of [`torch.device`](https://docs.pytorch.org/docs/stable/tensor_attributes.html#torch.device) provides more information. Only used when pytorch is available. dtype Floating point precision used for the computation. The documentation of [`torch.dtype`](https://docs.pytorch.org/docs/stable/tensor_attributes.html) provides an overview. Only used when pytorch is available. Returns ------- A tuple comprising the total entropy :math:`H(\mathbf{X})` and the entropy contributions :math:`p_i` from each row in the input descriptor matrix. """ if torch_available: res = _get_entropy_torch(descriptors, width, use_tqdm, block, eps, device, dtype) return res else: # pragma: no cover warn('Using the numpy implementation.' ' Install torch in order to use GPUs and achieve a considerable speed-up.') res = _get_entropy_numpy(descriptors, width, use_tqdm, block, eps) return res
def _get_entropy_numpy( descriptors: np.ndarray, width: float, use_tqdm: bool, block: int, eps: float, ) -> float: """Compute the informational entropy using numpy. See get_entropy for documentation. """ X = np.asarray(descriptors, dtype=np.float64) N, d = X.shape s = np.sum(X * X, axis=1) # (N,) inv_two_sigma2 = 1.0 / (2.0 * width * width) row_sums = np.zeros(N, dtype=np.float64) for i0 in tqdm(range(0, N, block), leave=False): i1 = min(i0 + block, N) # D2[i0:i1, :] = s[i0:i1,None] + s[None,:] - 2*X[i0:i1]@X.T G = X[i0:i1] @ X.T # (B,N) D2_blk = (s[i0:i1, None] + s[None, :] - 2.0 * G) row_sums[i0:i1] = np.sum(np.exp(-D2_blk * inv_two_sigma2), axis=1) subvals = -np.log(np.clip(row_sums / N, min=eps)) subvals /= N entropy = np.sum(subvals) return -float(entropy), subvals def _get_entropy_torch( descriptors: np.ndarray | torch.Tensor, width: float, use_tqdm: bool, block: int, eps: float, device: str | torch.device = 'cuda', dtype: torch.dtype = torch.float32, ) -> tuple[float, np.ndarray]: """Compute the informational entropy using torch. See get_entropy for documentation. """ with torch.no_grad(): # Move data to device if not torch.cuda.is_available() and str(device) == 'cuda': # pragma: no cover device = 'cpu' X = torch.as_tensor(descriptors, dtype=dtype, device=device) N, d = X.shape inv_two_sigma2 = 1.0 / (2.0 * width * width) # Precompute norms once s = (X * X).sum(dim=1) # (N,) row_sums = torch.zeros(N, dtype=dtype, device=device) # Block over rows i; each block computes K[i0:i1, :].sum(-1) # D2 = s[i] + s[j] - 2 * X[i] @ X[j]^T (formed in blocks to save memory) XT = X.T # reuse in matmuls for i0 in tqdm(range(0, N, block), leave=False): i1 = min(i0 + block, N) try: G = X[i0:i1] @ XT # (B, N) D2_blk = s[i0:i1, None] + s[None, :] - 2.0 * G except torch.cuda.OutOfMemoryError: # pragma: no cover torch.cuda.empty_cache() raise ValueError( 'Tried to allocate too much GPU memory.' f' Try to reduce the value of block, e.g., to {block//2}.') row_sums[i0:i1] = torch.exp(-D2_blk * inv_two_sigma2).sum(dim=1) subvals = -torch.log(torch.clamp(row_sums / N, min=eps)) subvals /= N entropy = torch.sum(subvals) return -float(entropy.detach().cpu().item()), subvals.detach().cpu().numpy()