def is_stereo_compatible(tra, sgr1, sgr2): """ is this transformation compatible with the reactant/product stereo assignments? """ cgr1 = without_stereo_parities(sgr1) cgr2 = without_stereo_parities(sgr2) atm_key_dct = _full_isomorphism(apply(tra, cgr1), cgr2) # determine the stereo centers which are preserved in the transformation sgr1 = _relabel(sgr1, atm_key_dct) atm_keys = sorted(atom_stereo_keys(sgr1) & atom_stereo_keys(sgr2)) bnd_keys = sorted(bond_stereo_keys(sgr1) & bond_stereo_keys(sgr2)) atm_pars1 = dict_.values_by_key(atom_stereo_parities(sgr1), atm_keys) atm_pars2 = dict_.values_by_key(atom_stereo_parities(sgr2), atm_keys) bnd_pars1 = dict_.values_by_key(bond_stereo_parities(sgr1), bnd_keys) bnd_pars2 = dict_.values_by_key(bond_stereo_parities(sgr2), bnd_keys) atm_ngb_keys_dct1 = atom_neighbor_keys(sgr1) atm_ngb_keys_dct2 = atom_neighbor_keys(sgr2) ret = True for atm_key, par1, par2 in zip(atm_keys, atm_pars1, atm_pars2): atm_ngb_keys1 = stereo_sorted_atom_neighbor_keys( sgr1, atm_key, atm_ngb_keys_dct1[atm_key]) atm_ngb_keys2 = stereo_sorted_atom_neighbor_keys( sgr2, atm_key, atm_ngb_keys_dct2[atm_key]) if _permutation_parity(atm_ngb_keys1, atm_ngb_keys2): ret &= (par1 == par2) else: ret &= (par1 != par2) for bnd_key, par1, par2 in zip(bnd_keys, bnd_pars1, bnd_pars2): atm1_key, atm2_key = bnd_key atm1_ngb_key1 = stereo_sorted_atom_neighbor_keys( sgr1, atm1_key, atm_ngb_keys_dct1[atm1_key] - {atm2_key})[0] atm2_ngb_key1 = stereo_sorted_atom_neighbor_keys( sgr1, atm2_key, atm_ngb_keys_dct1[atm2_key] - {atm1_key})[0] atm1_ngb_key2 = stereo_sorted_atom_neighbor_keys( sgr2, atm1_key, atm_ngb_keys_dct2[atm1_key] - {atm2_key})[0] atm2_ngb_key2 = stereo_sorted_atom_neighbor_keys( sgr2, atm2_key, atm_ngb_keys_dct2[atm2_key] - {atm1_key})[0] if not ((atm1_ngb_key1 != atm1_ngb_key2) ^ (atm2_ngb_key1 != atm2_ngb_key2)): ret &= (par1 == par2) else: ret &= (par1 != par2) return ret
def atoms_stereo_sorted_neighbor_atom_keys(sgr): """ Obtain neighbor atom keys for all stereo atoms, sorted by stereo priority. Includes all stereo atoms and atoms constituting stereo bonds. For stereo bonds, the neighbors for each atom in the bond exclude the other atom in the bond. :param sgr: the graph :returns: Neighbor atom keys, sorted by stereo priority, keyed by atom. :rtype: dict """ atm_ste_keys = atom_stereo_keys(sgr) bnd_ste_keys = bond_stereo_keys(sgr) atm_ngb_keys_dct = atoms_neighbor_atom_keys(sgr) ste_atm_ngb_keys_dct = {} for atm_key in atm_ste_keys: atm_ngb_keys = atm_ngb_keys_dct[atm_key] ste_atm_ngb_keys_dct[atm_key] = atom_stereo_sorted_neighbor_atom_keys( sgr, atm_key, atm_ngb_keys) for bnd_key in bnd_ste_keys: atm1_key, atm2_key = sorted(bnd_key) atm1_ngb_keys = atm_ngb_keys_dct[atm1_key] - bnd_key atm2_ngb_keys = atm_ngb_keys_dct[atm2_key] - bnd_key ste_atm_ngb_keys_dct[atm1_key] = atom_stereo_sorted_neighbor_atom_keys( sgr, atm1_key, atm1_ngb_keys) ste_atm_ngb_keys_dct[atm2_key] = atom_stereo_sorted_neighbor_atom_keys( sgr, atm2_key, atm2_ngb_keys) return ste_atm_ngb_keys_dct
def fake_stereo_geometry(gra, ntries=5, max_dist_err=0.5): """ generate a fake stereo geometry """ # determine stereo "groups" with geometrically interdependent chirality atm_ngbs_dct = atom_neighborhoods(gra) bnd_ngbs_dct = bond_neighborhoods(gra) atm_ste_keys = atom_stereo_keys(gra) bnd_ste_keys = bond_stereo_keys(gra) atm_ste_groups = list( map(atom_keys, map(atm_ngbs_dct.__getitem__, atm_ste_keys))) bnd_ste_groups = list( map(atom_keys, map(bnd_ngbs_dct.__getitem__, bnd_ste_keys))) ste_groups = _aggregate_connected_groups(atm_ste_groups + bnd_ste_groups) ste_groups = list(map(sorted, ste_groups)) natms = 0 geo_idx_dct = {} geo = () for group in ste_groups: group_geo = geometry( gra, keys=group, ntries=ntries, max_dist_err=max_dist_err) group_natms = len(group) idxs = list(range(natms, natms+group_natms)) geo_idx_dct.update(dict(zip(group, idxs))) natms += group_natms geo = geometry_join(geo, group_geo) return geo, geo_idx_dct
def nonconserved_bond_stereo_keys(ste_tsg, check=True): """ Determine bond stereo centers which are not conserved by the reaction. This includes bonds which are stereogenic for the products but not for the reactants ("created" stereo centers) and bonds which are stereogenic for the reactants but not for the products ("destroyed" stereo centers). :param ste_tsg: The TS graph, with stereo assignments. :returns: Created and destroyed bond stereo centers, respectively. :rtype: (frozenset, frozenset) """ rcts_gra = reactants_graph(ste_tsg) prds_gra = products_graph(ste_tsg) if check: ste_atm_keys = stereogenic_atom_keys(rcts_gra) ste_bnd_keys = stereogenic_bond_keys(rcts_gra) assert not ste_atm_keys, ( "Unassigned atom stereo centers: {}".format(str(ste_atm_keys))) assert not ste_bnd_keys, ( "Unassigned bond stereo centers: {}".format(str(ste_bnd_keys))) keys1 = bond_stereo_keys(rcts_gra) keys2 = stereogenic_bond_keys(prds_gra, assigned=True) cre_ste_bnd_keys = frozenset(keys2 - keys1) des_ste_bnd_keys = frozenset(keys1 - keys2) return cre_ste_bnd_keys, des_ste_bnd_keys
def stereogenic_bond_keys(gra, assigned=False): """ Find stereogenic bonds in this graph. If the `assigned` flag is set to `False`, only unassigned stereogenic bonds will be detected. :param gra: the graph :param assigned: Include bonds that already have stereo assignments? :param assigned: bool :returns: the stereogenic bond keys :rtype: frozenset """ gra = without_bond_orders(gra) gra = explicit(gra) # for simplicity, add the explicit hydrogens back in # get candidates: planar bonds bnd_keys = sp2_bond_keys(gra) if not assigned: # remove bonds that already have stereo assignments bnd_keys -= bond_stereo_keys(gra) bnd_keys -= functools.reduce( # remove double bonds in small rings frozenset.union, filter(lambda x: len(x) < 8, rings_bond_keys(gra)), frozenset()) atm_ngb_keys_dct = atoms_neighbor_atom_keys(gra) def _is_stereogenic(bnd_key): atm1_key, atm2_key = bnd_key def _is_symmetric_on_bond(atm_key, atm_ngb_key): atm_ngb_keys = list(atm_ngb_keys_dct[atm_key] - {atm_ngb_key}) if not atm_ngb_keys: # C=:O: ret = True elif len(atm_ngb_keys) == 1: # C=N:-X ret = False else: assert len(atm_ngb_keys) == 2 # C=C(-X)-Y ret = (stereo_priority_vector( gra, atm_key, atm_ngb_keys[0]) == stereo_priority_vector( gra, atm_key, atm_ngb_keys[1])) return ret return not (_is_symmetric_on_bond(atm1_key, atm2_key) or _is_symmetric_on_bond(atm2_key, atm1_key)) ste_gen_bnd_keys = frozenset(filter(_is_stereogenic, bnd_keys)) return ste_gen_bnd_keys
def to_index_based_stereo(sgr): """ Convert a graph to index-based stereo assignments, where parities are defined relative to the ordering of indices rather than the absolute stereo priority. :param sgr: a graph with absolute stereo assignments :returns: a graph with index-based stereo assignments """ assert sgr == explicit(sgr), ("Not an explicit graph:\n{}".format( string(sgr, one_indexed=False))) abs_srt_keys_dct = atoms_stereo_sorted_neighbor_atom_keys(sgr) atm_ste_keys = atom_stereo_keys(sgr) bnd_ste_keys = bond_stereo_keys(sgr) abs_atm_ste_par_dct = atom_stereo_parities(sgr) abs_bnd_ste_par_dct = bond_stereo_parities(sgr) idx_atm_ste_par_dct = {} idx_bnd_ste_par_dct = {} # Determine index-based stereo assignments for atoms for atm_key in atm_ste_keys: abs_srt_keys = abs_srt_keys_dct[atm_key] idx_srt_keys = sorted(abs_srt_keys) if automol.util.is_even_permutation(idx_srt_keys, abs_srt_keys): idx_atm_ste_par_dct[atm_key] = abs_atm_ste_par_dct[atm_key] else: idx_atm_ste_par_dct[atm_key] = not abs_atm_ste_par_dct[atm_key] # Determine index-based stereo assignments for bonds for bnd_key in bnd_ste_keys: atm1_key, atm2_key = sorted(bnd_key) atm1_abs_srt_keys = abs_srt_keys_dct[atm1_key] atm2_abs_srt_keys = abs_srt_keys_dct[atm2_key] atm1_idx_srt_keys = sorted(atm1_abs_srt_keys) atm2_idx_srt_keys = sorted(atm2_abs_srt_keys) if not ((atm1_idx_srt_keys[0] != atm1_abs_srt_keys[0]) ^ (atm2_idx_srt_keys[0] != atm2_abs_srt_keys[0])): idx_bnd_ste_par_dct[bnd_key] = abs_bnd_ste_par_dct[bnd_key] else: idx_bnd_ste_par_dct[bnd_key] = not abs_bnd_ste_par_dct[bnd_key] sgr = set_atom_stereo_parities(sgr, idx_atm_ste_par_dct) sgr = set_bond_stereo_parities(sgr, idx_bnd_ste_par_dct) return sgr
def compatible_reverse_stereomers(ste_tsg): """ Given a TS graph with stereo assignments, expand all possible reverse graphs compatble with the forward graph. :param ste_tsg: The TS graph, with stereo assignments. :returns: All possible reverse TS graphs. """ frm_bnd_keys = forming_bond_keys(ste_tsg) brk_bnd_keys = breaking_bond_keys(ste_tsg) _, des_ste_atm_keys = nonconserved_atom_stereo_keys(ste_tsg) _, des_ste_bnd_keys = nonconserved_bond_stereo_keys(ste_tsg) cons_atm_keys = sorted(atom_stereo_keys(ste_tsg) - des_ste_atm_keys) cons_bnd_keys = sorted(bond_stereo_keys(ste_tsg) - des_ste_bnd_keys) # 1. Determine index-based stereo assignments for conserved stereo centers idx_tsg = to_index_based_stereo(ste_tsg) cons_idx_atm_pars = dict_.values_by_key( atom_stereo_parities(idx_tsg), cons_atm_keys) cons_idx_bnd_pars = dict_.values_by_key( bond_stereo_parities(idx_tsg), cons_bnd_keys) # 2. Determine all possible index-based stereo assignments for the reverse # reaction. prds_gra = without_stereo_parities(products_graph(ste_tsg)) prds_sgrs = _stereomers(prds_gra) prds_idx_sgrs = list(map(_to_index_based_stereo, prds_sgrs)) rev_idx_tsgs_pool = [ graph(p, brk_bnd_keys, frm_bnd_keys) for p in prds_idx_sgrs] # 3. Find possibilities which match the assignments for the conserved # stereo centers. rev_idx_tsgs = [] for rev_idx_tsg in rev_idx_tsgs_pool: rev_cons_idx_atm_pars = dict_.values_by_key( atom_stereo_parities(rev_idx_tsg), cons_atm_keys) rev_cons_idx_bnd_pars = dict_.values_by_key( bond_stereo_parities(rev_idx_tsg), cons_bnd_keys) if (rev_cons_idx_atm_pars == cons_idx_atm_pars and rev_cons_idx_bnd_pars == cons_idx_bnd_pars): rev_idx_tsgs.append(rev_idx_tsg) # 4. Convert the matching reverse graphs back from index-based stereo # assignments to absolute stereo assignments. rev_ste_tsgs = list(map(from_index_based_stereo, rev_idx_tsgs)) return rev_ste_tsgs
def from_index_based_stereo(sgr): """ Convert a graph from index-based stereo assignments back to absolute stereo assignments, where parities are independent of atom ordering. :param sgr: a graph with index-based stereo assignments :returns: a graph with absolute stereo assignments """ assert sgr == explicit(sgr), ("Not an explicit graph:\n{}".format( string(sgr, one_indexed=False))) gra = without_stereo_parities(sgr) if has_stereo(sgr): atm_keys_pool = atom_stereo_keys(sgr) bnd_keys_pool = bond_stereo_keys(sgr) idx_atm_ste_par_dct = atom_stereo_parities(sgr) idx_bnd_ste_par_dct = bond_stereo_parities(sgr) atm_ngb_keys_dct = atoms_neighbor_atom_keys(sgr) atm_keys = set() bnd_keys = set() last_gra = None # Do the assignments iteratively to handle higher-order stereo while last_gra != gra: last_gra = gra abs_atm_ste_par_dct = {} abs_bnd_ste_par_dct = {} atm_keys.update(stereogenic_atom_keys(gra) & atm_keys_pool) bnd_keys.update(stereogenic_bond_keys(gra) & bnd_keys_pool) # Determine absolute stereo assignments for atoms for atm_key in atm_keys: abs_srt_keys = atom_stereo_sorted_neighbor_atom_keys( gra, atm_key, atm_ngb_keys_dct[atm_key]) idx_srt_keys = sorted(abs_srt_keys) if automol.util.is_even_permutation(idx_srt_keys, abs_srt_keys): abs_atm_ste_par_dct[atm_key] = ( idx_atm_ste_par_dct[atm_key]) else: abs_atm_ste_par_dct[atm_key] = ( not idx_atm_ste_par_dct[atm_key]) # Determine absolute stereo assignments for bonds for bnd_key in bnd_keys: atm1_key, atm2_key = sorted(bnd_key) atm1_abs_srt_keys = atom_stereo_sorted_neighbor_atom_keys( gra, atm1_key, atm_ngb_keys_dct[atm1_key] - bnd_key) atm2_abs_srt_keys = atom_stereo_sorted_neighbor_atom_keys( gra, atm2_key, atm_ngb_keys_dct[atm2_key] - bnd_key) atm1_idx_srt_keys = sorted(atm1_abs_srt_keys) atm2_idx_srt_keys = sorted(atm2_abs_srt_keys) if not ((atm1_idx_srt_keys[0] != atm1_abs_srt_keys[0]) ^ (atm2_idx_srt_keys[0] != atm2_abs_srt_keys[0])): abs_bnd_ste_par_dct[bnd_key] = ( idx_bnd_ste_par_dct[bnd_key]) else: abs_bnd_ste_par_dct[bnd_key] = ( not idx_bnd_ste_par_dct[bnd_key]) gra = set_atom_stereo_parities(gra, abs_atm_ste_par_dct) gra = set_bond_stereo_parities(gra, abs_bnd_ste_par_dct) atm_ste_keys = atom_stereo_keys(gra) bnd_ste_keys = bond_stereo_keys(gra) assert atm_ste_keys == atm_keys_pool, ( "Index-based to absolute stereo conversion failed:\n" "{} != {}".format(str(atm_ste_keys), str(atm_keys_pool))) assert bnd_ste_keys == bnd_keys_pool, ( "Index-based to absolute stereo conversion failed:\n" "{} != {}".format(str(bnd_ste_keys), str(bnd_keys_pool))) return gra
def path_distance_bounds_(gra): """ upper distance bound between two ends of a path :param gra: molecular graph :param path: the shortest path between two atoms :type path: list or tuple """ hyb_dct = resonance_dominant_atom_hybridizations(gra) rng_keys_lst = rings_atom_keys(gra) atm_ngb_keys = atoms_neighbor_atom_keys(gra) ste_bnd_keys = bond_stereo_keys(gra) bnd_par_dct = bond_stereo_parities(gra) def _distance_bounds(path): # if the path is 0, the atoms are disconnected and could be arbitrarily # far apart rsz = shared_ring_size(path, rng_keys_lst) if len(path) == 1: ldist = udist = 0 elif len(path) == 2: ldist = udist = heuristic_bond_distance(gra, *path) elif len(path) == 3: if rsz == 0: ldist = udist = heuristic_bond_angle_distance( gra, *path, hyb_dct=hyb_dct) else: a123 = (rsz - 2.) * 180. / rsz la123 = a123 - 10. ua123 = a123 + 10. lrdist = heuristic_bond_angle_distance(gra, *path, a123=la123) urdist = heuristic_bond_angle_distance(gra, *path, a123=ua123) odist = heuristic_bond_angle_distance( gra, *path, hyb_dct=hyb_dct) ldist = min(lrdist, odist) udist = max(urdist, odist) elif len(path) == 4: if rsz == 0: key2, key3 = path[1:3] bnd_key23 = frozenset({key2, key3}) # handle bond stereo here if bnd_key23 in ste_bnd_keys: key2_ngbs = atom_stereo_sorted_neighbor_atom_keys( gra, key2, atm_ngb_keys[key2]-{key3}) key3_ngbs = atom_stereo_sorted_neighbor_atom_keys( gra, key3, atm_ngb_keys[key3]-{key2}) pos2 = key2_ngbs.index(path[0]) pos3 = key3_ngbs.index(path[-1]) cis = bnd_par_dct[bnd_key23] != (pos2 != pos3) dih = 0. if cis else 180. ldist = udist = heuristic_torsion_angle_distance( gra, *path, d1234=dih, degree=True, hyb_dct=hyb_dct) else: ldist = heuristic_torsion_angle_distance( gra, *path, d1234=0., degree=True, hyb_dct=hyb_dct) udist = heuristic_torsion_angle_distance( gra, *path, d1234=180., degree=True, hyb_dct=hyb_dct) else: ang = (rsz - 2.) * 180. / rsz rdist = heuristic_torsion_angle_distance( gra, *path, d1234=0., degree=True, a123=ang, a234=ang) cdist = heuristic_torsion_angle_distance( gra, *path, d1234=0., degree=True, hyb_dct=hyb_dct) tdist = heuristic_torsion_angle_distance( gra, *path, d1234=180., degree=True, hyb_dct=hyb_dct) ldist = min(rdist, cdist) udist = max(rdist, tdist) # otherwise, just do the sum of the distances between atoms along the # path else: # we can't handle disconnected points, because in that case the # path is [] and there is no way to recover the keys assert len(path) > 2 ldist = closest_approach(gra, path[0], path[-1]) udist = 999. return ldist, udist return _distance_bounds
def qualitative_convergence_checker_(gra, keys, rqq_bond_max=1.8, rqh_bond_max=1.3, rhh_bond_max=1.1, bond_nobond_diff=0.3): """ a convergence checker for error minimization, checking that the geometry is qualitatively correct (correct connectivity and stereo) """ symb_dct = atom_symbols(gra) pairs = set(map(frozenset, itertools.combinations(keys, 2))) bnd_keys = pairs & bond_keys(gra) nob_keys = pairs - bond_keys(gra) nob_symbs = tuple(tuple(map(symb_dct.__getitem__, nob_key)) for nob_key in nob_keys) bnd_symbs = tuple(tuple(map(symb_dct.__getitem__, bnd_key)) for bnd_key in bnd_keys) nob_idxs = tuple(tuple(map(keys.index, nob_key)) for nob_key in nob_keys) bnd_idxs = tuple(tuple(map(keys.index, bnd_key)) for bnd_key in bnd_keys) bnd_udists = tuple((rqq_bond_max if 'H' not in symb else rhh_bond_max if set(symb) == {'H'} else rqh_bond_max) for symb in bnd_symbs) diff = bond_nobond_diff nob_ldists = tuple((rqq_bond_max+diff if 'H' not in symb else rhh_bond_max+diff if set(symb) == {'H'} else rqh_bond_max+diff) for symb in nob_symbs) bnd_idxs += tuple(map(tuple, map(reversed, bnd_idxs))) bnd_idx_vecs = tuple(map(list, zip(*bnd_idxs))) bnd_udists *= 2 nob_idxs += tuple(map(tuple, map(reversed, nob_idxs))) nob_idx_vecs = tuple(map(list, zip(*nob_idxs))) nob_ldists *= 2 symbs = tuple(map(symb_dct.__getitem__, keys)) geo_idx_dct = dict(map(reversed, enumerate(keys))) atm_ste_keys = atom_stereo_keys(gra) & set(keys) bnd_ste_keys = bond_stereo_keys(gra) & bnd_keys atm_ste_par_dct = atom_stereo_parities(gra) bnd_ste_par_dct = bond_stereo_parities(gra) def _is_converged(xmat, err, grad): assert err and numpy.any(grad) xyzs = xmat[:, :3] dmat = embed.distance_matrix_from_coordinates(xyzs) # check for correct connectivity connectivity_check = ( (numpy.all(dmat[bnd_idx_vecs] < bnd_udists) if bnd_udists else True) and (numpy.all(dmat[nob_idx_vecs] > nob_ldists) if nob_ldists else True)) # check for correct stereo parities geo = _create.geom.from_data(symbs, xyzs, angstrom=True) atom_stereo_check = all( (_atom_stereo_parity_from_geometry(gra, atm_key, geo, geo_idx_dct) == atm_ste_par_dct[atm_key]) for atm_key in atm_ste_keys) bond_stereo_check = all( (_bond_stereo_parity_from_geometry(gra, bnd_key, geo, geo_idx_dct) == bnd_ste_par_dct[bnd_key]) for bnd_key in bnd_ste_keys) return connectivity_check and atom_stereo_check and bond_stereo_check return _is_converged