Beispiel #1
0
 def _is_compatible(sgr):
     atm_ste_par_dct = _assigned(atom_stereo_parities(sgr))
     bnd_ste_par_dct = _assigned(bond_stereo_parities(sgr))
     _compat_atm_assgns = (set(known_atm_ste_par_dct.items()) <= set(
         atm_ste_par_dct.items()))
     _compat_bnd_assgns = (set(known_bnd_ste_par_dct.items()) <= set(
         bnd_ste_par_dct.items()))
     return _compat_atm_assgns and _compat_bnd_assgns
Beispiel #2
0
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
Beispiel #3
0
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), (
        f"Not an explicit graph:\n{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 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
Beispiel #4
0
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
Beispiel #5
0
def _atom_stereo_layer(gra):
    """ AMChI atom stereo (t, m) layer from graph

        S = counterclockwise = '@'  = '-' = False
        R = clockwise        = '@@' = '+' = True

        :param gra: molecular graph
        :type gra: automol graph data structure
        :rtype: (str, str)
    """
    # Generate the atom stereo layer strings
    atm_keys = sorted(atom_stereo_keys(gra))
    atm_par_dct = atom_stereo_parities(gra)

    atm_pars = list(map(atm_par_dct.__getitem__, atm_keys))
    atm_sgns = ['+' if p else '-' for p in atm_pars]
    atm_strs = [f'{i+1}{s}' for i, s in zip(atm_keys, atm_sgns)]

    atm_ste_lyr = ','.join(atm_strs)
    return atm_ste_lyr
Beispiel #6
0
def substereomers(gra):
    """ all stereomers compatible with this graph's assignments
    """
    _assigned = functools.partial(dict_.filter_by_value,
                                  func=lambda x: x is not None)

    known_atm_ste_par_dct = _assigned(atom_stereo_parities(gra))
    known_bnd_ste_par_dct = _assigned(bond_stereo_parities(gra))

    def _is_compatible(sgr):
        atm_ste_par_dct = _assigned(atom_stereo_parities(sgr))
        bnd_ste_par_dct = _assigned(bond_stereo_parities(sgr))
        _compat_atm_assgns = (set(known_atm_ste_par_dct.items()) <= set(
            atm_ste_par_dct.items()))
        _compat_bnd_assgns = (set(known_bnd_ste_par_dct.items()) <= set(
            bnd_ste_par_dct.items()))
        return _compat_atm_assgns and _compat_bnd_assgns

    sgrs = tuple(filter(_is_compatible, stereomers(gra)))
    return sgrs
Beispiel #7
0
def _stereo_corrected_geometry(sgr, geo, geo_idx_dct):
    """ correct the stereo parities of a geometry

    (works iterately to handle cases of higher-order stereo)
    """
    assert sgr == explicit(sgr)
    gra = without_stereo_parities(sgr)

    if has_stereo(sgr):
        full_atm_ste_par_dct = atom_stereo_parities(sgr)
        full_bnd_ste_par_dct = bond_stereo_parities(sgr)

        atm_keys = set()
        bnd_keys = set()

        last_gra = None

        while last_gra != gra:
            last_gra = gra

            atm_keys.update(stereogenic_atom_keys(gra))
            bnd_keys.update(stereogenic_bond_keys(gra))

            atm_ste_par_dct = {
                atm_key: full_atm_ste_par_dct[atm_key]
                for atm_key in atm_keys
            }
            bnd_ste_par_dct = {
                bnd_key: full_bnd_ste_par_dct[bnd_key]
                for bnd_key in bnd_keys
            }
            geo, gra = _atom_stereo_corrected_geometry(gra, atm_ste_par_dct,
                                                       geo, geo_idx_dct)
            geo, gra = _bond_stereo_corrected_geometry(gra, bnd_ste_par_dct,
                                                       geo, geo_idx_dct)

    return geo
Beispiel #8
0
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), (
        f"Not an explicit graph:\n{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 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"
            f"{str(atm_ste_keys)} != {str(atm_keys_pool)}")
        assert bnd_ste_keys == bnd_keys_pool, (
            "Index-based to absolute stereo conversion failed:\n"
            f"{str(bnd_ste_keys)} != {str(bnd_keys_pool)}")

    return gra
Beispiel #9
0
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