Source code for torchpme.tuning.pme

import math
from itertools import product
from typing import Any
from warnings import warn

import torch

from ..calculators import PMECalculator
from .tuner import GridSearchTuner, TuningErrorBounds


[docs] def tune_pme( charges: torch.Tensor, cell: torch.Tensor, positions: torch.Tensor, cutoff: float, neighbor_indices: torch.Tensor, neighbor_distances: torch.Tensor, full_neighbor_list: bool = False, prefactor: float = 1.0, exponent: int = 1, nodes_lo: int = 3, nodes_hi: int = 7, mesh_lo: int = 2, mesh_hi: int = 7, accuracy: float = 1e-3, ) -> tuple[float, dict[str, Any], float]: r""" Find the optimal parameters for :class:`torchpme.PMECalculator`. For the error formulas are given `elsewhere <https://doi.org/10.1063/1.470043>`_. Note the difference notation between the parameters in the reference and ours: .. math:: \alpha = \left(\sqrt{2}\,\mathrm{smearing} \right)^{-1} :param charges: torch.tensor of shape ``(len(positions, 1))`` containing the atomic (pseudo-)charges :param cell: torch.tensor of shape ``(3, 3)``, where ``cell[i]`` is the i-th basis vector of the unit cell :param positions: torch.tensor of shape ``(N, 3)`` containing the Cartesian coordinates of the ``N`` particles within the supercell. :param cutoff: float, cutoff distance for the neighborlist :param neighbor_indices: torch.tensor with the ``i,j`` indices of neighbors for which the potential should be computed in real space. :param neighbor_distances: torch.tensor with the pair distances of the neighbors for which the potential should be computed in real space. :param full_neighbor_list: If set to :py:obj:`True`, a "full" neighbor list is expected as input. This means that each atom pair appears twice. If set to :py:obj:`False`, a "half" neighbor list is expected. :param prefactor: electrostatics prefactor; see :ref:`prefactors` for details and common values. :param exponent: :math:`p` in :math:`1/r^p` potentials, currently only :math:`p=1` is supported :param nodes_lo: Minimum number of interpolation nodes :param nodes_hi: Maximum number of interpolation nodes :param mesh_lo: Controls the minimum number of mesh points along the shortest axis, :math:`2^{mesh_lo}` :param mesh_hi: Controls the maximum number of mesh points along the shortest axis, :math:`2^{mesh_hi}` :param accuracy: Recomended values for a balance between the accuracy and speed is :math:`10^{-3}`. For more accurate results, use :math:`10^{-6}`. :return: Tuple containing a float of the optimal smearing for the :class: `CoulombPotential`, a dictionary with the parameters for :class:`PMECalculator` and a float of the optimal cutoff value for the neighborlist computation, and the timing of this set of parameters. Example ------- >>> import torch To allow reproducibility, we set the seed to a fixed value >>> _ = torch.manual_seed(0) >>> positions = torch.tensor([[0.0, 0.0, 0.0], [0.5, 0.5, 0.5]]) >>> charges = torch.tensor([[1.0], [-1.0]]) >>> cell = torch.eye(3) >>> neighbor_distances = torch.tensor( ... [0.9381, 0.9381, 0.8246, 0.9381, 0.8246, 0.8246, 0.6928], ... ) >>> neighbor_indices = torch.tensor( ... [[0, 1], [0, 1], [0, 1], [0, 1], [0, 1], [0, 1], [0, 1]] ... ) >>> smearing, parameter, timing = tune_pme( ... charges, ... cell, ... positions, ... cutoff=1.0, ... neighbor_distances=neighbor_distances, ... neighbor_indices=neighbor_indices, ... accuracy=1e-1, ... ) """ # if cell is 0 `min_dimension` will be zero as well; we raise a propper error later min_dimension = float(torch.min(torch.linalg.norm(cell, dim=1))) params = [ { "interpolation_nodes": interpolation_nodes, "mesh_spacing": 2 * min_dimension / (2**ns - 1), } for interpolation_nodes, ns in product( range(nodes_lo, nodes_hi + 1), range(mesh_lo, mesh_hi + 1) ) ] tuner = GridSearchTuner( charges=charges, cell=cell, positions=positions, cutoff=cutoff, exponent=exponent, neighbor_indices=neighbor_indices, neighbor_distances=neighbor_distances, full_neighbor_list=full_neighbor_list, prefactor=prefactor, calculator=PMECalculator, error_bounds=PMEErrorBounds(charges=charges, cell=cell, positions=positions), params=params, ) smearing = tuner.estimate_smearing(accuracy) errs, timings = tuner.tune(accuracy) # There are multiple errors below the accuracy, return the one with the shortest # calculation time. The timing of those parameters leading to an higher error # than the accuracy are set to infinity if any(err < accuracy for err in errs): return smearing, params[timings.index(min(timings))], min(timings) # No parameter meets the requirement, return the one with the smallest error, and # throw a warning warn( f"No parameter meets the accuracy requirement.\n" f"Returning the parameter with the smallest error, which is {min(errs)}.\n", stacklevel=1, ) return smearing, params[errs.index(min(errs))], timings[errs.index(min(errs))]
[docs] class PMEErrorBounds(TuningErrorBounds): r""" Error bounds for :class:`torchpme.PMECalculator`. .. note:: The :func:`torchpme.tuning.pme.PMEErrorBounds.forward` method takes floats as the input, in order to be in consistency with the rest of the package -- these parameters are always ``float`` but not ``torch.Tensor``. This design, however, prevents the utilization of ``torch.autograd`` and other ``torch`` features. To take advantage of these features, one can use the :func:`torchpme.tuning.pme.PMEErrorBounds.err_rspace` and :func:`torchpme.tuning.pme.PMEErrorBounds.err_kspace`, which takes ``torch.Tensor`` as parameters. :param charges: torch.tensor of shape ``(len(positions, 1))`` containing the atomic (pseudo-)charges :param cell: torch.tensor of shape ``(3, 3)``, where ``cell[i]`` is the i-th basis vector of the unit cell :param positions: torch.tensor of shape ``(N, 3)`` containing the Cartesian coordinates of the ``N`` particles within the supercell. Example ------- >>> import torch >>> positions = torch.tensor( ... [[0.0, 0.0, 0.0], [0.4, 0.4, 0.4]], dtype=torch.float64 ... ) >>> charges = torch.tensor([[1.0], [-1.0]], dtype=torch.float64) >>> cell = torch.eye(3, dtype=torch.float64) >>> error_bounds = PMEErrorBounds(charges, cell, positions) >>> print( ... error_bounds( ... smearing=1.0, mesh_spacing=0.5, cutoff=4.4, interpolation_nodes=3 ... ) ... ) tensor(0.0011, dtype=torch.float64) """ def __init__( self, charges: torch.Tensor, cell: torch.Tensor, positions: torch.Tensor ): super().__init__(charges, cell, positions) self.volume = torch.abs(torch.det(cell)) self.sum_squared_charges = (charges**2).sum() self.prefac = 2 * self.sum_squared_charges / math.sqrt(len(positions)) self.cell_dimensions = torch.linalg.norm(cell, dim=1)
[docs] def err_kspace( self, smearing: torch.Tensor, mesh_spacing: torch.Tensor, interpolation_nodes: torch.Tensor, ) -> torch.Tensor: """ The Fourier space error of PME. :param smearing: see :class:`torchpme.PMECalculator` for details :param mesh_spacing: see :class:`torchpme.PMECalculator` for details :param interpolation_nodes: see :class:`torchpme.PMECalculator` for details """ actual_spacing = self.cell_dimensions / ( 2 * self.cell_dimensions / mesh_spacing + 1 ) h = torch.prod(actual_spacing) ** (1 / 3) i_n_factorial = torch.exp(torch.lgamma(interpolation_nodes + 1)) RMS_phi = [None, None, 0.246, 0.404, 0.950, 2.51, 8.42] return ( self.prefac * torch.pi**0.25 * (6 * (1 / 2**0.5 / smearing) / (2 * interpolation_nodes + 1)) ** 0.5 / self.volume ** (2 / 3) * (2**0.5 / smearing * h) ** interpolation_nodes / i_n_factorial * torch.exp( interpolation_nodes * (torch.log(interpolation_nodes / 2) - 1) / 2 ) * RMS_phi[interpolation_nodes - 1] )
[docs] def err_rspace(self, smearing: torch.Tensor, cutoff: torch.Tensor) -> torch.Tensor: """ The real space error of PME. :param smearing: see :class:`torchpme.PMECalculator` for details :param cutoff: see :class:`torchpme.PMECalculator` for details """ return ( self.prefac / torch.sqrt(cutoff * self.volume) * torch.exp(-(cutoff**2) / 2 / smearing**2) )
[docs] def error( self, cutoff: float, smearing: float, mesh_spacing: float, interpolation_nodes: float, ) -> torch.Tensor: r""" Calculate the error bound of PME. .. math:: \text{Error}_{\text{total}} = \sqrt{\text{Error}_{\text{real space}}^2 + \text{Error}_{\text{Fourier space}}^2 :param smearing: if its value is given, it will not be tuned, see :class:`torchpme.PMECalculator` for details :param mesh_spacing: if its value is given, it will not be tuned, see :class:`torchpme.PMECalculator` for details :param cutoff: if its value is given, it will not be tuned, see :class:`torchpme.PMECalculator` for details :param interpolation_nodes: The number ``n`` of nodes used in the interpolation per coordinate axis. The total number of interpolation nodes in 3D will be ``n^3``. In general, for ``n`` nodes, the interpolation will be performed by piecewise polynomials of degree ``n - 1`` (e.g. ``n = 4`` for cubic interpolation). Only the values ``3, 4, 5, 6, 7`` are supported. """ smearing = torch.tensor(smearing) mesh_spacing = torch.tensor(mesh_spacing) cutoff = torch.tensor(cutoff) interpolation_nodes = torch.tensor(interpolation_nodes) return torch.sqrt( self.err_rspace(smearing, cutoff) ** 2 + self.err_kspace(smearing, mesh_spacing, interpolation_nodes) ** 2 )