🆕 Similarity Search in Monomer Set

Problem

You want to find similar monomers either within OEChem TK’s built-in monomer sets or in a custom set defined in a json file.

Ingredients

Difficulty Level

🌶️

Download

Download code

monomersim.py

See also Usage subsection.

Source Code

monomersim
#!/usr/bin/env python3
# (C) 2026 Cadence Design Systems, Inc. (Cadence)
# All rights reserved.
# TERMS FOR USE OF SAMPLE CODE The software below ("Sample Code") is
# provided to current licensees or subscribers of Cadence products or
# SaaS offerings (each a "Customer").
# Customer is hereby permitted to use, copy, and modify the Sample Code,
# subject to these terms. Cadence claims no rights to Customer's
# modifications. Modification of Sample Code is at Customer's sole and
# exclusive risk. Sample Code may require Customer to have a then
# current license or subscription to the applicable Cadence offering.
# THE SAMPLE CODE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED.  OPENEYE DISCLAIMS ALL WARRANTIES, INCLUDING, BUT
# NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
# PARTICULAR PURPOSE AND NONINFRINGEMENT. In no event shall Cadence be
# liable for any damages or liability in connection with the Sample Code
# or its use.

"""Similarity search in monomer set."""

import argparse
import json
import os
import pathlib
import sys

import rich.console
import rich.table
from openeye import oechem, oegraphsim
from rich_argparse import HelpPreviewAction, RichHelpFormatter

__SCRIPT_NAME__ = pathlib.Path(__file__).absolute().stem
__SCRIPT_DESC__ = "Similarity search in monomer set."
__SCRIPT_TOOLKITS__ = ["oechem", "oegraphsim"]
__SCRIPT_KEYWORDS__ = [
    "monomer",
    "peptide",
    "peptide-informatics",
    "similarity",
    "search",
]
__SCRIPT_CATEGORIES__ = ["peptide-informatics"]

MONOMER_SET_STANDARD = "Standard"
MONOMER_SET_OPENEYE = "OpenEye"


def parse_options() -> argparse.Namespace:
    """Parse command-line options for monomer similarity search."""
    parser = argparse.ArgumentParser(
        add_help=True,
        formatter_class=RichHelpFormatter,
        description="[yellow]" + __SCRIPT_DESC__ + "[/yellow]",
    )

    search_group = parser.add_argument_group("Query options")
    exclusive_search_group = search_group.add_mutually_exclusive_group(required=True)
    exclusive_search_group.add_argument(
        "--smiles",
        type=str,
        metavar="SMILES",
        help="smiles of the query structure",
    )
    exclusive_search_group.add_argument(
        "--code",
        type=str,
        help="monomer code in any code-set in loaded monomers",
    )

    monomers_group = parser.add_argument_group("Monomer set options")
    _add_monomer_collection(monomers_group)

    sim_group = parser.add_argument_group("Similarity options")
    sim_group.add_argument(
        "--cutoff",
        type=float,
        metavar="[0.0, 1.0]",
        default=0.3,
        help="similarity score cutoff value  in range (default: %(default)s)",
    )
    sim_group.add_argument(
        "--limit",
        type=int,
        metavar="INT",
        default=20,
        help="maximum number of similar monomers (default: %(default)s)",
    )

    parser.add_argument("--help-image", action=HelpPreviewAction)
    parser.add_argument(
        "--save-console-svg",
        default=False,
        action="store_true",
        help=f"run command and capture console output in {__SCRIPT_NAME__}.svg file",
    )

    return parser.parse_args()


def main() -> int:
    """Run monomer similarity search and print results."""
    args = parse_options()

    console = rich.console.Console(record=args.save_console_svg, highlight=False)
    monomers = _get_monomer_collection(args)

    query_mol = _get_query_mol(args, monomers, console)
    if query_mol is None:  # warning was already printed
        return os.EX_DATAERR
    console.print(f"[green]Query {oechem.OEMolToSmiles(query_mol)}[/green]")

    # generating fingerprints
    fp_type = oegraphsim.OEGetFPType(oegraphsim.OEFPType_Path)
    console.print(f"[green]Using fingerprint {fp_type.GetFPTypeString()}[/green]")
    query_fp = oegraphsim.OEFingerPrint()
    oegraphsim.OEMakeFP(query_fp, query_mol, fp_type)

    scores: list[tuple[float, oechem.OEMonomer]] = []

    for monomer in monomers.GetMonomers():
        score = _get_sim_score(monomer, query_fp)
        scores.append((score, monomer))
        oegraphsim.OEMakeFP(query_fp, query_mol, fp_type)

    limit = min(max(1, args.limit), len(scores))
    scores.sort(key=lambda s: s[0], reverse=True)
    scores = scores[:limit]
    cutoff = min(max(0.01, args.cutoff), 1.0)

    primary_code_set = monomers.GetPrimaryCodeSet()
    code_sets: list[str] = [
        primary_code_set,
        *[c for c in monomers.GetCodeSets() if c != primary_code_set],
    ]
    columns = ["idx", "tanimoto", *code_sets, "monomer smiles"]
    table = rich.table.Table(*columns)

    color_gradient = oechem.OELinearColorGradient()
    color_gradient.AddStop(oechem.OEColorStop(0.0, oechem.OERedOrange))
    color_gradient.AddStop(oechem.OEColorStop(1.0, oechem.OESeaGreen))
    for index, (score, monomer) in enumerate(scores, start=1):
        if score < cutoff:
            break
        score_color = color_gradient.GetColorAt(score)
        row_data: list[str | rich.text.Text] = [
            rich.text.Text(f"{index}", justify="right"),
            rich.text.Text(
                f"{score:.4f}", style=score_color.GetText(), justify="right"
            ),
            *[
                rich.text.Text(monomer.GetCode(s)) if monomer.HasCode(s) else ""
                for s in code_sets
            ],
            monomer.GetCanonicalSmiles(),
        ]
        table.add_row(*row_data)

    console.print(table, markup=False)

    if args.save_console_svg:
        console.save_svg(f"{__SCRIPT_NAME__}.svg", title="output")
    return os.EX_OK


def _get_sim_score(
    monomer: oechem.OEMonomer, query_fp: oegraphsim.OEFingerPrint
) -> float:
    """Compute Tanimoto similarity between a monomer and a query fingerprint."""
    mol = oechem.OEGraphMol()
    oechem.OESmilesToMol(mol, monomer.GetCanonicalSmiles())
    fp = oegraphsim.OEFingerPrint()
    oegraphsim.OEMakeFP(fp, mol, query_fp.GetFPTypeBase())
    return oegraphsim.OETanimoto(query_fp, fp)


class MonomerSetParameter:  # noqa: PLW1641
    """Utility class to handle both built-in and user defined monomer sets."""

    def __init__(self) -> None:  # noqa: D107
        self._monomer_sets = [
            MONOMER_SET_STANDARD,
            MONOMER_SET_OPENEYE,
            "JSON-FILENAME",
        ]

    def __repr__(self) -> str:  # noqa: D105
        return ",".join(self._monomer_sets)

    def __eq__(self, param: object) -> bool:  # noqa: D105
        if not isinstance(param, str):
            return False
        if param in [MONOMER_SET_STANDARD, MONOMER_SET_OPENEYE]:
            return True

        console = rich.console.Console()
        monomer_set_filepath = pathlib.Path(param)
        if (
            not monomer_set_filepath.exists()
            or monomer_set_filepath.suffix.lower() != ".json"
        ):
            console.print(f"[red]Invalid monomer set file '{param}' ![/red]")
            return False

        is_valid = True
        try:
            with monomer_set_filepath.open("r") as json_file:
                json.load(json_file)
        except json.JSONDecodeError as e:
            console.print(f"[red]Invalid monomer set file '{param}' ![/red]")
            console.print(f"[red]Error decoding JSON: {e} ![/red]")
            is_valid = False
        return is_valid


def _add_monomer_collection(arg_group: argparse._ArgumentGroup) -> None:
    arg_group.add_argument(
        "-m",
        "--monomers",
        type=str,
        default=MONOMER_SET_STANDARD,
        choices=[MonomerSetParameter()],
        help="built-in monomer-set type or json file of monomers",
    )


def _get_monomer_collection(args: argparse.Namespace) -> oechem.OEMonomerSet:
    monomers = oechem.OEMonomerSet()
    match args.monomers:
        case "Standard":
            oechem.OELoadStandardMonomerSet(monomers)
        case "OpenEye":
            oechem.OELoadOpenEyeMonomerSet(monomers)
        case _:
            oechem.OEReadMonomerSet(monomers, args.monomers)
    return monomers


def _get_query_mol(
    args: argparse.Namespace,
    monomers: oechem.OEMonomerSet,
    console: rich.console.Console,
) -> None | oechem.OEMolBase:
    query_mol = oechem.OEGraphMol()
    # --smiles
    if args.smiles:
        opts = oechem.OEParseSmilesOptions()
        opts.SetQuiet(True)
        if (
            not oechem.OEParseSmiles(query_mol, args.smiles, opts)
            or query_mol.NumAtoms() == 0
        ):
            console.print(f"[red]Error: invalid SMILES '{args.smiles}'![/red]")
            return None
        return query_mol

    # --code
    for code_set in monomers.GetCodeSets():
        if (monomer := monomers.GetMonomer(code_set, args.code)) is not None:
            oechem.OESmilesToMol(query_mol, monomer.GetCanonicalSmiles())
            return query_mol

    console.print(f"[red]Error: no monomer with code '{args.code}' identified![/red]")
    return None


setattr(main, "__SCRIPT_NAME__", __SCRIPT_NAME__)
setattr(main, "__SCRIPT_DESC__", __SCRIPT_DESC__)
setattr(main, "__SCRIPT_TOOLKITS__", __SCRIPT_TOOLKITS__)
setattr(main, "__SCRIPT_KEYWORDS__", __SCRIPT_KEYWORDS__)
setattr(main, "__SCRIPT_CATEGORIES__", __SCRIPT_CATEGORIES__)

if __name__ == "__main__":
    sys.exit(main())

Discussion

The monomersim script uses the path fingerprint type and the OETanimoto similarity measure by default. The GraphSim TK API supports many other fingerprint types and similarity measures, which can be easily substituted in the script. For more details see links in See also in GraphSim TK manual section.

Usage

See Download section to download the script.

> monomersim --help
../_images/monomersim-help.svg
--code

Search for monomers in the OpenEye monomer set that are similar to phenylalanine.

> monomersim --code F --monomers OpenEye
../_images/monomersim-01-stdout.svg
--smiles

The query molecule can also be specified by --smiles option.

> monomersim --smiles 'c1cc(cc(c1)F)C[C@@H](C(=O)O)N' --monomers OpenEye
../_images/monomersim-02-stdout.svg

See also in OEChem TK manual

API

See also in GraphSim TK manual

Theory

API