"""
Module for reading and writing QSite input files.
See QSiteInput class for more documentation.
Copyright Schrodinger, LLC. All rights reserved.
"""
#Contributors: Mike Beachy
import os
import shutil
from collections.abc import MutableMapping
import schrodinger.infra.mm as mm
from schrodinger.infra.mmim import MMIMArgList
from schrodinger.infra.mmim import MMIMDict
from schrodinger.utils.fileutils import force_remove
from schrodinger.utils.fileutils import splitext
_qmregion_int_default = -9999
_qmregion_str_default = ""
# Module-level constants for the levels of theory that we support
QM = "qm"
NDDO = "nddo"
[docs]class Cut(object):
"""
A class to handle representation and printing of general qm region cuts.
To avoid having to update individual attributes in the back end,
attributes are read-only. To modify attributes, make a new Cut instance
with the desired values. The clone method is provided as a convenience
method to ease creation of a Cut with modified values.
"""
# Attributes and their defaults specified as a list of tuples to
# maintain order in the output of _definedAttrs.
_attrs = [
("molid", _qmregion_int_default),
("chain", _qmregion_str_default),
("resnum", _qmregion_int_default),
("inscode", _qmregion_str_default),
("qmatom", _qmregion_str_default),
("mmatom", _qmregion_str_default),
("theory", _qmregion_str_default),
]
[docs] def __init__(self,
molid=None,
chain=None,
resnum=None,
inscode=None,
qmatom=None,
mmatom=None,
theory=QM):
"""
Initializer for a qmregion cut specification.
At least one of molid or qmatom to be defined.
:param molid: A molecule id.
:type molid: int
:param chain: A chain id.
:type chain: str
:param resnum: A residue number.
:type resnum: int
:param inscode: An insertion code.
:type inscode: str
:param qmatom: The name of an atom to place on the quantum
mechanical side of the cut.
:type qmatom: str
:param mmatom: The name of an atom to place on the molecular
mechanical side of the cut.
:type mmatom: str
:param theory: The theoretical method to use for this cut
("nddo" or "qm" and case-insensitive)
:type theory: str
"""
self._molid = molid
self._chain = chain
self._resnum = resnum
self._inscode = inscode
self._qmatom = qmatom
self._mmatom = mmatom
self._theory = theory
if self.molid is None and self.qmatom is None:
raise RuntimeError(
"Invalid qmregion cut specification: need at least molid or qmatom defined."
)
@property
def molid(self):
"""
Read-only access to the molid attribute.
"""
return self._molid
@property
def chain(self):
"""
Read-only access to the chain attribute.
"""
return self._chain
@property
def resnum(self):
"""
Read-only access to the resnum attribute.
"""
return self._resnum
@property
def inscode(self):
"""
Read-only access to the resnum attribute.
"""
return self._inscode
@property
def qmatom(self):
"""
Read-only access to the qmatom attribute.
"""
return self._qmatom
@property
def mmatom(self):
"""
Read-only access to the mmatom attribute.
"""
return self._mmatom
@property
def theory(self):
"""
Read-only access to the theory attribute.
"""
return self._theory.lower()
[docs] def clone(self, **kwargs):
"""
Create a copy of the current instance, modifying any attributes
provided as keywords.
"""
attrs = {}
for attr in self._definedAttrs():
attrs[attr] = getattr(self, "_" + attr)
attrs.update(kwargs)
return Cut(**attrs)
def _definedAttrs(self):
"""
Return the defined attributes for this cut.
"""
defined = []
for attr, default in self._attrs:
value = getattr(self, "_" + attr)
if value is not None and value != default:
defined.append(attr)
return defined
def __str__(self):
attrs = self._definedAttrs()
strings = []
for attr in attrs:
if attr == "resnum":
inscode = str(self._inscode)
strings.append("%*s%s" %
(6 - len(inscode), self._resnum, inscode))
elif attr == "inscode":
continue
else:
strings.append("%6s" % str(getattr(self, attr)))
return " ".join(strings)
def __repr__(self):
return "Cut(%s)" % ", ".join("%s=%s" % (attr, repr(getattr(self, attr)))
for attr in self._definedAttrs())
[docs]class HydrogenCap(object):
"""
A class to handle representation and printing of hydrogen caps for the
qm region.
"""
[docs] def __init__(self, qm=None, mm=None, distance=0.0, theory=QM):
"""
Initialize the hydrogen cap. A quantum mechanical atom and molecular
mechanical atom must always be specified.
:param qm: The name of the quantum mechanical atom.
:type qm: str
:param mm: The name of the molecular mechanical atom.
:type mm: str
:param distance: The hydrogen cap distance.
:type distance: float
:param theory: The theoretical model to use for the QM atom
(Either "nddo" or "qm", case-insensitive)
:type theory: str
"""
if qm is None or mm is None:
raise RuntimeError("Invalid qmregion hcap specification: "
"qm and mm must both be specified.")
self._qm = str(qm)
self._mm = str(mm)
self._distance = distance
self._theory = str(theory).lower()
@property
def qm(self):
"""
Read-only access to the _qm attribute.
"""
return self._qm
@property
def mm(self):
"""
Read-only access to the _mm attribute.
"""
return self._mm
@property
def theory(self):
"""
Read-only access to the _theory attribute.
"""
return self._theory
@property
def distance(self):
"""
Read-only access to the distance attribute.
"""
return self._distance
[docs] def clone(self, **kwargs):
"""
Create a copy of the current instance, modifying any attributes
provided as keywords.
"""
attrs = {}
attrs["qm"] = self._qm
attrs["mm"] = self._mm
attrs["theory"] = self._theory
attrs["distance"] = self._distance
attrs.update(kwargs)
return HydrogenCap(**attrs)
def __str__(self):
if self.distance == 0.0:
if self.theory == NDDO:
return "%8s %6s" % (self.qm, self.mm)
else:
return "%6s %6s" % (self.qm, self.mm)
else:
if self.theory == NDDO:
return "%8s %6s %8.6f" % (self.qm, self.mm, self.distance)
else:
return "%6s %6s %8.6f" % (self.qm, self.mm, self.distance)
def __repr__(self):
if self.distance:
dstring = ", distance=%s" % repr(self.distance)
else:
dstring = ""
if self.theory == NDDO:
return "HydrogenCap(nddo=%s, mm=%s%s, theory=%s)" % (repr(
self.qm), repr(self.mm), dstring, repr(self.theory))
else:
return "HydrogenCap(qm=%s, mm=%s%s)" % (repr(self.qm), repr(
self.mm), dstring)
class _GenDict(MutableMapping):
"""
A dictionary-like class for getting and setting keywords in the &gen
section of qsite input, which controls the Jaguar portion of the
calculation.
Note the 'default' method, which is non-standard for dictionaries. It
retrieves the default value for a keyword.
"""
def __init__(self, handle):
self.handle = handle
def default(self, key):
"""
Return the default value for the provided keyword.
"""
type_ = mm.mmjag_key_type(self.handle, key)
if type_ == mm.MMJAG_INT:
return mm.mmjag_key_int_def(self.handle, key)
elif type_ == mm.MMJAG_REAL:
return mm.mmjag_key_real_def(self.handle, key)
else:
return mm.mmjag_key_char_def(self.handle, key)
def __getitem__(self, key):
"""
Get a value for a &gen section keyword.
"""
type_ = mm.mmjag_key_type(self.handle, key)
if type_ == mm.MMJAG_INT:
return mm.mmjag_key_int_get(self.handle, key)
elif type_ == mm.MMJAG_REAL:
return mm.mmjag_key_real_get(self.handle, key)
else:
return mm.mmjag_key_char_get(self.handle, key)
def __setitem__(self, key, value):
"""
Set a value for the &gen section.
"""
if isinstance(value, int):
mm.mmjag_key_int_set(self.handle, key, value)
elif isinstance(value, float):
mm.mmjag_key_real_set(self.handle, key, value)
else:
mm.mmjag_key_char_set(self.handle, key, value)
return
def __delitem__(self, key):
"""
Delete a keyword from the &gen section.
"""
mm.mmjag_key_delete(self.handle, key)
return
def keys(self):
"""
Return the keys for the &gen section. Note that only keys with
non-default values will be returned.
"""
genkeys = []
for i in range(1, mm.mmjag_key_nondef_count(self.handle) + 1):
genkeys.append(mm.mmjag_key_nondef_get(self.handle, i))
return genkeys
def __len__(self):
return len(self.keys())
def __iter__(self):
return iter(self.keys())
class _MopacDict(MutableMapping):
"""
A dictionary-like class for getting and setting keywords in the &mopac
section of qsite input, which controls the Jaguar/MOPAC portion of the
calculation.
Note the 'default' method, which is non-standard for dictionaries. It
retrieves the default value for a keyword.
"""
def __init__(self, handle):
self.handle = handle
def default(self, key):
"""
Return the default value for the provided keyword.
"""
type_ = mm.mmjag_mopac_key_type(self.handle, key)
if type_ == mm.MMJAG_INT:
return mm.mmjag_mopac_key_int_def(self.handle, key)
elif type_ == mm.MMJAG_REAL:
return mm.mmjag_mopac_key_real_def(self.handle, key)
else:
return mm.mmjag_mopac_key_char_def(self.handle, key)
def __getitem__(self, key):
"""
Get a value for a &mopac section keyword.
"""
type_ = mm.mmjag_mopac_key_type(self.handle, key)
if type_ == mm.MMJAG_INT:
return mm.mmjag_mopac_key_int_get(self.handle, key)
elif type_ == mm.MMJAG_REAL:
return mm.mmjag_mopac_key_real_get(self.handle, key)
else:
return mm.mmjag_mopac_key_char_get(self.handle, key)
def __setitem__(self, key, value):
"""
Set a value for the &mopac section.
"""
if isinstance(value, int):
mm.mmjag_mopac_key_int_set(self.handle, key, value)
elif isinstance(value, float):
mm.mmjag_mopac_key_real_set(self.handle, key, value)
else:
mm.mmjag_mopac_key_char_set(self.handle, key, value, False)
return
def __delitem__(self, key):
"""
Delete a keyword from the &mopac section.
"""
mm.mmjag_mopac_key_delete(self.handle, key)
return
def keys(self):
"""
Return the keys for the &mopac section. Note that only keys with
non-default values will be returned.
"""
mopkeys = []
for i in range(1, mm.mmjag_mopac_key_nondef_count(self.handle) + 1):
mopkeys.append(mm.mmjag_mopac_key_nondef_get(self.handle, i))
return mopkeys
def __len__(self):
return len(self.keys())
def __iter__(self):
return iter(self.keys())
[docs]class QMRegion(object):
"""
A class to provide access to a &qmregion specification and the ability
to create one on the fly.
"""
_list_defaults = {
"molid": _qmregion_int_default,
"chain": _qmregion_str_default,
"resnum": _qmregion_int_default,
"inscode": _qmregion_str_default,
"qmatom": _qmregion_str_default,
"seatom": _qmregion_str_default,
"mmatom": _qmregion_str_default,
"hcap": 0,
"hcap_distance": 0.0,
"theory": QM,
}
[docs] def __init__(self, handle):
"""
:param handle: An MMIM handle.
:param type: int
"""
self.handle = handle
self.molid = MMIMArgList(handle, mm.MMIM_QSITE_QMMOL)
self.chain = MMIMArgList(handle, mm.MMIM_QSITE_QMCHN)
self.resnum = MMIMArgList(handle, mm.MMIM_QSITE_QMRES)
self.inscode = MMIMArgList(handle, mm.MMIM_QSITE_QMINST)
self.qmatom = MMIMArgList(handle, mm.MMIM_QSITE_QMATOM)
self.seatom = MMIMArgList(handle, mm.MMIM_QSITE_SEATOM)
self.mmatom = MMIMArgList(handle, mm.MMIM_QSITE_MMATOM)
self.hcap = MMIMArgList(handle, mm.MMIM_QSITE_HCAP)
self.hcap_distance = MMIMArgList(handle, mm.MMIM_QSITE_HCAPDIST)
self.theory = MMIMArgList(handle, mm.MMIM_QSITE_THEORY)
[docs] def __len__(self):
return mm.mmim_handle_get_indexed_arg_int_count(self.handle,
mm.MMIM_QSITE_QMMOL)
def __getitem__(self, index):
if self.hcap[index]:
return HydrogenCap(
qm=self.qmatom[index],
mm=self.mmatom[index],
distance=self.hcap_distance[index],
theory=self.theory[index],
)
else:
return Cut(
self.molid[index],
self.chain[index],
self.resnum[index],
self.inscode[index],
self.qmatom[index],
self.mmatom[index],
self.theory[index],
)
def __setitem__(self, index, qmregion):
if isinstance(qmregion, Cut):
for attr in qmregion._definedAttrs():
arglist = getattr(self, attr)
arglist[index] = getattr(qmregion, attr)
elif isinstance(qmregion, HydrogenCap):
index = self.__len__() - 1
self.hcap[index] = 1
self.theory[index] = qmregion.theory
if self.theory[index] == NDDO:
self.seatom[index] = qmregion.qm
else:
self.qmatom[index] = qmregion.qm
self.mmatom[index] = qmregion.mm
self.hcap_distance[index] = qmregion.distance
else:
raise Exception("QMRegion elements can only be set to instances "
"of Cut or HydrogenCap.")
def _growLists(self):
"""
Extend all MMIMArgLists by one element with their default values.
"""
for list_attr, default in QMRegion._list_defaults.items():
getattr(self, list_attr).append(default)
[docs] def modify(self, index, **kwargs):
"""
A method to modify attributes of a specific element in the qmregion
list in-place.
This method is provided because Cut and HydrogenCap instances have
read-only values.
"""
for k, v in kwargs.items():
arglist = getattr(self, k)
if v is None:
v = QMRegion._list_defaults[k]
arglist[index] = v
[docs] def append(self, qmregion):
"""
Add a new QM region to the list.
:param qmregion: A specification of a QM region cut or hydrogen cap.
:type qmregion: Cut or HydrogenCap
"""
if isinstance(qmregion, Cut) or isinstance(qmregion, HydrogenCap):
self._growLists()
index = self.__len__() - 1
self.__setitem__(index, qmregion)
else:
raise RuntimeError("The append method only takes instances of "
"Cut or HydrogenCap.")
#EOF