Source code for apbs.input_file.calculate.boundary_element

"""Parameters for a boundary-element polar solvation calculation."""
import logging
from math import log2
from .. import check
from .. import InputFile
from .generic import MobileIons, WriteMap


_LOGGER = logging.getLogger(__name__)


[docs]class Mesh(InputFile): """Boundary element mesh input. Objects can be initialized with dictionary/JSON/YAML data with the following keys: * ``software``: software to use when generating the mesh; see :func:`software` * ``solvent radius``: see :func:`solvent_radius`. * ``surface density``: see :func:`surface_density`. * ``surface method``: see :func:`surface_method`. """ def __init__(self, dict_=None, yaml=None, json=None): self._software = None self._solvent_radius = None self._surface_density = None self._surface_method = None super().__init__(dict_=dict_, yaml=yaml, json=json) @property def software(self) -> str: """Software for generating mesh. One of the following values: * ``nanoshaper``: `NanoShaper software <https://www.electrostaticszone.eu/downloads>` .. note:: The user is responsible for downloading the software and ensuring that it is available in their path. :raises TypeError: if not set to a string :raises ValueError: if set to an invalid value """ return self._software @software.setter def software(self, value): if not check.is_string(value): raise TypeError( f"Value {value} (type {type(value)}) is not a string." ) value = value.lower() if value not in ["nanoshaper"]: raise ValueError(f"{value} is not a valid value.") self._software = value @property def solvent_radius(self) -> float: """Radius of the solvent molecules. This parameter is used to define various solvent-related surfaces and volumes (see :func:`surface_method`). This value is usually set to 1.4 Å for a water-like molecular surface and set to 0 Å for a van der Waals surface. :raises ValueError: if value is not a non-negative number """ return self._solvent_radius @solvent_radius.setter def solvent_radius(self, value): if check.is_positive_semidefinite(value): self._solvent_radius = value else: raise ValueError(f"{value} is not a non-negative number.") @property def surface_density(self) -> float: """Number of points per area on surface. Units are number per Å\ :superscript:`2` and are used in calculation of surface terms (e.g., molecular surface, solvent accessible surface). This keyword is ignored when :func:`surface_radius` is 0.0 (e.g., for van der Waals surfaces) or if :func:`surface method` refers to splines. A typical value is 10.0. .. todo:: I am not sure how TABI uses this value with NanoShaper :raises ValueError: if value is not a positive number """ return self._surface_density @surface_density.setter def surface_density(self, value): if check.is_positive_definite(value): self._surface_density = value else: raise ValueError(f"{value} is not a positive number.") @property def surface_method(self) -> str: """Method to compute molecular surface (boundary). One of the following: * ``molecular surface``: The problem domain is divided into two spaces with the boundary defined as the interface between these spaces. The "free volume" space is defined by the union of solvent-sized spheres (see :func:`solvent_radius`) which do not overlap with the solute atoms. This free volume is assigned bulk solvent dielectric values. The complement of this space is assigned solute dielectric values. When the solvent radius is set to zero, this method corresponds to a van der Waals surface definition. The ion-accessibility coefficient is defined by an "inflated" van der Waals model. Specifically, the radius of each biomolecular atom is increased by the radius of the ion species (as specified with the :func:`ion` property). The problem domain is then divided into two spaces. The space inside the union of these inflated atomic spheres is assigned an ion-accessibility value of 0; the complement space is assigned the bulk ion accessibility value. See Connolly ML, J Appl Crystallography 16 548-558, 1983 (`10.1107/S0021889883010985 <https://doi.org/10.1107/S0021889883010985>`_). * ``skin``: Edelsbrunner molecular skin as defined in Edelsbrunner, H. “Deformable Smooth Surface Design.” Discrete and Computational Geometry 21, no. 1 (January 1, 1999): 87–115. DOI:`10.1007/PL00009412 <https://doi.org/10.1007/PL00009412>`_. :raises TypeError: if not set to string. :raises ValueError: if not set to valid value. """ return self._surface_method @surface_method.setter def surface_method(self, value): if not check.is_string(value): raise TypeError( f"Value {value} (type {type(value)} is not a string." ) value = value.lower() if value not in ["molecular surface", "skin"]: raise ValueError(f"{value} is not a valid value.") self._surface_method = value
[docs]class TABIParameters(InputFile): """Parameters for the TABI solver. "TABI" is the the Treecode-Accelerated Boundary Integral (TABI) solver. For more information, see the Geng & Krasny `2013 J Comput Phys paper <https://doi.org/10.1016/j.jcp.2013.03.056>`_. Objects can be initialized with dictionary/JSON/YAML data with the following keys: * ``maximum particles``: the maximum number of particles in a tree leaf; see :func:`maximum_particles`. * ``multipole acceptance criterion``: see :func:`multipole_acceptance_criterion`. * ``tree order``: see :func:`tree_order`. """ def __init__(self, dict_=None, yaml=None, json=None): self._maximum_particles = None self._multipole_acceptance_criterion = None self._tree_order = None super().__init__(dict_=dict_, yaml=yaml, json=json) @property def tree_order(self) -> int: """Specifies the order of the treecode multipole expansion. This is an integer that indicates the Taylor expansion order. Users can adjust the order for different accuracy. A typical choice for this parameter is 3. :raises TypeError: if not an integer :raises ValueError: if not a positive number. """ return self._tree_order @tree_order.setter def tree_order(self, value): if not isinstance(value, int): raise TypeError( f"Value {value} (type {type(value)} is not an integer." ) if not check.is_positive_definite(value): raise ValueError(f"Value {value} is not positive.") self._tree_order = value @property def multipole_acceptance_criterion(self) -> float: """The multipole acceptance criterion (MAC) controls the distance ratio at which the method uses direct summation or Taylor approximation (a particle-cluster interaction) to calculate the integral kernels. The MAC is related to cluster characteristics by :math:`\\frac{r_c}{R}\leqslant \\theta`, where :math:`r_c` is the cluster radius, :math:`R` is the distance of the particle to the cluster center, and :math:`0 < \\theta < 1`. If the this relationship is satisfied, then the Taylor approximation will be used instead of direct summation. A typical value for this parameter is 0.8. :raises ValueError: if not a positive number less than 1. """ return self._multipole_acceptance_criterion @multipole_acceptance_criterion.setter def multipole_acceptance_criterion(self, value): if not check.is_positive_definite(value): raise ValueError( f"Value {value} (type {type(value)}) is not a positive number." ) if value >= 1: raise ValueError(f"Value {value} is not less than 1.") self._multipole_acceptance_criterion = value @property def maximum_particles(self) -> int: """The maximum number of particles in the tree-code leaf. This controls leaf size in the process of building the tree structure. A typical value for this parameter is 500. :raises ValueError: if not set to a positive number :raise TypeError: if not set to an integer """ return self._maximum_particles @maximum_particles.setter def maximum_particles(self, value): if not check.is_positive_definite(value): raise ValueError(f"Value {value} is not a positive number.") if not isinstance(value, int): raise TypeError( f"Value {value} (type {type(value)} is not an integer." )
[docs]class BoundaryElement(InputFile): """Parameters for a boundary element linearized Poisson-Boltzmann polar solvation calculation. Boundary element methods offer the ability to focus numerical effort on a much smaller region of the problem domain: the interface between the molecule and the solvent. In this method, two coupled integral equations defined on the solute-solvent boundary define a mathematical relationship between the electrostatic surface potential and its normal derivative with a set of integral kernels consisting of Coulomb and screened Coulomb potentials with their normal derivatives. The boundary element method requires a surface triangulation, generated by a program such as `NanoShaper <https://www.electrostaticszone.eu/downloads>`_, on which to discretize the integral equations. Objects can be initialized with dictionary/JSON/YAML data with the following keys: * ``calculate energy``: see :func:`calculate_energy` * ``calculate forces``: see :func:`calculate_forces` * ``error tolerance``: solver error tolerance; see :func:`error_tolerance` * ``ions``: information about mobile ion species; see :func:`ions` * ``molecule``: alias to molecule for calculation; see :func:`molecule` * ``solute dielectric``: see :func:`solute_dielectric` * ``solvent dielectric``: see :func:`solvent_dielectric` * ``solver``: see :func:`solver` * ``solver parameters``: see func:`solver_parameters` * ``temperature``: see :func:`temperature` * ``mesh``: specify how the mesh is obtained; see :func:`mesh` * ``write atom potentials``: write out atom potentials; see :func:`write_atom_potentials` """ def __init__(self, dict_=None, yaml=None, json=None): self._calculate_energy = None self._calculate_forces = None self._error_tolerance = None self._ions = None self._mesh = None self._molecule = None self._solute_dielectric = None self._solvent_dielectric = None self._solver = None self._solver_parameters = None self._temperature = None self._write_atom_potentials = None super().__init__(dict_=dict_, yaml=yaml, json=json)
[docs] def validate(self): if self.solver == "tabi": if not isinstance(self.solver_parameters, TABIParameters): raise TypeError( f"Solver parameters (type {type(self.solver_parameters)})" f" are not from the TABIParameters class." ) raise NotImplementedError()
@property def write_atom_potentials(self) -> str: """Write out the electrostatic potential at each atom location. Write out text file with potential at center of atom in units of :math:`k_b \\, T \\, e_c^{-1}`. .. note:: These numbers are meaningless by themselves due to the presence of "self-energy" terms that are sensitive to grid spacing and position. These numbers should be evaluated with respect to a reference calculation: the potentials from that reference calculation should be subtracted from the target system. For example, one calculation might include a molecule with a heterogeneous dielectric coefficient and the reference system might be exactly the same system setup but with a homeogeneous dielectric coefficient. If the results from the reference calculation are substracted from the first calculation, then the result will be a physically meaningful reaction field potential. However, the results from the first and reference calculations are meaningless by themselves. :returns: path to text file for writing atom potential values. :raises TypeError: if not set to string """ return self._write_atom_potentials @write_atom_potentials.setter def write_atom_potentials(self, value): if not check.is_string(value): raise TypeError( f"Value {value} (type {type(value)}) is not a string." ) self._write_atom_potentials = value @property def temperature(self) -> float: """Temperature for the calculation in Kelvin. :raises ValueError: if not a positive number (no violations of the 3rd Law!) """ return self._temperature @temperature.setter def temperature(self, value): if check.is_positive_definite(value): self._temperature = value else: raise ValueError(f"{value} is not a positive number.") @property def solver_parameters(self) -> InputFile: """Parameters for specified solver. :returns: an object derived from :class:`InputFile` based on the :func:`solver` property: * ``tabi``: returns object of type :class:`TABIParameters` :raises TypeError: if object is not derived from :class:`InputFile` """ return self._solver_parameters @solver_parameters.setter def solver_parameters(self, value): if not isinstance(value, InputFile): raise TypeError( f"Value {value} (type {type(value)} is not derived from " f"InputClass." ) self._solver_parameters = value @property def solver(self) -> str: """Boundary element solver. Allowed values are one of the following: * ``tabi``: the Treecode-Accelerated Boundary Integral (TABI) solver. For more information, see the Geng & Krasny `2013 J Comput Phys paper <https://doi.org/10.1016/j.jcp.2013.03.056>`_. Each value must be accompanied by the corresponding solver parameters, set via :func:`solver_parameters`. :raises TypeError: if not set to string. :raises ValueError: if set to invalid string. """ return self._solver @solver.setter def solver(self, value): if not check.is_string(value): raise TypeError( f"Value {value} (type {type(value)}) is not a string." ) value = value.lower() if value not in ["tabi"]: raise ValueError(f"Value {value} is not an allowed value.") self._solver = value @property def solvent_dielectric(self) -> float: """Solvent dielectric. 78.5 is a good choice for water at 298 K. :returns: a floating point number greater than or equal to one :raises TypeError: if not a number :raises ValueError: if not greater than or equal to 1 """ return self._solvent_dielectric @solvent_dielectric.setter def solvent_dielectric(self, value): if not check.is_positive_definite(value): raise TypeError(f"Value {value} is not a positive number.") if value < 1: raise ValueError(f"Value {value} is not >= 1.") self._solvent_dielectric = value @property def solute_dielectric(self) -> float: """Solute dielectric. The dielectric value of a solute is often chosen using the following rules of thumb: * 1: only used to compare calculation results with non-polarizable molecular simulation * 2-4: "molecular" dielectric value; used when conformational degrees of freedom are modeled explicitly * 4-8: used to mimic sidechain libration and other small-scale conformational degrees of freedom * 8-12: used to model larger-scale sidechain rearrangement * 20-40: used to model larger-scale macromolecular conformational changes and/or water penetration into interior of molecule .. note:: What does the continuum dielectric value of a non-continuum molecule mean? Hard to say -- this approximation can be very difficult to interpret and can significant affect your results. :returns: a floating point number greater than or equal to one :raises TypeError: if not a number :raises ValueError: if not greater than or equal to 1 """ return self._solute_dielectric @solute_dielectric.setter def solute_dielectric(self, value): if not check.is_positive_definite(value): raise TypeError(f"Value {value} is not a positive number.") if value < 1: raise ValueError(f"Value {value} is not >= 1.") self._solute_dielectric = value @property def molecule(self) -> str: """Specify which molecule to use for calculation. :returns: alias for molecule read (see :ref:`read_new_input`) :raises TypeError: if not set to a string """ return self._molecule @molecule.setter def molecule(self, value): if not check.is_string(value): raise TypeError( f"Value {value} (type {type(value)}) is not a string." ) self._molecule = value @property def mesh(self) -> Mesh: """Parameters for the boundary mesh. :raises TypeError: if not set to :class:`Mesh` object. """ return self._mesh @mesh.setter def mesh(self, value): if not isinstance(value, Mesh): raise TypeError( f"Value {value} (type {type(value)} is not class Mesh." ) self._mesh = value @property def ions(self) -> MobileIons: """Descriptions of mobile ion species. :raises TypeError: if not set to a :class:`Ions` object """ return self._ions @ions.setter def ions(self, value): if not isinstance(value, MobileIons): raise TypeError( f"Value {value} (type {type(value)} is not an Ions object." ) self._ions = value @property def error_tolerance(self) -> float: """Relative error tolerance for iterative solver. If not specified, the default value is :const:`ERROR_TOLERANCE`. :raises TypeError: if not set to positive number :raises ValueError: if not set to number less than 1 """ return self._error_tolerance @error_tolerance.setter def error_tolerance(self, value): if not check.is_positive_definite(value): raise TypeError(f"Value {value} is not a positive number.") if value >= 1.0: raise ValueError(f"Value {value} is not less than 1.0.") self._error_tolerance = value @property def calculate_forces(self) -> bool: """Indicate whether forces should be calculated. :raises TypeError: if not Boolean """ return self._calculate_forces @calculate_forces.setter def calculate_forces(self, value): if check.is_bool(value): self._calculate_forces = value else: raise ValueError(f"{value} is not Boolean.") @property def calculate_energy(self) -> bool: """Indicate whether energy should be calculated. :raises TypeError: if not Boolean """ return self._calculate_energy @calculate_energy.setter def calculate_energy(self, value): if check.is_bool(value): self._calculate_energy = value else: raise ValueError(f"{value} is not Boolean.")