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 stereogenic_atom_keys(gra, assigned=False): """ Find stereogenic atoms in this graph. If the `assigned` flag is set to `False`, only unassigned stereogenic atoms will be detected. :param gra: the graph :param assigned: Include atoms that already have stereo assignments? :param assigned: bool :returns: the stereogenic atom keys :rtype: frozenset """ gra = without_bond_orders(gra) gra = explicit(gra) # for simplicity, add the explicit hydrogens back in atm_keys = dict_.keys_by_value(atom_bond_valences(gra), lambda x: x == 4) if not assigned: # Remove assigned stereo keys atm_keys -= atom_stereo_keys(gra) atm_ngb_keys_dct = atoms_neighbor_atom_keys(gra) def _is_stereogenic(atm_key): atm_ngb_keys = list(atm_ngb_keys_dct[atm_key]) pri_vecs = [ stereo_priority_vector(gra, atm_key, atm_ngb_key) for atm_ngb_key in atm_ngb_keys ] ret = not any(pv1 == pv2 for pv1, pv2 in itertools.combinations(pri_vecs, r=2)) return ret ste_gen_atm_keys = frozenset(filter(_is_stereogenic, atm_keys)) return ste_gen_atm_keys
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_atom_stereo_keys(ste_tsg, check=True): """ Determine atom stereo centers which are not conserved by the reaction. This includes atoms which are stereogenic for the products but not for the reactants ("created" stereo centers) and atoms 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 atom 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 = atom_stereo_keys(rcts_gra) keys2 = stereogenic_atom_keys(prds_gra, assigned=True) cre_ste_atm_keys = frozenset(keys2 - keys1) des_ste_atm_keys = frozenset(keys1 - keys2) return cre_ste_atm_keys, des_ste_atm_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 chirality_constraint_bounds(gra, keys): """ bounds for enforcing chirality restrictions """ ste_keys = set(atom_stereo_keys(gra)) & set(keys) par_dct = atom_stereo_parities(gra) ngb_key_dct = atoms_neighbor_atom_keys(gra) def _chirality_constraint(key): ngb_keys = ngb_key_dct[key] ngb_keys = atom_stereo_sorted_neighbor_atom_keys(gra, key, ngb_keys) idxs = tuple(map(keys.index, ngb_keys)) vol_range = (-999., -7.) if par_dct[key] else (+7., +999.) return idxs, vol_range chi_dct = dict(map(_chirality_constraint, ste_keys)) return chi_dct
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 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