def linear_atoms(geo, gra=None, tol=5.): """ find linear atoms in a geometry (atoms with 180 degree bond angle) :param geo: the geometry :type geo: automol geometry data structure :param gra: the graph describing connectivity; if None, a connectivity graph will be generated using default distance thresholds :type gra: automol graph data structure :param tol: the tolerance threshold for linearity, in degrees :type tol: float :rtype: tuple(int) """ gra = connectivity_graph(geo) if gra is None else gra ngb_idxs_dct = automol.graph.atoms_neighbor_atom_keys(gra) lin_idxs = [] for idx in range(count(geo)): nidxs = ngb_idxs_dct[idx] if len(nidxs) >= 2: for nidx1, nidx2 in itertools.combinations(nidxs, 2): ang = central_angle(geo, nidx1, idx, nidx2, degree=True) if numpy.abs(ang - 180.) < tol: lin_idxs.append(idx) lin_idxs = tuple(lin_idxs) return lin_idxs
def hydrogen_bonded_idxs(geo, dist_thresh=5.3, angle_thresh=1.92, grxn=None): """ Compare bond lengths in structure to determine if there is a hydrogen bond. :param geo: geometry object :type geo: geo object (tuple of tuples) :param grxn: reaction object (geo indexing) :type grxn: automol.reac.Reaction object :param dist_thresh: cutoff value for hbond length (Bohr) :type dist_thresh: float :param angle_thresh: cutoff value for hbond angle (Radian) :type angle_thresh: float :rtype: tuple """ # Initialize the hydrogen bond list to None hydrogen_bond = None if count(geo) > 1: # Get the forming/breaking bond idxs if possible if grxn is not None: frm_bnd_keys = automol.graph.ts.forming_bond_keys( grxn.forward_ts_graph) brk_bnd_keys = automol.graph.ts.breaking_bond_keys( grxn.forward_ts_graph) rxn_keys = set() for key in frm_bnd_keys: rxn_keys = rxn_keys | key for key in brk_bnd_keys: rxn_keys = rxn_keys | key rxn_h_idxs = tuple(rxn_keys) else: rxn_h_idxs = () # Get all potential indices for HB interactions gra = graph(geo) dist_mat = distance_matrix(geo) adj_atm_dct = automol.graph.atoms_neighbor_atom_keys(gra) h_idxs = automol.graph.atom_keys(gra, sym='H') acceptor_idxs = list( automol.graph.resonance_dominant_radical_atom_keys(gra)) acceptor_idxs.extend(list(automol.graph.atom_keys(gra, sym='O'))) # Loop over indices, ignoring H-idxs in reacting bonds hb_idxs = tuple(idx for idx in h_idxs if idx not in rxn_h_idxs) for h_idx in hb_idxs: for acceptor_idx in acceptor_idxs: donor_idx = list(adj_atm_dct[h_idx])[0] if acceptor_idx in adj_atm_dct[donor_idx]: continue if dist_mat[h_idx][acceptor_idx] < dist_thresh: ang = central_angle(geo, donor_idx, h_idx, acceptor_idx) if ang > angle_thresh: hydrogen_bond = ( donor_idx, h_idx, acceptor_idx, ) dist_thresh = dist_mat[h_idx][acceptor_idx] return hydrogen_bond
def _perpendicular_direction(idxs): """ find a nice perpendicular direction for a series of linear atoms """ triplets = [] for idx in idxs: for n1idx in ngb_idxs_dct[idx]: for n2idx in ngb_idxs_dct[n1idx]: if n2idx != idx: ang = central_angle(geo, idx, n1idx, n2idx, degree=True) if numpy.abs(ang - 180.) > tol: triplets.append((idx, n1idx, n2idx)) if triplets: idx1, idx2, idx3 = min(triplets, key=lambda x: x[1:]) xyz1, xyz2, xyz3 = map(xyzs.__getitem__, (idx1, idx2, idx3)) r12 = util.vec.unit_direction(xyz1, xyz2) r23 = util.vec.unit_direction(xyz2, xyz3) direc = util.vec.orthogonalize(r12, r23, normalize=True) else: if len(idxs) > 1: idx1, idx2 = idxs[:2] else: idx1, = idxs idx2, = ngb_idxs_dct[idx1] xyz1, xyz2 = map(xyzs.__getitem__, (idx1, idx2)) r12 = util.vec.unit_direction(xyz1, xyz2) for i in range(3): disp = numpy.zeros((3, )) disp[i] = -1. alt = numpy.add(r12, disp) direc = util.vec.unit_perpendicular(r12, alt) if numpy.linalg.norm(direc) > 1e-2: break return direc
def ring_angles_reasonable(geo, ring_atoms, thresh=ATHRESH): """ Assess whether any of the angles of the prospective rings of a geometry are bent at unphysical angles. :param geo: molecular geometry :type geo: automol.geom object :param rng_atoms: idxs for atoms inside rings :type rng_atoms: list :param thresh: angular threshold for large angles :type thresh: float :rtype: bool """ condition = True for i, _ in enumerate(ring_atoms): _atoms = [ring_atoms[i], ring_atoms[i - 1], ring_atoms[i - 2]] cangle = central_angle(geo, *_atoms, degree=False) if cangle < thresh: condition = False break return condition
def _ts_compare(ref_zma, zma, zrxn): """ Perform a series of checks to assess the viability of a transition state geometry prior to saving """ # Initialize viable viable = True # Get the bond dists and calculate the distance of bond being formed ref_geo = automol.zmat.geometry(ref_zma) cnf_geo = automol.zmat.geometry(zma) grxn = automol.reac.relabel_for_geometry(zrxn) frm_bnd_keys = automol.reac.forming_bond_keys(grxn) brk_bnd_keys = automol.reac.breaking_bond_keys(grxn) cnf_dist_lst = [] ref_dist_lst = [] bnd_key_lst = [] cnf_ang_lst = [] ref_ang_lst = [] for frm_bnd_key in frm_bnd_keys: frm_idx1, frm_idx2 = list(frm_bnd_key) cnf_dist = distance(cnf_geo, frm_idx1, frm_idx2) ref_dist = distance(ref_geo, frm_idx1, frm_idx2) cnf_dist_lst.append(cnf_dist) ref_dist_lst.append(ref_dist) bnd_key_lst.append(frm_bnd_key) for brk_bnd_key in brk_bnd_keys: brk_idx1, brk_idx2 = list(brk_bnd_key) cnf_dist = distance(cnf_geo, brk_idx1, brk_idx2) ref_dist = distance(ref_geo, brk_idx1, brk_idx2) cnf_dist_lst.append(cnf_dist) ref_dist_lst.append(ref_dist) bnd_key_lst.append(brk_bnd_key) for frm_bnd_key in frm_bnd_keys: for brk_bnd_key in brk_bnd_keys: for frm_idx in frm_bnd_key: for brk_idx in brk_bnd_key: if frm_idx == brk_idx: idx2 = frm_idx idx1 = list(frm_bnd_key - frozenset({idx2}))[0] idx3 = list(brk_bnd_key - frozenset({idx2}))[0] cnf_ang = central_angle(cnf_geo, idx1, idx2, idx3) ref_ang = central_angle(ref_geo, idx1, idx2, idx3) cnf_ang_lst.append(cnf_ang) ref_ang_lst.append(ref_ang) # print('bnd_key_list', bnd_key_lst) # print('conf_dist', cnf_dist_lst) # print('ref_dist', ref_dist_lst) # print('conf_angle', cnf_ang_lst) # print('ref_angle', ref_ang_lst) # Set the maximum allowed displacement for a TS conformer max_disp = 0.6 # better to check for bond-form length in bond scission with ring forming if 'addition' in grxn.class_: max_disp = 0.8 if 'abstraction' in grxn.class_: # this was 1.4 - SJK reduced it to work for some OH abstractions max_disp = 1.0 # Check forming bond angle similar to ini config if 'elimination' not in grxn.class_: for ref_angle, cnf_angle in zip(ref_ang_lst, cnf_ang_lst): if abs(cnf_angle - ref_angle) > .44: print('angle', ref_angle, cnf_angle) viable = False symbs = symbols(cnf_geo) lst_info = zip(ref_dist_lst, cnf_dist_lst, bnd_key_lst) for ref_dist, cnf_dist, bnd_key in lst_info: if 'add' in grxn.class_ or 'abst' in grxn.class_: bnd_key1, bnd_key2 = min(list(bnd_key)), max(list(bnd_key)) symb1 = symbs[bnd_key1] symb2 = symbs[bnd_key2] if bnd_key in frm_bnd_keys: # Check if radical atom is closer to some atom # other than the bonding atom cls = automol.zmat.base.is_atom_closest_to_bond_atom( zma, bnd_key2, cnf_dist) if not cls: print('distance', ref_dist, cnf_dist) print(' - Radical atom now has a new nearest neighbor') viable = False # check forming bond distance if abs(cnf_dist - ref_dist) > max_disp: print('distance', ref_dist, cnf_dist) viable = False # Check distance relative to equi. bond equi_bnd = dict_.values_by_unordered_tuple(bnd.LEN_DCT, (symb1, symb2), fill_val=0.0) displace_from_equi = cnf_dist - equi_bnd dchk1 = abs(cnf_dist - ref_dist) > 0.1 dchk2 = displace_from_equi < 0.2 if dchk1 and dchk2: print(cnf_dist, equi_bnd) viable = False else: # check forming/breaking bond distance # if abs(cnf_dist - ref_dist) > 0.4: # max disp of 0.4 causes problems for bond scission w/ ring forming # not sure if setting it to 0.3 will cause problems for other cases if abs(cnf_dist - ref_dist) > 0.3: print('distance', ref_dist, cnf_dist) viable = False return viable
def change_zmatrix_row_values(geo, idx, dist=None, idx1=None, ang=None, idx2=None, dih=None, idx3=None, angstrom=True, degree=True, gra=None): """ Change the z-matrix coordinates of a given atom, shifting those connected to it accordingly. :param geo: molecular geometry :type geo: automol geometry data structure :param idx: the atom to be shifted :type idx: int :param dist: the distance coordinate; if None, use the current distance :type dist: float :param idx1: the atom used to specify the distance coordinate :type idx1: int :param ang: the central angle coordinate :type ang: float :param idx2: the atom used to specify the central angle coordinate :type idx2: int :param dih: the dihedral angle coordinate :type ang: float :param idx3: the atom used to specify the dihedral angle coordinate :type idx3: int :param angstrom: are distances in angstrom? If not, assume bohr. :type angstrom: bool :param degree: are angles in degrees? If not, assume radians. :type degree: bool :param gra: molecular graph for tracking connectivity (will be generated if None) :type gra: automol molecular graph data structure :rtype: automol geometry data structure """ gra = gra if gra is not None else connectivity_graph(geo) dist = dist if dist is not None or idx1 is None else ( distance(geo, idx, idx1, angstrom=angstrom)) ang = ang if ang is not None or idx2 is None else ( central_angle(geo, idx, idx1, idx2, degree=degree)) dih = dih if dih is not None or idx3 is None else ( dihedral_angle(geo, idx, idx1, idx2, idx3, degree=degree)) dist = dist if not angstrom else dist * phycon.ANG2BOHR ang = ang if not degree else ang * phycon.DEG2RAD dih = dih if not degree else dih * phycon.DEG2RAD tol = 2. * phycon.DEG2RAD lin = numpy.pi idxs = automol.graph.branch_atom_keys(gra, idx1, [idx1, idx]) # Note that the coordinates will change throughout, but the change # shouldn't affect the operations so we can work in terms of the inital set xyzs = coordinates(geo) if idx1 is not None: # First, adjust the distance vec0 = numpy.subtract(xyzs[idx], xyzs[idx1]) vec = dist * vec0 / numpy.linalg.norm(vec0) disp = vec - vec0 geo = translate(geo, disp, idxs=idxs) if idx2 is not None: assert idx1 is not None, "idx1 is required if changing an angle" # Second, adjust the angle ang0 = central_angle(geo, idx, idx1, idx2) ang_diff = ang - ang0 # If idx-idx1-idx2 is not linear, use the normal to this plane, # otherwise, use the normal to the idx1-idx2-idx3 plane if not numpy.abs(ang0) < tol or numpy.abs(ang0 - lin) < tol: axis = util.vec.unit_perpendicular(xyzs[idx], xyzs[idx2], orig_xyz=xyzs[idx1]) else: axis = util.vec.unit_perpendicular(xyzs[idx1], xyzs[idx3], orig_xyz=xyzs[idx1]) # I don't know how to figure out which way to rotate, so just try both # and see which one works for dang in [-ang_diff, ang_diff]: geo_ = rotate(geo, axis, dang, orig_xyz=xyzs[idx1], idxs=idxs) ang_out = central_angle(geo_, idx, idx1, idx2) abs_diff = numpy.abs(numpy.mod(ang, 2*numpy.pi) - numpy.mod(ang_out, 2*numpy.pi)) ang_comp = numpy.abs(numpy.pi - numpy.abs(abs_diff - numpy.pi)) if ang_comp < tol: geo = geo_ break if idx3 is not None: assert idx1 is not None, "idx1 is required if changing a dihedral" assert idx2 is not None, "idx2 is required if changing a dihedral" # Third, adjust the dihedral dih0 = dihedral_angle(geo, idx, idx1, idx2, idx3) ddih = dih - dih0 axis = numpy.subtract(xyzs[idx1], xyzs[idx2]) geo = rotate(geo, axis, ddih, orig_xyz=xyzs[idx1], idxs=idxs) return geo