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 angle_distances(geos, angstrom=True): """ return a dictionary of distances between ends of a bond angle for a sequence of reactant geometries, shifting the keys as needed :param geos: the reactant geometries :param angstrom: return the distances in angstroms? """ dist_dct = {} shift = 0 for geo in geos: gra = connectivity_graph(geo) pairs = [(k1, k3) for k1, _, k3 in automol.graph.angle_keys(gra)] keys = [frozenset({k1 + shift, k2 + shift}) for k1, k2 in pairs] dists = [distance(geo, *p, angstrom=angstrom) for p in pairs] dist_dct.update(dict(zip(keys, dists))) shift += count(geo) return dist_dct
def inchi_with_sort(geo, stereo=True): """ Generate an InChI string from a molecular geometry. (Sort?) :param geo: molecular geometry :type geo: automol geometry data structure :param stereo: parameter to include stereochemistry information :type stereo: bool :rtype: str """ ich = automol.inchi.base.hardcoded_object_to_inchi_by_key( 'geom', geo, comp=_compare) nums_lst = None if ich is None: gra = connectivity_graph(geo) if not stereo: geo = None geo_idx_dct = None else: geo_idx_dct = dict(enumerate(range(count(geo)))) ich, nums_lst = automol.graph.inchi_with_sort_from_geometry( gra=gra, geo=geo, geo_idx_dct=geo_idx_dct) return ich, nums_lst
def join(geo1, geo2, key2, key3, r23, a123=85., a234=85., d1234=85., key1=None, key4=None, angstrom=True, degree=True): """ join two geometries based on four of their atoms, two on the first and two on the second Variables set the coordinates for 1-2...3-4 where 1-2 are bonded atoms in geo1 and 3-4 are bonded atoms in geo2. """ key3 = key3 - count(geo1) a123 *= phycon.DEG2RAD if degree else 1 a234 *= phycon.DEG2RAD if degree else 1 d1234 *= phycon.DEG2RAD if degree else 1 gra1, gra2 = map(connectivity_graph, (geo1, geo2)) key1 = (automol.graph.atom_neighbor_atom_key(gra1, key2) if key1 is None else key1) key4 = (automol.graph.atom_neighbor_atom_key(gra2, key3) if key4 is None else key4) syms1 = symbols(geo1) syms2 = symbols(geo2) xyzs1 = coordinates(geo1, angstrom=angstrom) xyzs2 = coordinates(geo2, angstrom=angstrom) xyz1 = xyzs1[key1] xyz2 = xyzs1[key2] orig_xyz3 = xyzs2[key3] if key4 is not None: orig_xyz4 = xyzs2[key4] else: orig_xyz4 = [1., 1., 1.] r34 = vec.distance(orig_xyz3, orig_xyz4) # Place xyz3 as far away from the atoms in geo1 as possible by optimizing # the undetermined dihedral angle xyz0 = vec.arbitrary_unit_perpendicular(xyz2, orig_xyz=xyz1) def _distance_norm(dih): # objective function for minimization dih, = dih xyz3 = vec.from_internals(dist=r23, xyz1=xyz2, ang=a123, xyz2=xyz1, dih=dih, xyz3=xyz0) dist_norm = numpy.linalg.norm(numpy.subtract(xyzs1, xyz3)) # return the negative norm so that minimum value gives maximum distance return -dist_norm res = scipy.optimize.basinhopping(_distance_norm, 0.) dih = res.x[0] # Now, get the next position with the optimized dihedral angle xyz3 = vec.from_internals(dist=r23, xyz1=xyz2, ang=a123, xyz2=xyz1, dih=dih, xyz3=xyz0) # Don't use the dihedral angle if 1-2-3 are linear if numpy.abs(a123 * phycon.RAD2DEG - 180.) > 5.: xyz4 = vec.from_internals(dist=r34, xyz1=xyz3, ang=a234, xyz2=xyz2, dih=d1234, xyz3=xyz1) else: xyz4 = vec.from_internals(dist=r34, xyz1=xyz3, ang=a234, xyz2=xyz2) align_ = vec.aligner(orig_xyz3, orig_xyz4, xyz3, xyz4) xyzs2 = tuple(map(align_, xyzs2)) syms = syms1 + syms2 xyzs = xyzs1 + xyzs2 geo = from_data(syms, xyzs, angstrom=angstrom) return geo
def insert_dummies_on_linear_atoms(geo, lin_idxs=None, gra=None, dist=1., tol=5.): """ Insert dummy atoms over linear atoms in the geometry. :param geo: the geometry :type geo: automol molecular geometry data structure :param lin_idxs: the indices of the linear atoms; if None, indices are automatically determined from the geometry based on the graph :type lin_idxs: tuple(int) :param gra: the graph describing connectivity; if None, a connectivity graph will be generated using default distance thresholds :type gra: automol molecular graph data structure :param dist: distance of dummy atom from the linear atom, in angstroms :type dist: float :param tol: the tolerance threshold for linearity, in degrees :type tol: float :returns: geometry with dummy atoms inserted, along with a dictionary mapping the linear atoms onto their associated dummy atoms :rtype: automol molecular geometry data structure """ lin_idxs = linear_atoms(geo) if lin_idxs is None else lin_idxs gra = connectivity_graph(geo) if gra is None else gra dummy_ngb_idxs = set( automol.graph.dummy_atoms_neighbor_atom_key(gra).values()) assert not dummy_ngb_idxs & set(lin_idxs), ( "Attempting to add dummy atoms on atoms that already have them: {}". format(dummy_ngb_idxs & set(lin_idxs))) ngb_idxs_dct = automol.graph.atoms_sorted_neighbor_atom_keys(gra) xyzs = coordinates(geo, angstrom=True) 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 # partition the linear atoms into adjacent groups, to be handled together lin_idxs_lst = sorted( map( sorted, util.equivalence_partition(lin_idxs, lambda x, y: x in ngb_idxs_dct[y]))) dummy_key_dct = {} for idxs in lin_idxs_lst: direc = _perpendicular_direction(idxs) for idx in idxs: xyz = numpy.add(xyzs[idx], numpy.multiply(dist, direc)) dummy_key_dct[idx] = count(geo) geo = insert(geo, 'X', xyz, angstrom=True) return geo, dummy_key_dct
def join(geo1, geo2, key2, key3, r23, a123=85., a234=85., d1234=85., key1=None, key4=None, angstrom=True, degree=True): """ join two geometries based on four of their atoms, two on the first and two on the second Variables set the coordinates for 1-2...3-4 where 1-2 are bonded atoms in geo1 and 3-4 are bonded atoms in geo2. """ key3 = key3 - count(geo1) a123 *= phycon.DEG2RAD if degree else 1 a234 *= phycon.DEG2RAD if degree else 1 d1234 *= phycon.DEG2RAD if degree else 1 gra1, gra2 = map(connectivity_graph, (geo1, geo2)) key1 = (automol.graph.atom_neighbor_atom_key(gra1, key2) if key1 is None else key1) key4 = (automol.graph.atom_neighbor_atom_key(gra2, key3) if key4 is None else key4) syms1 = symbols(geo1) syms2 = symbols(geo2) xyzs1 = coordinates(geo1, angstrom=angstrom) xyzs2 = coordinates(geo2, angstrom=angstrom) xyz1 = xyzs1[key1] if key1 is not None else None xyz2 = xyzs1[key2] orig_xyz3 = xyzs2[key3] orig_xyz4 = xyzs2[key4] if key4 is not None else [1., 1., 1.] if key1 is None: # If the first fragment is monatomic, we can place the other one # anywhere we want (direction doesn't matter) xyz3 = numpy.add(xyz2, [0., 0., r23]) else: # If the first fragment isn't monatomic, we need to take some care in # where we place the second one. # r23 and a123 are fixed, so the only degree of freedom we have to do # this is to optimize a dihedral angle relative to an arbitrary point # xyz0. # This dihedral angle is optimized to maximize the distance of xyz3 # from all of the atoms in fragment 1 (xyzs1). xyz1 = xyzs1[key1] # Place xyz3 as far away from the atoms in geo1 as possible by # optimizing the undetermined dihedral angle xyz0 = vec.arbitrary_unit_perpendicular(xyz2, orig_xyz=xyz1) def _distance_norm(dih): # objective function for minimization dih, = dih xyz3 = vec.from_internals(dist=r23, xyz1=xyz2, ang=a123, xyz2=xyz1, dih=dih, xyz3=xyz0) dist_norm = numpy.linalg.norm(numpy.subtract(xyzs1, xyz3)) # return the negative norm so that minimum value gives maximum # distance return -dist_norm res = scipy.optimize.basinhopping(_distance_norm, 0.) dih = res.x[0] # Now, get the next position with the optimized dihedral angle xyz3 = vec.from_internals(dist=r23, xyz1=xyz2, ang=a123, xyz2=xyz1, dih=dih, xyz3=xyz0) r34 = vec.distance(orig_xyz3, orig_xyz4) # If 2 doen't have neighbors or 1-2-3 are linear, ignore the dihedral angle if key1 is None or numpy.abs(a123 * phycon.RAD2DEG - 180.) < 5.: xyz4 = vec.from_internals(dist=r34, xyz1=xyz3, ang=a234, xyz2=xyz2) else: xyz4 = vec.from_internals(dist=r34, xyz1=xyz3, ang=a234, xyz2=xyz2, dih=d1234, xyz3=xyz1) align_ = vec.aligner(orig_xyz3, orig_xyz4, xyz3, xyz4) xyzs2 = tuple(map(align_, xyzs2)) syms = syms1 + syms2 xyzs = xyzs1 + xyzs2 geo = from_data(syms, xyzs, angstrom=angstrom) return geo