def linear_segments_atom_keys(gra, lin_keys=None): """ atom keys for linear segments in the graph """ ngb_keys_dct = atoms_neighbor_atom_keys(without_dummy_atoms(gra)) lin_keys = (dummy_atoms_neighbor_atom_key(gra).values() if lin_keys is None else lin_keys) lin_keys = [k for k in lin_keys if len(ngb_keys_dct[k]) <= 2] lin_segs = connected_components(subgraph(gra, lin_keys)) lin_keys_lst = [] for lin_seg in lin_segs: lin_seg_keys = atom_keys(lin_seg) if len(lin_seg_keys) == 1: key, = lin_seg_keys lin_keys_lst.append([key]) else: end_key1, end_key2 = sorted([ key for key, ngb_keys in atoms_neighbor_atom_keys(lin_seg).items() if len(ngb_keys) == 1 ]) ngb_keys_dct = atoms_neighbor_atom_keys(lin_seg) key = None keys = [end_key1] while key != end_key2: key, = ngb_keys_dct[keys[-1]] - set(keys) keys.append(key) lin_keys_lst.append(keys) lin_keys_lst = tuple(map(tuple, lin_keys_lst)) return lin_keys_lst
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 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 heuristic_bond_angle(gra, key1, key2, key3, degree=False, check=False, hyb_dct=None): """ heuristic bond angle If being reused multiple times, you can speed this up by passing in the hybridizations, so they don't need to be recalculated """ if check: assert {key1, key3} <= set(atoms_neighbor_atom_keys(gra)[key2]) if hyb_dct is None: hyb_dct = resonance_dominant_atom_hybridizations(gra) hyb2 = hyb_dct[key2] if hyb2 == 3: ang = TET_ANG elif hyb2 == 2: ang = TRI_ANG else: assert hyb2 == 1 ang = LIN_ANG ang *= 1 if degree else phycon.DEG2RAD return ang
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 _cumulene_chains(rgr): atm_hyb_dct = resonance_dominant_atom_hybridizations(rgr) sp1_atm_keys = dict_.keys_by_value(atm_hyb_dct, lambda x: x == 1) sp2_atm_keys = dict_.keys_by_value(atm_hyb_dct, lambda x: x == 2) atm_ngb_keys_dct = atoms_neighbor_atom_keys(rgr) def _cumulene_chain(chain): ret = None atm_key = chain[-1] next_atm_keys = atm_ngb_keys_dct[atm_key] - {chain[-2]} if next_atm_keys: assert len(next_atm_keys) == 1 next_atm_key, = next_atm_keys if next_atm_key in sp1_atm_keys: chain.append(next_atm_key) ret = _cumulene_chain(chain) elif next_atm_key in sp2_atm_keys: chain.append(next_atm_key) ret = chain return ret cum_chains = [] for atm_key in sp2_atm_keys: sp1_atm_ngb_keys = atm_ngb_keys_dct[atm_key] & sp1_atm_keys chains = [[atm_key, atm_ngb_key] for atm_ngb_key in sp1_atm_ngb_keys] for chain in chains: cum_chain = _cumulene_chain(chain) if cum_chain is not None: cum_chains.append(cum_chain) cum_chains = tuple(map(tuple, cum_chains)) return cum_chains
def _radical_graph_isomorphisms(gra): """ Generate a set of graphs where the radical has been migrated to a new atom and then calculate the isomorphism between the input graph and all of the new graphs generated by moving the radical """ # Determine useful keys symbols = atom_symbols(gra) unsat_keys = unsaturated_atom_keys(gra) unsat_key = next(iter(unsat_keys)) h_atm_key = max(symbols.keys()) + 1 new_gras, isomorphisms = [], [] for aidx, symbol in enumerate(symbols.values()): # Loop over saturated (non-radical) heavy atoms if symbol != 'H' and aidx != unsat_key: # Add hydrogen atom to radical atom new_graph = add_atom_explicit_hydrogen_keys( gra, {unsat_key: [h_atm_key]}) # Remove hydrogen from saturated atom neighbors = atoms_neighbor_atom_keys(new_graph) for neigh in neighbors[aidx]: if symbols[neigh] == 'H': aneighbor = neigh break new_graph = remove_atoms(new_graph, [aneighbor]) # Build lists to return new_gras.append(new_graph) isomorphisms.append(full_isomorphism(gra, new_graph)) return tuple(zip(new_gras, isomorphisms))
def atom_longest_chain(gra, atm_key): """ longest chain for a specific atom """ atm_ngb_keys_dct = atoms_neighbor_atom_keys(gra) atm_ngb_keys = atm_ngb_keys_dct[atm_key] chains_lst = [] if atm_ngb_keys: next_chains_lst = [[atm_key, atm_ngb_key] for atm_ngb_key in atm_ngb_keys] while True: chains_lst = next_chains_lst next_chains_lst = [] for chain in chains_lst: atm_ngb_keys = atm_ngb_keys_dct[chain[-1]] next_atm_keys = sorted(atm_ngb_keys - set(chain)) for next_atm_key in next_atm_keys: next_chains_lst.append(chain + [next_atm_key]) if not next_chains_lst: break max_chain = tuple(chains_lst[0]) else: max_chain = tuple((atm_key, )) return max_chain
def rotational_symmetry_number(gra, key1, key2, lin_keys=None): """ get the rotational symmetry number along a given rotational axis :param gra: the graph :param key1: the first atom key :param key2: the second atom key """ ngb_keys_dct = atoms_neighbor_atom_keys(without_dummy_atoms(gra)) imp_hyd_vlc_dct = atom_implicit_hydrogen_valences(implicit(gra)) axis_keys = {key1, key2} # If the keys are part of a linear chain, use the ends of that for the # symmetry number calculation lin_keys_lst = linear_segments_atom_keys(gra, lin_keys=lin_keys) for keys in lin_keys_lst: if key1 in keys or key2 in keys: if len(keys) == 1: key1, key2 = sorted(ngb_keys_dct[keys[0]]) else: key1, = ngb_keys_dct[keys[0]] - {keys[1]} key2, = ngb_keys_dct[keys[-1]] - {keys[-2]} axis_keys |= set(keys) break sym_num = 1 for key in (key1, key2): if key in imp_hyd_vlc_dct: ngb_keys = ngb_keys_dct[key] - axis_keys if len(ngb_keys) == imp_hyd_vlc_dct[key] == 3: sym_num = 3 break return sym_num
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 = 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 bond_symmetry_numbers(gra, frm_bnd_key=None, brk_bnd_key=None): """ symmetry numbers, by bond the (approximate) symmetry number of the torsional potential for this bond, based on the hydrogen counts for each atom It is reduced to 1 if one of the H atoms in the torsional bond is a neighbor to the special bonding atom (the atom that is being transferred) """ imp_gra = implicit(gra) atm_imp_hyd_vlc_dct = atom_implicit_hydrogen_valences(imp_gra) bnd_keys = bond_keys(imp_gra) tfr_atm = None if frm_bnd_key and brk_bnd_key: for atm_f in list(frm_bnd_key): for atm_b in list(brk_bnd_key): if atm_f == atm_b: tfr_atm = atm_f if tfr_atm: neighbor_dct = atoms_neighbor_atom_keys(gra) nei_tfr = neighbor_dct[tfr_atm] atms = gra[0] all_hyds = [] for atm in atms: if atms[atm][0] == 'H': all_hyds.append(atm) else: nei_tfr = {} bnd_symb_num_dct = {} bnd_symb_nums = [] for bnd_key in bnd_keys: bnd_sym = 1 vlc = max(map(atm_imp_hyd_vlc_dct.__getitem__, bnd_key)) if vlc == 3: bnd_sym = 3 if tfr_atm: for atm in nei_tfr: nei_s = neighbor_dct[atm] h_nei = 0 for nei in nei_s: if nei in all_hyds: h_nei += 1 if h_nei == 3: bnd_sym = 1 bnd_symb_nums.append(bnd_sym) bnd_symb_num_dct = dict(zip(bnd_keys, bnd_symb_nums)) # fill in the rest of the bonds for completeness bnd_symb_num_dct = dict_.by_key(bnd_symb_num_dct, bond_keys(gra), fill_val=1) return bnd_symb_num_dct
def rotational_bond_keys(gra, lin_keys=None, with_h_rotors=True): """ get all rotational bonds for a graph :param gra: the graph :param lin_keys: keys to linear atoms in the graph """ gra = explicit(gra) sym_dct = atom_symbols(gra) ngb_keys_dct = atoms_neighbor_atom_keys(gra) bnd_ord_dct = resonance_dominant_bond_orders(gra) rng_bnd_keys = list(itertools.chain(*rings_bond_keys(gra))) def _is_rotational_bond(bnd_key): ngb_keys_lst = [ngb_keys_dct[k] - bnd_key for k in bnd_key] is_single = max(bnd_ord_dct[bnd_key]) <= 1 has_neighbors = all(ngb_keys_lst) not_in_ring = bnd_key not in rng_bnd_keys is_h_rotor = any( set(map(sym_dct.__getitem__, ks)) == {'H'} for ks in ngb_keys_lst) return is_single and has_neighbors and not_in_ring and ( not is_h_rotor or with_h_rotors) rot_bnd_keys = frozenset(filter(_is_rotational_bond, bond_keys(gra))) lin_keys_lst = linear_segments_atom_keys(gra, lin_keys=lin_keys) dum_keys = tuple(atom_keys(gra, sym='X')) for keys in lin_keys_lst: bnd_keys = sorted((k for k in rot_bnd_keys if k & set(keys)), key=sorted) # Check whether there are neighboring atoms on either side of the # linear segment excl_keys = set(keys) | set(dum_keys) end_key1 = atom_neighbor_atom_key(gra, keys[0], excl_atm_keys=excl_keys) excl_keys |= {end_key1} end_key2 = atom_neighbor_atom_key(gra, keys[-1], excl_atm_keys=excl_keys) end_keys = {end_key1, end_key2} ngb_keys_lst = [ngb_keys_dct[k] - excl_keys for k in end_keys] has_neighbors = all(ngb_keys_lst) if not has_neighbors: rot_bnd_keys -= set(bnd_keys) else: rot_bnd_keys -= set(bnd_keys[:-1]) return rot_bnd_keys
def _atoms_missing_neighbors(gra, zma_keys): """ get atoms from the list currently in the v-matrix with neighbors that are not in the v-matrix """ ngb_keys_dct = atoms_neighbor_atom_keys(gra) keys = [] for key in zma_keys: if any(k not in zma_keys for k in ngb_keys_dct[key]): keys.append(key) keys = tuple(keys) return keys
def atom_groups(gra, atm): """ return a list of groups off of one atom """ adj_atms = atoms_neighbor_atom_keys(gra) keys = [] for atmi in adj_atms[atm]: key = [atm, atmi] key.sort() key = frozenset(key) keys.append(key) gras = remove_bonds(gra, keys) return connected_components(gras)
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 equivalent_bonds(gra, bnd_key, stereo=True, dummy=True): """ Identify sets of isomorphically equivalent bonds Two bonds are equivalent if they transform into each other under an automorphism :param gra: A graph :param bnd_key: An bond key for the graph, which may be sorted or unsorted :param backbone_only: Compare backbone atoms only? :type stereo: bool :param dummy: Consider dummy atoms? :type dummy: bool :returns: Keys to equivalent bonds :rtype: frozenset """ bnd_key = tuple(bnd_key) bnd_keys = list(map(tuple, map(sorted, bond_keys(gra)))) bnd_keys += list(map(tuple, map(reversed, bnd_keys))) assert bnd_key in bnd_keys, "{} not in {}".format(bnd_key, bnd_keys) atm_symb_dct = atom_symbols(gra) atm_ngbs_dct = atoms_neighbor_atom_keys(gra) def _symbols(bnd_key): return list(map(atm_symb_dct.__getitem__, bnd_key)) def _neighbor_symbols(bnd_key): key1, key2 = bnd_key nsymbs1 = sorted(map(atm_symb_dct.__getitem__, atm_ngbs_dct[key1])) nsymbs2 = sorted(map(atm_symb_dct.__getitem__, atm_ngbs_dct[key2])) return nsymbs1, nsymbs2 # 1. Find bonds with the same atom types bnd_symbs = _symbols(bnd_key) cand_keys = [k for k in bnd_keys if _symbols(k) == bnd_symbs] # 2. Of those, find bonds with the same neighboring atom types bnd_ngb_symbs = _neighbor_symbols(bnd_key) cand_keys = [k for k in cand_keys if _neighbor_symbols(k) == bnd_ngb_symbs] # 3. Find the equivalent bonds from the list of candidates. # Strategy: Change the atom symbols to 'Lv' and 'Ts' and check for # isomorphism. Assumes none of the compounds have element 116 or 117. ref_gra = set_atom_symbols(gra, {bnd_key[0]: 'Lv', bnd_key[1]: 'Ts'}) bnd_keys = [] for key in cand_keys: comp_gra = set_atom_symbols(gra, {key[0]: 'Lv', key[1]: 'Ts'}) if isomorphism(ref_gra, comp_gra, stereo=stereo, dummy=dummy): bnd_keys.append(key) return frozenset(bnd_keys)
def atom_groups(gra, atm, stereo=False): """ return a list of groups off of one atom """ if not stereo: gra = without_stereo_parities(gra) adj_atms = atoms_neighbor_atom_keys(gra) keys = [] for atmi in adj_atms[atm]: key = [atm, atmi] key.sort() key = frozenset(key) keys.append(key) gras = remove_bonds(gra, keys) return connected_components(gras)
def terminal_heavy_atom_keys(gra): """ terminal heavy atoms, sorted by atom type and hydrogen count """ gra = implicit(gra) atm_imp_hyd_vlc_dct = atom_implicit_hydrogen_valences(gra) atm_keys = [ key for key, ngb_keys in atoms_neighbor_atom_keys(gra).items() if len(ngb_keys) == 1 ] atm_keys = sorted(atm_keys, key=atm_imp_hyd_vlc_dct.__getitem__, reverse=True) atm_symbs = dict_.values_by_key(atom_symbols(gra), atm_keys) srt = automol.formula.argsort_symbols(atm_symbs, symbs_first=('C', )) atm_keys = tuple(map(atm_keys.__getitem__, srt)) return atm_keys
def atoms_second_degree_neighbor_atom_keys(gra): """ keys of second-degree neighboring atoms, by atom That is, atoms that are connected through an intermediate atom """ atm_ngb_keys_dct = atoms_neighbor_atom_keys(gra) atm_ngb2_keys_dct = {} for atm_key, atm_ngb_keys in atm_ngb_keys_dct.items(): # Union of neighbors of neighbors atm_ngb2_keys = functools.reduce( operator.or_, map(atm_ngb_keys_dct.__getitem__, atm_ngb_keys)) # Subtract of the atom itself and its neighbors (in case of 3-rings) atm_ngb2_keys -= {atm_key} | atm_ngb_keys atm_ngb2_keys_dct[atm_key] = frozenset(atm_ngb2_keys) return atm_ngb2_keys_dct
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 _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 heuristic_bond_distance(gra, key1, key2, angstrom=True, check=False): """ heuristic bond distance (in angstroms) """ if check: assert key1 in atoms_neighbor_atom_keys(gra)[key2] symb_dct = atom_symbols(gra) symb1 = symb_dct[key1] symb2 = symb_dct[key2] if ptab.to_number(symb1) == 1 or ptab.to_number(symb2) == 1: dist = XH_DIST else: dist = XY_DIST dist *= 1 if angstrom else phycon.ANG2BOHR return dist
def radical_dissociation_prods(gra, pgra1): """ given a dissociation product, determine the other product """ gra = without_fractional_bonds(gra) pgra2 = None rads = sing_res_dom_radical_atom_keys(gra) adj_atms = atoms_neighbor_atom_keys(gra) # adj_idxs = tuple(adj_atms[rad] for rad in rads) for rad in rads: for adj in adj_atms[rad]: for group in atom_groups(gra, adj): if full_isomorphism(explicit(group), explicit(pgra1)): pgra2 = remove_atoms(gra, atom_keys(group)) # pgra2 = remove_bonds(pgra2, bond_keys(group)) if bond_keys(group) in pgra2: pgra2 = remove_bonds(pgra2, bond_keys(group)) return (pgra1, pgra2)
def equivalent_atoms(gra, atm_key, stereo=True, dummy=True): """ Identify sets of isomorphically equivalent atoms Two atoms are equivalent if they transform into each other under an automorphism :param gra: A graph :param atm_key: An atom key for the graph :param stereo: Consider stereo? :type stereo: bool :param dummy: Consider dummy atoms? :type dummy: bool :returns: Keys to equivalent atoms :rtype: frozenset """ assert atm_key in atom_keys(gra), ("{} not in {}".format( atm_key, atom_keys(gra))) atm_symb_dct = atom_symbols(gra) atm_ngbs_dct = atoms_neighbor_atom_keys(gra) def _neighbor_symbols(key): return sorted(map(atm_symb_dct.__getitem__, atm_ngbs_dct[key])) # 1. Find atoms with the same symbols atm_symb = atm_symb_dct[atm_key] cand_keys = atom_keys(gra, sym=atm_symb) # 2. Of those, find atoms with the same neighboring atom types atm_ngb_symbs = _neighbor_symbols(atm_key) cand_keys = [k for k in cand_keys if _neighbor_symbols(k) == atm_ngb_symbs] # 3. Find the equivalent atoms from the list of candidates. # Strategy: Change the atom symbol to 'Ts' and check for isomorphism. # Assumes none of the compounds have element 117. ref_gra = set_atom_symbols(gra, {atm_key: 'Ts'}) atm_keys = [] for key in cand_keys: comp_gra = set_atom_symbols(gra, {key: 'Ts'}) if isomorphism(ref_gra, comp_gra, stereo=stereo, dummy=dummy): atm_keys.append(key) return frozenset(atm_keys)
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 _extend_chain_to_include_terminal_hydrogens(gra, keys, start=True, end=True): """ extend each end of a chain to include terminal hydrogens, if any """ symb_dct = atom_symbols(gra) atm_ngb_dct = atoms_neighbor_atom_keys(gra) sta_ngbs = atm_ngb_dct[keys[0]] - {keys[1]} end_ngbs = atm_ngb_dct[keys[-1]] - {keys[-2]} sta_ngb = min((k for k in sta_ngbs if symb_dct[k] == 'H'), default=None) end_ngb = min((k for k in end_ngbs if symb_dct[k] == 'H'), default=None) keys = tuple(keys) if start and sta_ngb is not None: keys = (sta_ngb,) + keys if end and end_ngb is not None: keys = keys + (end_ngb,) return keys
def neighbors_of_type(gra, aidx, symb): """ For a given atom, determine the indices of all the atoms which neighbor it that are of the type specified. :param gra: molecular graph :type gra: molecular graph data structure :param aidx: index of atom for which to find neighbors :type aidx: int :param symb: symbols of desired atom types for neighbors :type symb: str """ idx_symb_dct = atom_symbols(gra) neighs = atoms_neighbor_atom_keys(gra)[aidx] neigh_symbs = atom_idx_to_symb(neighs, idx_symb_dct) idxs_of_type = tuple() for nidx, nsymb in zip(neighs, neigh_symbs): if nsymb == symb: idxs_of_type += (nidx, ) return idxs_of_type
def planarity_constraint_bounds(gra, keys): """ bounds for enforcing planarity restrictions """ ngb_key_dct = atoms_neighbor_atom_keys(gra) ngb_dct = bond_neighborhoods(gra) bnd_keys = [bnd_key for bnd_key in sp2_bond_keys(gra) if atom_keys(ngb_dct[bnd_key]) <= set(keys)] def _planarity_constraints(bnd_key): key1, key2 = sorted(bnd_key) key1ab = sorted(ngb_key_dct[key1] - {key2}) key2ab = sorted(ngb_key_dct[key2] - {key1}) lst = [] # I don't think the order of the keys matters, but I tried to be # roughly consistent with Figure 8 in the Blaney Dixon paper if len(key1ab) == 2 and len(key2ab) == 2: lst.append(tuple(map(keys.index, key1ab + key2ab))) if len(key1ab) == 2: lst.append(tuple(map(keys.index, [key1, key2] + key1ab))) if len(key2ab) == 2: lst.append(tuple(map(keys.index, [key1, key2] + key2ab))) if (len(key1ab) == 2 and len(key2ab) == 1) or ( len(key1ab) == 1 and len(key2ab) == 2): lst.append(tuple(map(keys.index, [key1] + key1ab + key2ab))) lst.append(tuple(map(keys.index, [key2] + key1ab + key2ab))) if len(key1ab) == 1 and len(key2ab) == 1: lst.append(tuple(map(keys.index, [key1, key2] + key1ab + key2ab))) return tuple(lst) const_dct = { idxs: (-0.5, +0.5) for idxs in itertools.chain(*map(_planarity_constraints, bnd_keys))} return const_dct
def two_bond_idxs(gra, symb1, cent, symb2): """ Determine the triplet of indices of atoms of specified types that are connected in a chain by two bonds: (symb1_idx, cent_idx, symb2_idx). :param gra: molecular graph :type gra: molecular graph data structure :param symb1: symbol of atom at one end of chain :type symb1: str :param cent: symbol of atom in the middle of a chain :type cent: str :param symb2: symbol of atom at other end of chain :type symb2: str """ grps = tuple() neigh_dct = atoms_neighbor_atom_keys(gra) idx_symb_dct = atom_symbols(gra) symb_idx_dct = atom_symbol_idxs(gra) cent_idxs = symb_idx_dct.get(cent, tuple()) for cent_idx in cent_idxs: neighs = tuple(neigh_dct[cent_idx]) neigh_symbs = atom_idx_to_symb(neighs, idx_symb_dct) if neigh_symbs == (symb1, symb2): grp_idxs = (neighs[0], cent_idx, neighs[1]) elif neigh_symbs == (symb2, symb1): grp_idxs = (neighs[1], cent_idx, neighs[0]) else: grp_idxs = () if grp_idxs: grps += ((grp_idxs), ) return grps
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