def reactants_graph(tsg): """ get a graph of the reactants from a transition state graph """ frm_bnd_keys = forming_bond_keys(tsg) ord_dct = dict_.transform_values(bond_orders(tsg), func=round) gra = set_bond_orders(tsg, ord_dct) gra = remove_bonds(gra, frm_bnd_keys) return gra
def resonance_dominant_bond_orders(rgr): """ resonance-dominant bond orders, by bond """ rgr = without_fractional_bonds(rgr) bnd_keys = list(bond_keys(rgr)) bnd_ords_by_res = [ dict_.values_by_key(bond_orders(dom_rgr), bnd_keys) for dom_rgr in dominant_resonances(rgr) ] bnd_ords_lst = list(map(frozenset, zip(*bnd_ords_by_res))) bnd_dom_res_ords_dct = dict(zip(bnd_keys, bnd_ords_lst)) return bnd_dom_res_ords_dct
def _add_pi_bonds(rgr, bnd_ord_inc_dct): """ add pi bonds to this graph """ bnd_keys = bond_keys(rgr) assert set(bnd_ord_inc_dct.keys()) <= bnd_keys bnd_keys = list(bnd_keys) bnd_ords = dict_.values_by_key(bond_orders(rgr), bnd_keys) bnd_ord_incs = dict_.values_by_key(bnd_ord_inc_dct, bnd_keys, fill_val=0) new_bnd_ords = numpy.add(bnd_ords, bnd_ord_incs) bnd_ord_dct = dict(zip(bnd_keys, new_bnd_ords)) rgr = set_bond_orders(rgr, bnd_ord_dct) return rgr
def resonance_avg_bond_orders(rgr): """ resonance-dominant bond orders, by bond """ rgr = without_fractional_bonds(rgr) bnd_keys = list(bond_keys(rgr)) bnd_ords_by_res = [ dict_.values_by_key(bond_orders(dom_rgr), bnd_keys) for dom_rgr in dominant_resonances(rgr) ] nres = len(bnd_ords_by_res) bnd_ords_lst = zip(*bnd_ords_by_res) avg_bnd_ord_lst = [sum(bnd_ords) / nres for bnd_ords in bnd_ords_lst] avg_bnd_ord_dct = dict(zip(bnd_keys, avg_bnd_ord_lst)) return avg_bnd_ord_dct
def from_graph(gra): """ networkx graph object from a molecular graph """ nxg = networkx.Graph() nxg.add_nodes_from(atom_keys(gra)) nxg.add_edges_from(bond_keys(gra)) networkx.set_node_attributes(nxg, atom_symbols(gra), 'symbol') networkx.set_node_attributes(nxg, atom_implicit_hydrogen_valences(gra), 'implicit_hydrogen_valence') networkx.set_node_attributes(nxg, atom_stereo_parities(gra), 'stereo_parity') networkx.set_edge_attributes(nxg, bond_orders(gra), 'order') networkx.set_edge_attributes(nxg, bond_stereo_parities(gra), 'stereo_parity') return nxg
def bonds_of_order(gra, mbond=1): """ Determine the indices of all bonds in a molecule with the specified bond order. :param gra: molecular graph :type gra: molecular graph data structure :param mbond: bond order of desired bond type :type mbond: int """ # Build resonance graph to get the bond orders gra = dominant_resonance(gra) bond_order_dct = bond_orders(gra) mbond_idxs = tuple() for bond, order in bond_order_dct.items(): if order == mbond: bnd1, bnd2 = bond mbond_idxs += ((bnd1, bnd2), ) return mbond_idxs
def subresonances(rgr): """ this connected graph and its lower-spin (more pi-bonded) resonances """ rgr = without_fractional_bonds(rgr) # get the bond capacities (room for increasing bond order), filtering out # the negative ones to avoid complications with hypervalent atoms in TSs bnd_cap_dct = dict_.by_value(_bond_capacities(rgr), lambda x: x > 0) ret_rgrs = [] if bnd_cap_dct: bnd_keys, bnd_caps = zip(*bnd_cap_dct.items()) atm_keys = list(functools.reduce(frozenset.union, bnd_keys)) # Loop over all possible combinations of bond order increments (amounts # by which to increase the bond order), filtering out combinations that # exceed the valences of the atoms involved. # (Note that we are only testing the bonds with available pi electrons, # so this is compatible with having hypervalent atoms elsewhere in the # molecule) bnd_ord_inc_ranges = [range(bnd_cap + 1) for bnd_cap in bnd_caps] for bnd_ord_incs in itertools.product(*bnd_ord_inc_ranges): bnd_ord_inc_dct = dict(zip(bnd_keys, bnd_ord_incs)) ret_rgr = _add_pi_bonds(rgr, bnd_ord_inc_dct) max_bnd_ord = max(bond_orders(ret_rgr).values()) atm_unsat_vlcs = dict_.values_by_key( atom_unsaturated_valences(ret_rgr), atm_keys) if not any(atm_unsat_vlc < 0 for atm_unsat_vlc in atm_unsat_vlcs): if max_bnd_ord < 4: ret_rgrs.append(ret_rgr) if not ret_rgrs: ret_rgrs = (rgr, ) else: ret_rgrs = tuple(ret_rgrs) return ret_rgrs
def breaking_bond_keys(tsg): """ get the forming bonds from a transition state graph """ ord_dct = bond_orders(tsg) brk_bnd_keys = [k for k, o in ord_dct.items() if round(o, 1) == 0.9] return frozenset(map(frozenset, brk_bnd_keys))
def smiles(gra, stereo=True, local_stereo=False, res_stereo=False): """ SMILES string from graph :param gra: molecular graph :type gra: automol graph data structure :param stereo: Include stereo? :type stereo: bool :param local_stereo: Is the graph using local stereo assignments? That is, are they based on atom keys rather than canonical keys? :type local_stereo: bool :param res_stereo: allow resonant double-bond stereo? :type res_stereo: bool :returns: the SMILES string :rtype: str """ assert is_connected(gra), ( "Cannot form connection layer for disconnected graph.") if not stereo: gra = without_stereo_parities(gra) # If not using local stereo assignments, canonicalize the graph first. # From this point on, the stereo parities can be assumed to correspond to # the neighboring atom keys. if not local_stereo: gra = canonical(gra) # Convert to implicit graph gra = implicit(gra) # Insert hydrogens necessary for bond stereo gra = _insert_stereo_hydrogens(gra) # Find a dominant resonance rgr = dominant_resonance(gra) # Determine atom symbols symb_dct = atom_symbols(rgr) # Determine atom implicit hydrogens nhyd_dct = atom_implicit_hydrogen_valences(rgr) # Determine bond orders for this resonance bnd_ord_dct = bond_orders(rgr) # Find radical sites for this resonance rad_atm_keys = radical_atom_keys_from_resonance(rgr) # Determine neighbors nkeys_dct = atoms_neighbor_atom_keys(rgr) # Find stereo parities atm_par_dct = dict_.filter_by_value(atom_stereo_parities(rgr), lambda x: x is not None) bnd_par_dct = dict_.filter_by_value(bond_stereo_parities(rgr), lambda x: x is not None) # Remove stereo parities if requested if not res_stereo: print('before') print(bnd_par_dct) bnd_par_dct = dict_.filter_by_key(bnd_par_dct, lambda x: bnd_ord_dct[x] == 2) print('after') print(bnd_par_dct) else: raise NotImplementedError("Not yet implemented!") def _atom_representation(key, just_seen=None, nkeys=(), closures=()): symb = ptab.to_symbol(symb_dct[key]) nhyd = nhyd_dct[key] needs_brackets = key in rad_atm_keys or symb not in ORGANIC_SUBSET hyd_rep = f'H{nhyd}' if nhyd > 1 else ('H' if nhyd == 1 else '') par_rep = '' if key in atm_par_dct: needs_brackets = True skeys = [just_seen] if nhyd: assert nhyd == 1 skeys.append(-numpy.inf) if closures: skeys.extend(closures) skeys.extend(nkeys) can_par = atm_par_dct[key] smi_par = can_par ^ util.is_odd_permutation(skeys, sorted(skeys)) par_rep = '@@' if smi_par else '@' if needs_brackets: rep = f'[{symb}{par_rep}{hyd_rep}]' else: rep = f'{symb}' return rep # Get the pool of stereo bonds for the graph and set up a dictionary for # storing the ending representation. ste_bnd_key_pool = list(bnd_par_dct.keys()) drep_dct = {} def _bond_representation(key, just_seen=None): key0 = just_seen key1 = key # First, handle the bond order if key0 is None or key1 is None: rep = '' else: bnd_ord = bnd_ord_dct[frozenset({key0, key1})] if bnd_ord == 1: rep = '' elif bnd_ord == 2: rep = '=' elif bnd_ord == 3: rep = '#' else: raise ValueError("Bond orders greater than 3 not permitted.") drep = drep_dct[(key0, key1)] if (key0, key1) in drep_dct else '' bnd_key = next((b for b in ste_bnd_key_pool if key1 in b), None) if bnd_key is not None: # We've encountered a new stereo bond, so remove it from the pool ste_bnd_key_pool.remove(bnd_key) # Determine the atoms involved key2, = bnd_key - {key1} nkey1s = set(nkeys_dct[key1]) - {key2} nkey2s = set(nkeys_dct[key2]) - {key1} nmax1 = max(nkey1s) nmax2 = max(nkey2s) nkey1 = just_seen if just_seen in nkey1s else nmax1 nkey2 = nmax2 # Determine parity can_par = bnd_par_dct[bnd_key] smi_par = can_par if nkey1 == nmax1 else not can_par # Determine bond directions drep1 = drep if drep else '/' if just_seen in nkey1s: drep = drep1 flip = not smi_par else: drep_dct[(key1, nkey1)] = drep1 flip = smi_par drep2 = _flip_direction(drep1, flip=flip) drep_dct[(key2, nkey2)] = drep2 rep += drep # Second, handle directionality (bond stereo) return rep # Get the pool of rings for the graph and set up a dictionary for storing # their tags. As the SMILES is built, each next ring that is encountered # will be given a tag, removed from the pool, and transferred to the tag # dictionary. rng_pool = list(rings_atom_keys(rgr)) rng_tag_dct = {} def _ring_representation_with_nkeys_and_closures(key, nkeys=()): nkeys = nkeys.copy() # Check for new rings in the ring pool. If a new ring is found, create # a tag, add it to the tags dictionary, and drop it from the rings # pool. for new_rng in rng_pool: if key in new_rng: # Choose a neighbor key for SMILES ring closure clos_nkey = sorted(set(new_rng) & set(nkeys))[0] # Add it to the ring tag dictionary with the current key first # and the closure key last tag = max(rng_tag_dct.values(), default=0) + 1 assert tag < 10, ( f"Ring tag exceeds 10 for this graph:\n{string(gra)}") rng = cycle_ring_atom_key_to_front(new_rng, key, clos_nkey) rng_tag_dct[rng] = tag # Remove it from the pool of unseen rings rng_pool.remove(new_rng) tags = [] closures = [] for rng, tag in rng_tag_dct.items(): if key == rng[-1]: nkeys.remove(rng[0]) closures.append(rng[0]) # Handle the special case where the last ring bond has stereo if (rng[-1], rng[0]) in drep_dct: drep = drep_dct[(rng[-1], rng[0])] tags.append(f'{drep}{tag}') else: tags.append(f'{tag}') if key == rng[0]: nkeys.remove(rng[-1]) closures.append(rng[-1]) tags.append(f'{tag}') rrep = ''.join(map(str, tags)) return rrep, nkeys, closures # Determine neighboring keys nkeys_dct_pool = dict_.transform_values(atoms_neighbor_atom_keys(rgr), sorted) def _recurse_smiles(smi, lst, key, just_seen=None): nkeys = nkeys_dct_pool.pop(key) if key in nkeys_dct_pool else [] # Remove keys just seen from the list of neighbors, to avoid doubling # back. if just_seen in nkeys: nkeys.remove(just_seen) # Start the SMILES string and connection list. The connection list is # used for sorting. rrep, nkeys, closures = _ring_representation_with_nkeys_and_closures( key, nkeys) arep = _atom_representation(key, just_seen, nkeys, closures=closures) brep = _bond_representation(key, just_seen) smi = f'{brep}{arep}{rrep}' lst = [key] # Now, extend the layer/list along the neighboring atoms. if nkeys: # Build sub-strings/lists by recursively calling this function. sub_smis = [] sub_lsts = [] while nkeys: nkey = nkeys.pop(0) sub_smi, sub_lst = _recurse_smiles('', [], nkey, just_seen=key) sub_smis.append(sub_smi) sub_lsts.append(sub_lst) # If this is a ring, remove the neighbor on the other side of # `key` to prevent repetition as we go around the ring. if sub_lst[-1] == key: nkeys.remove(sub_lst[-2]) # Now, join the sub-layers and lists together. # If there is only one neighbor, we joint it as # {arep1}{brep2}{arep2}... if len(sub_lsts) == 1: sub_smi = sub_smis[0] sub_lst = sub_lsts[0] # Extend the SMILES string smi += f'{sub_smi}' # Extend the list lst.extend(sub_lst) # If there are multiple neighbors, we joint them as # {arep1}({brep2}{arep2}...)({brep3}{arep3}...){brep4}{arep4}... else: assert len(sub_lsts) > 1 # Extend the SMILES string smi += (''.join(map("({:s})".format, sub_smis[:-1])) + sub_smis[-1]) # Append the lists of neighboring branches. lst.append(sub_lsts) return smi, lst # If there are terminal atoms, start from the first one atm_keys = atom_keys(rgr) term_keys = terminal_atom_keys(gra, heavy=False) start_key = min(term_keys) if term_keys else min(atm_keys) smi, _ = _recurse_smiles('', [], start_key) return smi