import logging
from itertools import combinations
import numpy as np
import qcelemental as qcel
from . import addIntcos, bend, oofp, stre, tors
from .exceptions import OptError
from .printTools import print_array_string, print_mat_string
from .v3d import are_collinear
from . import log_name
logger = logging.getLogger(f"{log_name}{__name__}")
[docs]
class Frag:
def __init__(self, Z, geom, masses, intcos=None, frozen=False):
"""Group of bonded atoms
Parameters
----------
Z : list[int]
atomic numbers
geom : np.ndarray
(nat, 3) cartesian geometry
masses : list[float]
atomic masses
intcos : list[Simple], optional
internal coordinates (stretches, bends, etch...)
"""
self._Z = Z
self._geom = geom
self._masses = masses
self._frozen = frozen
self._intcos = []
if intcos:
self._intcos = intcos
def __str__(self):
np.set_printoptions(suppress=True, floatmode="fixed", sign=" ")
s = f"\n\t {'Z (Atomic Numbers)':<20} {'Masses':^20} {'Geom':^40}"
strip = lambda x: str(x).replace("[", "").replace("]", "")
print_vals = [
f"\n\t {self._Z[i]: ^20f} {self._masses[i]:^20f} {strip(self._geom[i]):^40}" for i in range(self.natom)
]
s += "".join(print_vals)
s += "\n\n\t - Coordinate - - BOHR/RAD - - ANG/DEG -"
for x in self._intcos:
s += "\n\t%-18s=%17.6f%19.6f" % (x, x.q(self._geom), x.q_show(self._geom))
s += "\n"
np.set_printoptions()
return s
[docs]
def to_dict(self):
d = {
"Z": self._Z.copy(),
"geom": self._geom.copy(),
"masses": self._masses.copy(),
"frozen": self._frozen,
"intcos": [i.to_dict() for i in self._intcos],
}
return d
[docs]
@classmethod
def from_dict(cls, D):
if "Z" not in D or "geom" not in D or "masses" not in D:
raise OptError("Missing required Z/geom/masses in dict input")
Z = D["Z"]
geom = D["geom"]
masses = D["masses"]
frozen = D.get("frozen", False)
if "intcos" in D: # class constructor (cls), e.g., stre.Stre
intcos = []
for i in D["intcos"]:
clc = str.lower(i["type"]) + "." + i["type"]
intcos.append(eval(clc).from_dict(i))
else:
intcos = None
return cls(Z, geom, masses, intcos, frozen)
@property
def natom(self):
return len(self._Z)
@property
def Z(self):
return self._Z
@property
def geom(self):
return self._geom
@property
def masses(self):
return self._masses
@property
def intcos(self):
return self._intcos
@property
def frozen(self):
return self._frozen
@property
def num_intcos(self):
return len(self._intcos)
[docs]
def q(self):
return [intco.q(self.geom) for intco in self._intcos]
[docs]
def q_array(self):
return np.asarray(self.q())
[docs]
def q_show(self):
return [intco.q_show(self.geom) for intco in self._intcos]
[docs]
def q_show_array(self):
return np.asarray(self.q_show())
[docs]
def print_intcos(self):
intcos_report = "\tInternal Coordinate Values\n"
intcos_report += "\n\t - Coordinate - - BOHR/RAD - - ANG/DEG -\n"
for coord in self._intcos:
intcos_report += "\t%-18s=%17.6f%19.6f\n" % (
coord,
coord.q(self._geom),
coord.q_show(self._geom),
)
intcos_report += "\n"
logger.info(intcos_report)
[docs]
def connectivity_from_distances(self):
return addIntcos.connectivity_from_distances(self._geom, self._Z)
[docs]
def add_intcos_from_connectivity(self, connectivity=None):
if connectivity is None:
connectivity = self.connectivity_from_distances()
addIntcos.add_intcos_from_connectivity(connectivity, self._intcos, self._geom)
self.add_h_bonds()
[docs]
def add_auxiliary_bonds(self, connectivity=None):
if connectivity is None:
connectivity = self.connectivity_from_distances()
addIntcos.add_auxiliary_bonds(connectivity, self._intcos, self._geom, self._Z)
[docs]
def add_cartesian_intcos(self):
addIntcos.add_cartesian_intcos(self._intcos, self._geom)
[docs]
def add_h_bonds(self):
"""Prepend h_bonds because that's where optking 2 places them"""
h_bonds = addIntcos.add_h_bonds(self.geom, self.Z, self.natom)
for h_bond in h_bonds:
if stre.Stre(h_bond.A, h_bond.B) in self._intcos:
self._intcos.pop(self._intcos.index(stre.Stre(h_bond.A, h_bond.B)))
self._intcos = h_bonds + self._intcos # prepend internal coordinates
[docs]
def show_geom(self):
geometry = ""
Ang = self._geom * qcel.constants.bohr2angstroms
for i in range(self._geom.shape[0]):
geometry += "\t%5s%15.10f%15.10f%15.10f\n" % (
qcel.periodictable.to_E(self._Z[i]),
Ang[i, 0],
Ang[i, 1],
Ang[i, 2],
)
geometry += "\n"
return geometry
[docs]
def get_atom_symbol_list(self):
frag_atom_symbol_list = []
for i in range(self._geom.shape[0]):
frag_atom_symbol_list.append(qcel.periodictable.to_E(self._Z[i]))
return frag_atom_symbol_list
[docs]
def Bmat(self):
B = np.zeros((self.num_intcos, 3 * self.natom))
for i, intco in enumerate(self._intcos):
intco.DqDx(self.geom, B[i])
return B
[docs]
def fix_bend_axes(self):
for intco in self._intcos:
if isinstance(intco, bend.Bend):
intco.fix_bend_axes(self.geom)
[docs]
def unfix_bend_axes(self):
for intco in self._intcos:
if isinstance(intco, bend.Bend):
intco.unfix_bend_axes()
[docs]
def freeze(self):
for intco in self._intcos:
intco.freeze()
self._frozen = True
[docs]
def update_dihedral_orientations(self):
"""Update orientation of each dihedrals/tors coordinate
This saves an indicator if dihedral is slightly less than pi,
or slighly more than -pi. Subsequently, computation of values
can be greater than pi or less than -pi to enable computation
of Delta(q) when q passed through pi.
"""
for intco in self._intcos:
if isinstance(intco, tors.Tors) or isinstance(intco, oofp.Oofp):
intco.update_orientation(self.geom)
[docs]
def is_atom(self):
if self.natom == 1:
return True
else:
return False
[docs]
def is_linear(self):
if self.natom in [1, 2]: # lets tentatively call an atom linear here
return True
else:
xyz = self.geom
for (i, j, k) in combinations(range(self.natom), r=3):
if not are_collinear(xyz[i], xyz[j], xyz[k]):
return False
return True