from typing import Optional
import warnings
import numpy as np
from pydantic import Field
from . import base, charge
from .constraint import (ESPSurfaceConstraintMatrix,
SparseGlobalConstraintMatrix)
class BaseRespOptions(base.Model):
"""Base RESP options"""
restraint_slope: float = Field(default=0.1,
description="Tightness of hyperbolic penalty at its minimum")
restrained_fit: bool = Field(default=True,
description="Perform a restrained fit")
exclude_hydrogens: bool = Field(
default=True,
description="if True, exclude hydrogens from restraint",
)
convergence_tolerance: float = Field(
default=1e-6,
description="threshold for convergence",
)
max_iter: int = Field(
default=500,
description="max number of iterations to solve constraint matrices",
)
[docs]class RespOptions(BaseRespOptions):
restraint_height_stage_1: float = Field(
default=0.0005,
description=("scale factor of asymptote limits of hyperbola, "
"in the stage 1 fit"),
)
restraint_height_stage_2: float = Field(
default=0.001,
description=("scale factor of asymptote limits of hyperbola, "
"in the stage 2 fit"),
)
stage_2: bool = True
@property
def _base_kwargs(self):
return {k: getattr(self, k) for k in BaseRespOptions.__fields__}
[docs]class RespCharges(BaseRespOptions):
"""Self-contained class to solve RESP charges with charge constraints"""
restraint_height: float = Field(default=0.0005,
description="scale factor of asymptote limits of hyperbola")
_restrained_charges: Optional[np.ndarray] = None
_unrestrained_charges: Optional[np.ndarray] = None
_matrix: Optional[SparseGlobalConstraintMatrix] = None
charge_constraints: charge.MoleculeChargeConstraints
surface_constraints: ESPSurfaceConstraintMatrix
def __repr__(self) -> str:
respstr = (f"restraint_height={self.restraint_height}, restraint_slope={self.restraint_slope}, "
f"restrained_fit={self.restrained_fit}, "
f"exclude_hydrogens={self.exclude_hydrogens}")
constr = f"{self.charge_constraints.n_constraints} charge constraints"
if self._unrestrained_charges is None:
chgstr = "charges not computed"
else:
with np.printoptions(precision=5):
chgstr = (f"unrestrained_charges={self.unrestrained_charges}, "
f"restrained_charges={self.restrained_charges}")
return f"<{self._clsname}({respstr}) with {constr}; {chgstr}>"
def __post_init__(self, **kwargs):
super().__post_init__(**kwargs)
self._matrix = SparseGlobalConstraintMatrix.from_constraints(
surface_constraints=self.surface_constraints,
charge_constraints=self.charge_constraints,
exclude_hydrogens=self.exclude_hydrogens,
)
[docs] def solve(self):
self._matrix._solve()
self._unrestrained_charges = self._matrix._charges.flatten()
if not self.restrained_fit or not self.restraint_height:
return
n_iter = 0
b2 = self.restraint_slope ** 2
while (self._matrix.charge_difference > self.convergence_tolerance
and n_iter < self.max_iter):
self._matrix._iter_solve(self.restraint_height, self.restraint_slope, b2)
n_iter += 1
self._matrix._iter_solve(self.restraint_height, self.restraint_slope, b2)
if self._matrix.charge_difference > self.convergence_tolerance:
warnings.warn("Charge fitting did not converge to "
f"convergence_tolerance={self.convergence_tolerance} "
f"with max_iter={self.max_iter}")
self._restrained_charges = self._matrix._charges.flatten()
@property
def restrained_charges(self):
if self._restrained_charges is None:
return None
return self.charge_constraints._index_array(self._restrained_charges)
@property
def unrestrained_charges(self):
if self._unrestrained_charges is None:
return None
return self.charge_constraints._index_array(self._unrestrained_charges)
@property
def _charges(self):
if self._restrained_charges is not None:
return self._restrained_charges
return self._unrestrained_charges
@property
def charges(self):
charges_ = self._charges
if charges_ is None:
return
return self.charge_constraints._index_array(self._charges)