Source code for MDAnalysis.analysis.dielectric
# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding: utf-8 -*-
# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
#
# MDAnalysis --- https://www.mdanalysis.org
# Copyright (c) 2006-2017 The MDAnalysis Development Team and contributors
# (see the file AUTHORS for the full list of names)
#
# Released under the GNU Public Licence, v2 or any higher version
#
# Please cite your use of MDAnalysis in published work:
#
# R. J. Gowers, M. Linke, J. Barnoud, T. J. E. Reddy, M. N. Melo, S. L. Seyler,
# D. L. Dotson, J. Domanski, S. Buchoux, I. M. Kenney, and O. Beckstein.
# MDAnalysis: A Python package for the rapid analysis of molecular dynamics
# simulations. In S. Benthall and S. Rostrup editors, Proceedings of the 15th
# Python in Science Conference, pages 102-109, Austin, TX, 2016. SciPy.
# doi: 10.25080/majora-629e541a-00e
#
# N. Michaud-Agrawal, E. J. Denning, T. B. Woolf, and O. Beckstein.
# MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations.
# J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787
#
r"""
Dielectric --- :mod:`MDAnalysis.analysis.dielectric`
===========================================================
:Authors: Mattia Felice Palermo, Philip Loche
:Year: 2022
:Copyright: GNU Public License v3
"""
import numpy as np
from MDAnalysis.units import constants, convert
from MDAnalysis.analysis.base import AnalysisBase
from MDAnalysis.due import due, Doi
from MDAnalysis.exceptions import NoDataError
due.cite(Doi("10.1080/00268978300102721"),
         description="Dielectric analysis",
         path="MDAnalysis.analysis.dielectric",
         cite_module=True)
del Doi
[docs]class DielectricConstant(AnalysisBase):
    r"""
    Computes the average dipole moment
    .. math::
        \boldsymbol M = \sum_i q_i \boldsymbol r_i
    where :math:`q_i` is the charge and :math:`\boldsymbol r_i` the position of
    atom :math:`i` in the given :class:`MDAnalysis.core.groups.AtomGroup`.
    Also, the static dielectric constant
    .. math::
        \varepsilon = 1 + \frac{\langle M^2 \rangle - \langle M \rangle^2}
                              {3 \varepsilon_ 0 V k_B T}
    is calculated for a system in tin foil boundary conditions, which is
    the usual case if electrostatics are handled with a Ewald summation
    technique. See [Neumann1983]_ for details on the derivation.
    Parameters
    ----------
    atomgroup : MDAnalysis.core.groups.AtomGroup
      Atomgroup on which the analysis is executed
    temperature : float
      Temperature (Kelvin) at which the system has been simulated
    make_whole : bool
      Make molecules whole; If the input already contains whole molecules
      this can be disabled to gain speedup
    verbose : bool
      Show detailed progress of the calculation
    Attributes
    ----------
    results.M : numpy.ndarray
      Directional dependant dipole moment
      :math:`\langle \boldsymbol M \rangle` in :math:`eÅ`.
    results.M2 : numpy.ndarray
      Directional dependant squared dipole moment
      :math:`\langle \boldsymbol M^2 \rangle` in :math:`(eÅ)^2`
    results.fluct : float
      Directional dependant dipole moment fluctuation
      :math:`\langle \boldsymbol M^2 \rangle - \langle \boldsymbol M \rangle^2`
      in :math:`(eÅ)^2`
    results.eps : numpy.ndarray
      Directional dependant static dielectric constant
    results.eps_mean : float
      Static dielectric constant
    Example
    -------
    Create a :class:`DielectricConstant` instance by supplying an
    :class:`~MDAnalysis.core.groups.AtomGroup`,
    then use the :meth:`run` method::
      import MDAnalysis as mda
      from MDAnalysis.analysis.dielectric import DielectricConstant
      from MDAnalysisTests.datafiles import PSF_TRICLINIC, DCD_TRICLINIC
      # Load a pure water system
      universe = mda.Universe(PSF_TRICLINIC, DCD_TRICLINIC)
      diel = DielectricConstant(universe.atoms)
      diel.run()
      print(diel.results)
    The static dielectric constant of the provided atomgroup is saved
    within the :class:`~MDAnalysis.analysis.base.Results` attribute.
    .. versionadded:: 2.1.0
    """
    def __init__(self, atomgroup, temperature=300, make_whole=True, **kwargs):
        super(DielectricConstant, self).__init__(atomgroup.universe.trajectory,
                                                 **kwargs)
        self.atomgroup = atomgroup
        self.temperature = temperature
        self.make_whole = make_whole
    def _prepare(self):
        if not hasattr(self.atomgroup, "charges"):
            raise NoDataError("No charges defined given atomgroup.")
        if not np.allclose(self.atomgroup.total_charge(compound='fragments'),
                           0.0, atol=1E-5):
            raise NotImplementedError("Analysis for non-neutral systems or"
                                      " systems with free charges are not"
                                      " available.")
        self.volume = 0
        self.results.M = np.zeros(3)
        self.results.M2 = np.zeros(3)
        self.results.fluct = np.zeros(3)
        self.results.eps = np.zeros(3)
        self.results.eps_mean = 0
    def _single_frame(self):
        if self.make_whole:
            self.atomgroup.unwrap()
        self.volume += self.atomgroup.universe.trajectory.ts.volume
        M = np.dot(self.atomgroup.charges, self.atomgroup.positions)
        self.results.M += M
        self.results.M2 += M * M
    def _conclude(self):
        self.results.M /= self.n_frames
        self.results.M2 /= self.n_frames
        self.volume /= self.n_frames
        self.results.fluct = self.results.M2 - self.results.M * self.results.M
        self.results.eps = self.results.fluct / (
              convert(constants["Boltzman_constant"], "kJ/mol", "eV") *
              self.temperature * self.volume * constants["electric_constant"])
        self.results.eps_mean = self.results.eps.mean()
        self.results.eps += 1
        self.results.eps_mean += 1