Source code for apbs.input_file.calculate.finite_element

"""Parameters for a finite-element polar solvation calculation."""
import logging

# from typing import Type
from .. import check
from .. import InputFile
from .generic import MobileIons, UseMap, WriteMap


_LOGGER = logging.getLogger(__name__)


[docs]class FiniteElement(InputFile): """Parameters for a finite-difference polar solvation Poisson-Boltzmann calculation. Objects can be initialized with dictionary/JSON/YAML data with the following keys: * ``a priori refinement``: *a priori* mesh refinement strategy; see :func:`a_priori_refinement` * ``boundary condition``: :func:`boundary_condition` * ``calculate energy``: see :func:`calculate_energy` * ``calculate forces``: see :func:`calculate_forces` * ``charge discretization``: method used to map charges onto the grid; see :func:`charge_discretization` * ``domain length``: see :func:`domain_length` * ``error estimation``: how error is calculated for driving refinement; see :func:`error_estimation` * ``error-based refinement``: method for choosing which simplices to refine based on error; see :func:`error_based_refinement` * ``equation``: what version of the Poisson-Boltzmann equation to solve; see :func:`equation` * ``ions``: information about mobile ion species; see :func:`ions` * ``initial mesh resolution``: target resolution of initial mesh; see :func:`initial_mesh_resolution` * ``initial mesh vertices``: target number of vertices in the initial finite element mesh; see :func:`initial_mesh_vertices` * ``maximum refinement iterations``: number of times to perform the solve-estimate-refine iteration of the finite element solver; see :func:`maximum_refinement_iterations` * ``maximum vertices``: target for maximum number of vertices in mesh; see :func:`maximum_vertices` * ``molecule``: alias to molecule for calculation; see :func:`molecule` * ``no-op``: determine whether the solver should be run; see :func:`noop` * ``solute dielectric``: see :func:`solute_dielectric` * ``solvent dielectric``: see :func:`solvent_dielectric` * ``solvent radius``: see :func:`solvent_radius` * ``surface method``: see :func:`surface_method` * ``surface spline window``: see :func:`surface_spline_window` * ``temperature``: see :func:`temperature` * ``use maps``: use input map for one or more properties of the system; see :func:`use_maps` * ``write atom potentials``: write out atom potentials; see :func:`write_atom_potentials` * ``write maps``: write out one or more properties of the system to a map; see :func:`write_maps` """ def __init__(self, dict_=None, yaml=None, json=None): self._a_priori_refinement = None self._boundary_condition = None self._calculate_energy = None self._calculate_forces = None self._charge_discretization = None self._domain_length = None self._error_based_refinement = None self._error_tolerance = None self._equation = None self._ions = None self._initial_mesh_resolution = None self._initial_mesh_vertices = None self._maximum_refinement_iterations = None self._maximum_vertices = None self._molecule = None self._solute_dielectric = None self._solvent_dielectric = None self._solvent_radius = None self._surface_method = None self._surface_spline_window = None self._temperature = None self._use_maps = [] self._write_atom_potentials = None self._write_maps = [] super().__init__(dict_=dict_, yaml=yaml, json=json)
[docs] def from_dict(self, input_): """Populate object from dictionary. :param dict input_: dictionary with input data. :raises KeyError: if dictionary missing keys. """ self.a_priori_refinement = input_["a priori refinement"] self.boundary_condition = input_["boundary condition"] self.calculate_energy = input_["calculate energy"] self.calculate_forces = input_["calculate forces"] self.charge_discretization = input_["charge discretization"] self.domain_length = input_["domain length"] self.error_based_refinement = input_["error based refinement"] self.error_tolerance = input_["error tolerance"] self.equation = input_["equation"] self.ions = MobileIons(dict_=input_["ions"]) self.initial_mesh_resolution = input_["initial mesh resolution"] self.initial_mesh_vertices = input_["initial mesh vertices"] self.maximum_refinement_iterations = input_[ "maximum refinement iterations" ] self.maximum_vertices = input_["maximum vertices"] self.molecule = input_["molecule"] self.solute_dielectric = input_["solute dielectric"] self.solvent_dielectric = input_["solvent dielectric"] self.solvent_radius = input_["solvent radius"] self.surface_method = input_["surface method"] self.surface_spline_window = input_["surface spline window"] self.temperature = input_["temperature"] self.use_maps = [UseMap(dict_=dict_) for dict_ in input_["use maps"]] self.write_atom_potentials = input_["write atom potentials"] self.write_maps = [ WriteMap(dict_=dict_) for dict_ in input_["write maps"] ]
[docs] def to_dict(self) -> dict: """Dump dictionary from object.""" dict_ = {} dict_["a priori refinement"] = self.a_priori_refinement dict_["boundary condition"] = self.boundary_condition dict_["calculate energy"] = self.calculate_energy dict_["calculate forces"] = self.calculate_forces dict_["charge discretization"] = self.charge_discretization dict_["domain length"] = self.domain_length dict_["error based refinement"] = self.error_based_refinement dict_["error tolerance"] = self.error_tolerance dict_["equation"] = self.equation dict_["ions"] = self.ions.to_dict() dict_["initial mesh resolution"] = self.initial_mesh_resolution dict_["initial mesh vertices"] = self.initial_mesh_vertices dict_[ "maximum refinement iterations" ] = self.maximum_refinement_iterations dict_["maximum vertices"] = self.maximum_vertices dict_["molecule"] = self.molecule dict_["solute dielectric"] = self.solute_dielectric dict_["solvent dielectric"] = self.solvent_dielectric dict_["solvent radius"] = self.solvent_radius dict_["surface method"] = self.surface_method dict_["surface spline window"] = self.surface_spline_window dict_["temperature"] = self.temperature dict_["use maps"] = [map_.to_dict() for map_ in self.use_maps] dict_["write atom potentials"] = self.write_atom_potentials dict_["write maps"] = [map_.to_dict() for map_ in self.write_maps] return dict_
[docs] def validate(self): """Validate this object. Assumes that all attributes have been set via setters. :raises ValueError: if object is invalid. """ for map_ in self.use_maps + self.write_maps: map_.validate() self.ions.validate() if self.initial_mesh_vertices > self.maximum_vertices: raise ValueError( f"Initial mesh vertices {self.initial_mesh_vertices} setting " f"is greater than maximum mesh vertices setting " f"{self.maximum_vertices}." )
@property def write_maps(self) -> list: """Write out maps related to computed properties. See :class:`WriteMap` for more information. :raises TypeError: if set to wrong type """ return self._write_maps @write_maps.setter def write_maps(self, value): if not check.is_list(value): raise TypeError( f"Value {value} (type {type(value)}) is not a list." ) for elem in value: if not isinstance(elem, WriteMap): raise TypeError( f"Value {elem} (type {type(elem)}) is not a WriteMap " f"object." ) self._write_maps.append(elem) @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 use_maps(self) -> list: """Information for (optionally) using maps read into APBS. :returns: list of :class:`UseMap` objects :raises TypeError: if not a list of :class:`UseMap` objects """ return self._use_maps @use_maps.setter def use_maps(self, value): if not check.is_list(value): raise TypeError( f"Value {value} (type {type(value)}) is not a list." ) for elem in value: if not isinstance(elem, UseMap): raise TypeError( f"List contains element {elem} of type {type(elem)}." ) self._use_maps.append(elem) @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 surface_spline_window(self) -> float: """Window for spline-based surface definitions (not needed otherwise). This is the distance (in Å) over which the spline transitions from the solvent dielectric value to the solute dielectric value. A typical value is 0.3 Å. :returns: positive number :raises TypeError: if not a positive number """ return self._surface_spline_window @surface_spline_window.setter def surface_spline_window(self, value): if not check.is_positive_definite(value): raise TypeError(f"Value {value} is not a positive number.") self._surface_spline_window = value @property def surface_method(self) -> str: """Method used to defined solute-solvent interface. One of the following values: * ``molecular surface``: The dielectric coefficient is defined based on a molecular surface definition. The problem domain is divided into two 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>`_). * ``smoothed molecular surface``: The dielectric and ion-accessibility coefficients are defined as for the ``molecular surface`` (see above). However, they are then "smoothed" by a 9-point harmonic averaging to somewhat reduce sensitivity to the grid setup. See Bruccoleri et al. J Comput Chem 18 268-276, 1997 (`10.1007/s00214-007-0397-0 <http://dx.doi.org/10.1007/s00214-007-0397-0>`_). * ``cubic spline``: The dielectric and ion-accessibility coefficients are defined by a cubic-spline surface as described by Im et al, Comp Phys Commun 111 (1-3) 59-75, 1998 (`10.1016/S0010-4655(98)00016-2 <https://doi.org/10.1016/S0010-4655(98)00016-2>`_). The width of the dielectric interface is controlled by the :func:`spline_window` property. These spline-based surface definitions are very stable with respect to grid parameters and therefore ideal for calculating forces. However, they require substantial reparameterization of the force field; interested users should consult Nina et al, Biophys Chem 78 (1-2) 89-96, 1999 (`10.1016/S0301-4622(98)00236-1 <http://dx.doi.org/10.1016/S0301-4622(98)00236-1>`_). Additionally, these surfaces can generate unphysical results with non-zero ionic strengths. * ``septic spline``: The dielectric and ion-accessibility coefficients are defined by a 7th order polynomial. This surface definition has characteristics similar to the cubic spline, but provides higher order continuity necessary for stable force calculations with atomic multipole force fields (up to quadrupole). :raises TypeError: if not set to a string :raises ValueError: if set to invalid 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", "smoothed molecular surface", "cubic spline", "septic spline", ]: raise ValueError(f"Value {value} is an invalid surface method.") self._surface_method = 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 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 maximum_vertices(self) -> int: """Maximum number of vertices produced in solve-estimate-refine iterations. The solve-estimate-refine loop will continue until the mesh has more than this number of vertices or :func:`maximum_refinement_iterations` has been reached. .. todo:: add validation step to make sure this number is greater than or equal to the initial number of vertices. :raises TypeError: if not sent to positive integer """ return self._maximum_vertices @maximum_vertices.setter def maximum_vertices(self, value): if not isinstance(value, int): raise TypeError(f"{value} (type {type(value)}) is not an integer.") if value < 1: raise TypeError(f"{value} is not a positive integer.") self._maximum_vertices = value @property def maximum_refinement_iterations(self) -> int: """Maximum number of solve-estimate-refine iterations. The solve-estimate-refine loop will continue until this number of iterations is reached or the mesh has more than :func:`maximum_vertices` vertices. :raises TypeError: if not sent to positive integer """ return self._maximum_refinement_iterations @maximum_refinement_iterations.setter def maximum_refinement_iterations(self, value): if not isinstance(value, int): raise TypeError(f"{value} (type {type(value)}) is not an integer.") if value < 1: raise TypeError(f"{value} is not a positive integer.") self._maximum_refinement_iterations = value @property def initial_mesh_vertices(self) -> int: """Target number of vertices in initial mesh. Initial refinement will continue until this number of vertices is exceeded or :func:`initial_mesh_resolution` is reached. :raises TypeError: if not set to a positive integer """ return self._initial_mesh_vertices @initial_mesh_vertices.setter def initial_mesh_vertices(self, value): if not isinstance(value, int): raise TypeError(f"{value} (type {type(value)}) is not an integer.") if value < 1: raise TypeError(f"{value} is not a positive integer.") self._initial_mesh_vertices = value @property def initial_mesh_resolution(self) -> float: """Target spacing (in Å) of initial mesh. Initial refinement will continue until this target spacing is met or until :func:`initial_mesh_vertices` is exceeded. :raises TypeError: if not set to a positive number """ return self._initial_mesh_resolution @initial_mesh_resolution.setter def initial_mesh_resolution(self, value): if not check.is_positive_definite(value): raise ValueError(f"Value {value} is not a positive number.") self._initial_mesh_resolution = value @property def ions(self) -> MobileIons: """Descriptions of mobile ion species. :raises TypeError: if not set to a :class:`Ions` object """ _LOGGER.debug(f"self.ions = {self._ions}.") if self._ions is None: raise TypeError("Ions are set to None.") 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 domain_length(self) -> list: """Length of rectangular prism domain. :returns: list of non-zero lengths for x, y, and z directions. :raises TypeError: if not list of positive definite numbers. :raises IndexError: if list not length 3 """ return self._domain_length @domain_length.setter def domain_length(self, value): if not check.is_list(value): raise TypeError(f"{value} (type {type(value)}) is not a list.") if len(value) != 3: raise IndexError(f"{value} does not have length 3.") for elem in value: if not check.is_positive_definite(elem): raise TypeError(f"{elem} is not a positive number.") self._domain_length = value @property def a_priori_refinement(self) -> str: """Strategy for refining the initial very coarse 8-tetrahedron initial finite element to a resolution suitable for the main solve-estimate-refine iteration. Allowed values include: * ``geometric``: geometry-based refinement at molecular surface and charges * ``uniform``: uniform refinement of the mesh :raises ValueError: if not set to allowed value :raises TypeError: if not set to string """ return self._a_priori_refinement @a_priori_refinement.setter def a_priori_refinement(self, value): if not check.is_string(value): raise TypeError(f"{value} (type {type(value)}) is not a string.") value = value.lower() if value not in ["geometric", "uniform"]: raise ValueError(f"{value} is not a recognized value.") self._a_priori_refinement = value @property def boundary_condition(self) -> str: """Boundary condition for Poisson-Boltzmann equation. This property can have one of the following values: * ``zero``: Dirichlet condition where the potential at the boundary is set to zero. This condition is not commonly used and can result in large errors if used inappropriately. * ``single sphere``: Dirichlet condition where the potential at the boundary is set to the values prescribed by a Debye-Hückel model for a single sphere with a point charge, dipole, and quadrupole. The sphere radius in this model is set to the radius of the biomolecule and the sphere charge, dipole, and quadrupole are set to the total moments of the protein. This condition works best when the boundary is sufficiently far (multiple Debye lengths) from the biomolecule. * ``multiple sphere``: Dirichlet condition where the potential at the boundary is set to the values prescribed by a Debye-Hückel model for multiple, non-interacting spheres with a point charges. The radii of the non-interacting spheres are set to the atomic radii of and the sphere charges are set to the atomic charges. This condition works better than ``single sphere`` for closer boundaries but can be very slow for large biomolecules. * ``focus`` :c:var:`alias`: Dirichlet condition where the potential at the boundary is set to the values computed by a previous (usually lower-resolution) PB calculation with alias :c:var:`alias`. All of the boundary points should lie within the domain of the previous calculation for best accuracy; if any boundary points lie outside, their values are computed using the ``single sphere`` Debye-Hückel boundary condition (see above). :raises ValueError: if set to an invalid boundary type :raise IndexError: if an insufficient number of words are present """ return self._boundary_condition @boundary_condition.setter def boundary_condition(self, value): words = value.split() if words[0] == "zero": self._boundary_condition = "zero" elif words[0] == "single" and words[1] == "sphere": self._boundary_condition = "single sphere" elif words[0] == "multiple" and words[1] == "sphere": self._boundary_condition = "multiple sphere" elif words[0] == "focus": self._boundary_condition = " ".join(words) else: raise ValueError(f"Unknown boundary condition: {value}.") @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.") @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 charge_discretization(self) -> str: """The method by which the biomolecular point charges (i.e., Dirac delta functions) by which charges are mapped to the grid used for the finite difference calculation. As we are attempting to model delta functions, the support (domain) of these discretized charge distributions is always strongly dependent on the grid spacing. The following types of discretization are supported: * ``linear``: Traditional trilinear interpolation (linear splines). The charge is mapped onto the nearest-neighbor grid points. Resulting potentials are very sensitive to grid spacing, length, and position. * ``cubic``: Cubic B-spline discretization. The charge is mapped onto the nearest- and next-nearest-neighbor grid points. Resulting potentials are somewhat less sensitive (than ``linear``) to grid spacing, length, and position. * ``quintic``: Quintic B-spline discretization. Similar to ``cubic``, except the charge/multipole is additionally mapped to include next-next-nearest neighbors (125 grid points receive charge density). :raises TypeError: if not set to string :raises ValueError: if not one of the allowed values above """ return self._charge_discretization @charge_discretization.setter def charge_discretization(self, value): if not check.is_string(value): raise TypeError(f"{value} (type {type(value)}) is not a string.") value = value.lower() if value not in ["linear", "cubic", "quintic"]: raise ValueError(f"{value} is not an allowed value.") self._charge_discretization = value @property def error_based_refinement(self) -> str: """Specify error-based refinement strategy for simplices. Can be assigned one of the following values: * ``global``: this refines simplices until the global error is less than the amount specified by :func:`error_tolerance`. * ``simplex``: this refines simplices until the per-simplex error is less than the amount specified by :func:`error_tolerance`. * ``fraction``: this refines the specified fraction of simplices with the largest per-simplex error. The fraction is specified by :func:`error_tolerance`. :raises TypeError: if not set to string :raises ValueError: if not set to allowed value """ return self._error_based_refinement @error_based_refinement.setter def error_based_refinement(self, value): if not check.is_string(value): raise TypeError(f"{value} (type {type(value)} is not a string.") value = value.lower() if value not in ["global", "simplex", "fraction"]: raise ValueError(f"Invalid value: {value}.") self._error_based_refinement = value @property def error_tolerance(self) -> float: """Error tolerance for error-based refinement of simplices. The meaning of this property changes based on the setting of :func:`error_based_refinement`. raises TypeError: if not a positive number. """ return self._error_tolerance @error_tolerance.setter def error_tolerance(self, value): if not check.is_positive_definite(value): raise TypeError(f"{value} is not a positive number.") self._error_tolerance = value @property def equation(self) -> str: """Specifies which version of the Poisson-Boltzmann equation (PBE) to solve: * Most users should use one of these: * ``linearized pbe`` * ``nonlinear pbe`` * These versions are experimental and unstable: * ``linearized regularized pbe`` * ``nonlinear regularized pbe`` .. todo:: Confirm that all of these forms of the PBE work for FEM. :raises TypeError: if not set to a string. :raises ValueError: if set to an invalid value """ return self._equation @equation.setter def equation(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 [ "linearized pbe", "nonlinear pbe", "linearized regularized pbe", "nonlinear regularized pbe", ]: raise ValueError(f"Value {value} is invalid.") self._equation = value