def _bond_stereo_parity_from_geometry(gra, bnd_key, geo, geo_idx_dct): """ get the current stereo parity of a bond from its geometry """ atm1_key, atm2_key = bnd_key atm_ngb_keys_dct = atoms_neighbor_atom_keys(gra) atm1_ngb_keys = atm_ngb_keys_dct[atm1_key] - {atm2_key} atm2_ngb_keys = atm_ngb_keys_dct[atm2_key] - {atm1_key} atm1_ngb_keys = atom_stereo_sorted_neighbor_atom_keys( gra, atm1_key, atm1_ngb_keys) atm2_ngb_keys = atom_stereo_sorted_neighbor_atom_keys( gra, atm2_key, atm2_ngb_keys) # get the top priority neighbor keys on each side atm1_ngb_key = atm1_ngb_keys[0] atm2_ngb_key = atm2_ngb_keys[0] # determine the parity based on the coordinates xyzs = coordinates(geo) atm1_xyz = xyzs[geo_idx_dct[atm1_key]] atm2_xyz = xyzs[geo_idx_dct[atm2_key]] atm1_ngb_xyz = xyzs[geo_idx_dct[atm1_ngb_key]] atm2_ngb_xyz = xyzs[geo_idx_dct[atm2_ngb_key]] atm1_bnd_vec = numpy.subtract(atm1_ngb_xyz, atm1_xyz) atm2_bnd_vec = numpy.subtract(atm2_ngb_xyz, atm2_xyz) dot_val = numpy.vdot(atm1_bnd_vec, atm2_bnd_vec) assert dot_val != 0. # for now, assume no collinear par = dot_val > 0. return par
def dihedral_angle(geo, idx1, idx2, idx3, idx4, degree=False): """ Measure the angle inscribed by three atoms in a molecular geometry. :param geo: molecular geometry :type geo: automol geometry data structure :param idx1: index of atom 1 in the quartet to be measured :type idx1: int :param idx2: index of atom 2 in the quartet to be measured :type idx2: int :param idx3: index of atom 3 in the quartet to be measured :type idx3: int :param idx4: index of atom 4 in the quartet to be measured :type idx4: int :param degree: parameter to control conversion to degree :type degree: bool :rtype: float """ xyzs = coordinates(geo) xyz1 = xyzs[idx1] xyz2 = xyzs[idx2] xyz3 = xyzs[idx3] xyz4 = xyzs[idx4] dih = util.vec.dihedral_angle(xyz1, xyz2, xyz3, xyz4) dih *= phycon.RAD2DEG if degree else 1 return dih
def _atom_stereo_corrected_geometry(gra, atm_ste_par_dct, geo, geo_idx_dct): """ correct the atom stereo parities of a geometry, for a subset of atoms """ ring_atm_keys = set(itertools.chain(*rings_atom_keys(gra))) atm_ngb_keys_dct = atoms_neighbor_atom_keys(gra) atm_keys = list(atm_ste_par_dct.keys()) for atm_key in atm_keys: par = atm_ste_par_dct[atm_key] curr_par = _atom_stereo_parity_from_geometry(gra, atm_key, geo, geo_idx_dct) if curr_par != par: atm_ngb_keys = atm_ngb_keys_dct[atm_key] # for now, we simply exclude rings from the pivot keys # (will not work for stereo atom at the intersection of two rings) atm_piv_keys = list(atm_ngb_keys - ring_atm_keys)[:2] assert len(atm_piv_keys) == 2 atm3_key, atm4_key = atm_piv_keys # get coordinates xyzs = coordinates(geo) atm_xyz = xyzs[geo_idx_dct[atm_key]] atm3_xyz = xyzs[geo_idx_dct[atm3_key]] atm4_xyz = xyzs[geo_idx_dct[atm4_key]] # do the rotation rot_axis = util.vec.unit_bisector(atm3_xyz, atm4_xyz, orig_xyz=atm_xyz) rot_atm_keys = ( atom_keys(branch(gra, atm_key, {atm_key, atm3_key})) | atom_keys(branch(gra, atm_key, {atm_key, atm4_key}))) rot_idxs = list(map(geo_idx_dct.__getitem__, rot_atm_keys)) geo = automol.geom.rotate(geo, rot_axis, numpy.pi, orig_xyz=atm_xyz, idxs=rot_idxs) assert _atom_stereo_parity_from_geometry(gra, atm_key, geo, geo_idx_dct) == par gra = set_atom_stereo_parities(gra, {atm_key: par}) return geo, gra
def inchi_with_sort_from_geometry(gra, geo=None, geo_idx_dct=None): """ Generate an InChI string from a molecular graph. If coordinates are passed in, they are used to determine stereo. :param gra: molecular graph :type gra: automol graph data structure :param geo: molecular geometry :type geo: automol geometry data structure :param geo_idx_dct: :type geo_idx_dct: dict[:] :rtype: (str, tuple(int)) """ gra = without_dummy_atoms(gra) gra = dominant_resonance(gra) atm_keys = sorted(atom_keys(gra)) bnd_keys = list(bond_keys(gra)) atm_syms = dict_.values_by_key(atom_symbols(gra), atm_keys) atm_bnd_vlcs = dict_.values_by_key(atom_bond_valences(gra), atm_keys) atm_rad_vlcs = dict_.values_by_key(atom_unsaturated_valences(gra), atm_keys) bnd_ords = dict_.values_by_key(bond_orders(gra), bnd_keys) if geo is not None: assert geo_idx_dct is not None atm_xyzs = coordinates(geo) atm_xyzs = [ atm_xyzs[geo_idx_dct[atm_key]] if atm_key in geo_idx_dct else (0., 0., 0.) for atm_key in atm_keys ] else: atm_xyzs = None mlf, key_map_inv = _molfile.from_data(atm_keys, bnd_keys, atm_syms, atm_bnd_vlcs, atm_rad_vlcs, bnd_ords, atm_xyzs=atm_xyzs) rdm = _rdkit.from_molfile(mlf) ich, aux_info = _rdkit.to_inchi(rdm, with_aux_info=True) nums = _parse_sort_order_from_aux_info(aux_info) nums = tuple(map(key_map_inv.__getitem__, nums)) return ich, nums
def connectivity_graph(geo, rqq_bond_max=3.45, rqh_bond_max=2.6, rhh_bond_max=1.9): """ Generate a molecular graph from the molecular geometry that has information about bond connectivity. :param rqq_bond_max: maximum distance between heavy atoms :type rqq_bond_max: float :param rqh_bond_max: maximum distance between heavy atoms and hydrogens :type rqh_bond_max: float :param rhh_bond_max: maximum distance between hydrogens :type rhh_bond_max: float :rtype: automol molecular graph structure """ symbs = symbols(geo) xyzs = coordinates(geo) def _distance(idx_pair): xyz1, xyz2 = map(xyzs.__getitem__, idx_pair) dist = numpy.linalg.norm(numpy.subtract(xyz1, xyz2)) return dist def _are_bonded(idx_pair): sym1, sym2 = map(symbs.__getitem__, idx_pair) dist = _distance(idx_pair) return (False if 'X' in (sym1, sym2) else (dist < rqh_bond_max) if 'H' in (sym1, sym2) else (dist < rhh_bond_max) if (sym1 == 'H' and sym2 == 'H') else (dist < rqq_bond_max)) idxs = range(len(xyzs)) atm_symb_dct = dict(enumerate(symbs)) bnd_keys = tuple( map(frozenset, filter(_are_bonded, itertools.combinations(idxs, r=2)))) bnd_ord_dct = {bnd_key: 1 for bnd_key in bnd_keys} gra = create.graph.from_data(atom_symbols=atm_symb_dct, bond_keys=bnd_keys, bond_orders=bnd_ord_dct) return gra
def distance(geo, idx1, idx2, angstrom=False): """ Measure the distance between two atoms in a molecular geometry. :param geo: molecular geometry :type geo: automol geometry data structure :param idx1: index of atom 1 in the pair to be measured :type idx1: int :param idx2: index of atom 2 in the pair to be measured :type idx2: int :param angstrom: parameter to control conversion to Angstrom :type angstrom: bool :rtype: float """ xyzs = coordinates(geo) xyz1 = xyzs[idx1] xyz2 = xyzs[idx2] dist = util.vec.distance(xyz1, xyz2) dist *= phycon.BOHR2ANG if angstrom else 1 return dist
def from_subset(geo, idxs): """ Generate a new molecular geometry from a subset of the atoms in an input geometry. (Rename this and put it under operations?) :param geo: molecular geometry :type geo: automol molecular geometry data structure :param idxs: indices representing the subset of atoms :type idxs: tuple(int) :rtype: automol moleculer geometry data structure """ symbs = symbols(geo) xyzs = coordinates(geo) symbs = list(map(symbs.__getitem__, idxs)) xyzs = list(map(xyzs.__getitem__, idxs)) return automol.create.geom.from_data(symbs, xyzs)
def _atom_stereo_parity_from_geometry(gra, atm_key, geo, geo_idx_dct): """ get the current stereo parity of an atom from its geometry """ atm_ngb_keys_dct = atoms_neighbor_atom_keys(gra) atm_ngb_keys = atm_ngb_keys_dct[atm_key] # sort the neighbor keys by stereo priority atm_ngb_keys = atom_stereo_sorted_neighbor_atom_keys( gra, atm_key, atm_ngb_keys) # determine the parity based on the coordinates xyzs = coordinates(geo) atm_ngb_idxs = dict_.values_by_key(geo_idx_dct, atm_ngb_keys) atm_ngb_xyzs = [xyzs[idx] for idx in atm_ngb_idxs] det_mat = numpy.ones((4, 4)) det_mat[:, :3] = atm_ngb_xyzs det_val = numpy.linalg.det(det_mat) assert det_val != 0. # for now, assume no four-atom planes par = det_val > 0. return par
def insert(geo, symb, xyz, idx=None, angstrom=False): """ Insert an atom into a molecular geometry. :param geo: molecular geometry :type geo: automol molecular geometry data structure :param symb: symbol of atom to add :type symb: str :param xyz: xyz coordinates of atom to add :type xyz: tuple(float) :param idx: index of geometry to place atom :type idx: int :rtype: automol geometry date structure """ symbs = list(symbols(geo)) xyzs = list(coordinates(geo, angstrom=angstrom)) idx = idx if idx is not None else len(symbs) symbs.insert(idx, symb) xyzs.insert(idx, xyz) return automol.create.geom.from_data(symbs, xyzs, angstrom=angstrom)
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(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 = 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