Depicting Molecule Similarity Based on Fingerprints

Problem

You want to depict the 2D similarity of two molecules based on their fingerprints. See example in Figure 1.

../_images/simcalc2img-01.svg

Figure 1. Example of depiction of 2D molecule similarity

Ingredients

Difficulty level

🌶️ 🌶️

Download

Download code

simcalc2img.py

See also the Usage (simcalc2img) subsection.

Download code

simcalc2pdf.py

See also the Usage (simcalc2pdf) subsection.

Source Code

simcalc2img
#!/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.

"""Depicts 2D molecule similarity using fingerprint overlaps."""

import argparse
import enum
import io
import os
import pathlib
import sys

import numpy as np
from openeye import oechem, oedepict, oegrapheme, oegraphsim
from PIL import Image
from rich.console import Console
from rich_argparse import HelpPreviewAction, RichHelpFormatter

__SCRIPT_NAME__ = pathlib.Path(__file__).absolute().stem
__SCRIPT_DESC__ = "Depict 2D molecule similarity using fingerprint overlaps"
__SCRIPT_TOOLKITS__ = ["oechem", "oedepict", "oegrapheme", "oegraphsim"]
__SCRIPT_KEYWORDS__ = ["similarity", "fingerprints", "depiction"]
__SCRIPT_CATEGORIES__ = ["depiction"]


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

    io_group = parser.add_argument_group("Input/output options")
    io_group.add_argument(
        "--query",
        type=str,
        metavar="MOL-FILE",
        required=True,
        help="input query molecule file",
    )
    io_group.add_argument(
        "--target",
        type=str,
        metavar="MOL-FILE",
        required=True,
        help="input target molecule file",
    )
    io_group.add_argument(
        "--image",
        type=str,
        metavar="IMAGE-FILE",
        required=False,
        help="output image file (PNG, SVG) (required: %(required)s) -- if no output is provided the image will be displayed on the  screen",
    )

    fingerprint_group = parser.add_argument_group("Fingerprint options")
    fingerprint_group.add_argument(
        "--fp-type",
        "--fingerprint-type",
        type=FingerprintType,
        default=FingerprintType.Tree,
        choices=list(FingerprintType),
        help="type of fingerprint to use for similarity calculation (default: %(default)s)",
    )

    depict_group = parser.add_argument_group("Depiction options")
    depict_group.add_argument(
        "--width",
        type=float,
        default=800.0,
        help="image width (default: %(default)s)",
    )
    depict_group.add_argument(
        "--height",
        type=float,
        default=400.0,
        help="image height (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:
    """Depict molecule similarity."""
    args = parse_options()
    console = Console(record=args.save_console_svg)

    _check_image_file(args)

    query_mol = get_molecule(args.query)
    target_mol = get_molecule(args.target)

    # get fingerprint type

    fp_type = _get_fingerprint_type(args.fp_type)
    console.print(
        f"Using fingerprint type:{fp_type.GetFPTypeString()}", highlight=False
    )

    # create image

    image = oedepict.OEImage(args.width, args.height)

    # setup depiction options

    opts = oedepict.OE2DMolDisplayOptions(
        args.width, args.height, oedepict.OEScale_AutoScale
    )
    opts.SetBondWidthScaling(True)

    # depict molecules with overlaps

    depict_molecule_overlaps(image, query_mol, target_mol, fp_type, opts)

    if args.image:
        oedepict.OEWriteImage(args.image, image)
    else:
        _img = Image.open(io.BytesIO(oedepict.OEWriteImageToBytes("png", image)))
        _img.show()

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


def _set_fingerprint_similarity(
    query_mol: oechem.OEMolBase,
    target_mol: oechem.OEMolBase,
    fp_type: oegraphsim.OEFPTypeBase,
    tag: int,
    max_value: int = 0,
) -> int:
    """
    Calculate fingerprint overlap and store per-bond overlap counts.

    Computes the fingerprint overlap between query and target molecules and
    stores the number of overlapping substructure matches on each bond.
    """
    query_bonds = np.zeros(query_mol.GetMaxBondIdx(), dtype=np.uint32)
    target_bonds = np.zeros(target_mol.GetMaxBondIdx(), dtype=np.uint32)

    for match in oegraphsim.OEGetFPOverlap(query_mol, target_mol, fp_type):
        for bond in match.GetPatternBonds():
            query_bonds[bond.GetIdx()] += 1
        for bond in match.GetTargetBonds():
            target_bonds[bond.GetIdx()] += 1

    max_value = max(max_value, int(np.max(query_bonds)))
    max_value = max(max_value, int(np.max(target_bonds)))

    for bond in query_mol.GetBonds():
        bond.SetData(tag, int(query_bonds[bond.GetIdx()]))
    for bond in target_mol.GetBonds():
        bond.SetData(tag, int(target_bonds[bond.GetIdx()]))

    return max_value


class ColorBondByOverlapScore(oegrapheme.OEBondGlyphBase):
    """Bond glyph that colors bonds by fingerprint overlap score."""

    def __init__(self, color_gradient: oechem.OELinearColorGradient, tag: int) -> None:
        """Initialize the glyph with a color gradient and SD tag."""
        oegrapheme.OEBondGlyphBase.__init__(self)
        self._color_gradient = color_gradient
        self._tag = tag

    def RenderGlyph(  # noqa: N802
        self, disp: oedepict.OE2DMolDisplay, bond: oechem.OEBondBase
    ) -> bool:
        """Render the bond glyph."""
        bond_disp = disp.GetBondDisplay(bond)
        if bond_disp is None or not bond_disp.IsVisible():
            return False

        if not bond.HasData(self._tag):
            return False

        linewidth = disp.GetScale() / 3.0
        color = self._color_gradient.GetColorAt(bond.GetData(self._tag))
        pen = oedepict.OEPen(color, color, oedepict.OEFill_Off, linewidth)

        atom_disp_bgn = disp.GetAtomDisplay(bond.GetBgn())
        atom_disp_end = disp.GetAtomDisplay(bond.GetEnd())

        layer = disp.GetLayer(oedepict.OELayerPosition_Below)
        layer.DrawLine(atom_disp_bgn.GetCoords(), atom_disp_end.GetCoords(), pen)

        return True

    def ColorBondByOverlapScore(self) -> oegrapheme.OEBondGlyphBase:  # noqa: N802
        """Create a copy of this glyph."""
        return ColorBondByOverlapScore(self._color_gradient, self._tag).__disown__()


def depict_molecule_overlaps(
    image: oedepict.OEImageBase,
    query_mol: oechem.OEMolBase,
    target_mol: oechem.OEMolBase,
    fp_type: oegraphsim.OEFPTypeBase,
    opts: oedepict.OE2DMolDisplayOptions,
) -> None:
    """
    Depict query and target molecules with fingerprint overlap coloring.

    Renders query and target molecules side by side in a grid, with bonds
    colored by fingerprint overlap score and a Tanimoto similarity score
    displayed below.
    """
    tag = oechem.OEGetTag("fp-overlap")
    max_value: int = _set_fingerprint_similarity(query_mol, target_mol, fp_type, tag)

    color_gradient = oechem.OELinearColorGradient()
    color_gradient.AddStop(oechem.OEColorStop(0.0, oechem.OEPinkTint))
    color_gradient.AddStop(oechem.OEColorStop(1.0, oechem.OEYellow))
    color_gradient.AddStop(oechem.OEColorStop(max_value, oechem.OEDarkGreen))
    bond_glyph = ColorBondByOverlapScore(color_gradient, tag)

    oedepict.OEPrepareDepiction(query_mol)
    overlaps = oegraphsim.OEGetFPOverlap(query_mol, target_mol, fp_type)
    oedepict.OEPrepareMultiAlignedDepiction(target_mol, query_mol, overlaps)

    grid = oedepict.OEImageGrid(image, 1, 2)
    grid.SetMargin(oedepict.OEMargin_Bottom, 10)
    opts.SetDimensions(
        grid.GetCellWidth(), grid.GetCellHeight(), oedepict.OEScale_AutoScale
    )
    opts.SetAtomColorStyle(oedepict.OEAtomColorStyle_WhiteMonochrome)

    mol_scale = min(
        oedepict.OEGetMoleculeScale(query_mol, opts),
        oedepict.OEGetMoleculeScale(target_mol, opts),
    )
    opts.SetScale(mol_scale)

    query_disp = oedepict.OE2DMolDisplay(query_mol, opts)
    oegrapheme.OEAddGlyph(query_disp, bond_glyph, oechem.IsTrueBond())
    oedepict.OERenderMolecule(grid.GetCell(1, 1), query_disp)

    target_disp = oedepict.OE2DMolDisplay(target_mol, opts)
    oegrapheme.OEAddGlyph(target_disp, bond_glyph, oechem.IsTrueBond())
    oedepict.OERenderMolecule(grid.GetCell(1, 2), target_disp)

    qfp = oegraphsim.OEFingerPrint()
    oegraphsim.OEMakeFP(qfp, query_mol, fp_type)

    tfp = oegraphsim.OEFingerPrint()
    oegraphsim.OEMakeFP(tfp, target_mol, fp_type)

    score = oegraphsim.OETanimoto(qfp, tfp)

    font = oedepict.OEFont(
        oedepict.OEFontFamily_Default,
        oedepict.OEFontStyle_Default,
        16,
        oedepict.OEAlignment_Center,
        oechem.OEBlack,
    )
    center = oedepict.OE2DPoint(image.GetWidth() / 2.0, image.GetHeight() - 10)
    image.DrawText(center, f"Tanimoto score = {score:.3f}", font)


class FingerprintType(enum.Enum):
    """Molecule fragmentation type."""

    Tree = "tree"
    Circular = "circular"
    Path = "path"

    def __str__(self) -> str:
        """Convert to string representation."""
        return self.value


def _get_fingerprint_type(
    fp_type: FingerprintType,
) -> oegraphsim.OEFPTypeBase:
    """Return the appropriate fragmentation function based on the type."""
    match fp_type:
        case FingerprintType.Tree:
            return oegraphsim.OEGetFPType(oegraphsim.OEFPType_Tree)
        case FingerprintType.Circular:
            return oegraphsim.OEGetFPType(oegraphsim.OEFPType_Circular)
        case FingerprintType.Path:
            return oegraphsim.OEGetFPType(oegraphsim.OEFPType_Path)

    msg = f"Unsupported fingerprint type: {fp_type}"
    raise ValueError(msg)


def get_molecule(filename: str) -> oechem.OEMolBase:
    """Read a molecule from a file."""
    ifs = oechem.oemolistream()
    mol = oechem.OEGraphMol()
    if not ifs.open(filename) or not oechem.OEReadMolecule(ifs, mol):
        oechem.OEThrow.Fatal(f"Cannot read molecule from file: {filename}")

    return mol


def _check_image_file(args: argparse.Namespace) -> None:
    # script will terminate if there is some issues
    if not args.image:
        # image will be displayed on the screen
        return
    ext = pathlib.Path(args.image).suffix[1:].upper()
    if not oedepict.OEIsRegisteredImageFile(ext):
        oechem.OEThrow.Fatal("Unknown image output type!")

    ofs = oechem.oeofstream()
    if not ofs.open(args.image):
        oechem.OEThrow.Fatal("Cannot open output image file!")


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())
simcalc2pdf
#!/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.

"""Performs 2D fingerprint similarity search and depicts molecule similarities."""

import argparse
import os
import pathlib
import sys

import numpy as np
from openeye import oechem, oedepict, oegrapheme, oegraphsim
from rich.console import Console
from rich_argparse import HelpPreviewAction, RichHelpFormatter

__SCRIPT_NAME__ = pathlib.Path(__file__).absolute().stem
__SCRIPT_DESC__ = (
    "Perform 2D fingerprint similarity search and depict molecule similarities"
)
__SCRIPT_TOOLKITS__ = ["oechem", "oedepict", "oegrapheme", "oegraphsim"]
__SCRIPT_KEYWORDS__ = ["similarity", "fingerprints", "depiction"]
__SCRIPT_CATEGORIES__ = ["depiction"]


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

    io_group = parser.add_argument_group("Input options")
    io_group.add_argument(
        "--query",
        type=str,
        metavar="MOL-FILE",
        required=True,
        help="input query molecule file",
    )
    io_group.add_argument(
        "--mol",
        "--mol-file",
        type=str,
        metavar="MOL-FILE",
        required=True,
        help="input molecule database file",
    )
    io_group.add_argument(
        "--fp-database",
        type=str,
        metavar="FP-FILE",
        required=True,
        help="input fast fingerprint database file",
    )

    output_group = parser.add_argument_group("Output options")
    output_group.add_argument(
        "--report",
        type=str,
        metavar="REPORT-FILE",
        required=True,
        help="output report file (.pdf)",
    )
    output_group.add_argument(
        "--page-by-page",
        action="store_true",
        help="write pages of report to separate numbered image files (default: %(default)s)",
    )

    fp_group = parser.add_argument_group("Fingerprint database options")
    fp_group.add_argument(
        "--num-hits",
        type=int,
        default=10,
        help="number of hits to return (default: %(default)s)",
    )
    fp_group.add_argument(
        "--mem-type",
        type=str,
        default="memory-mapped",
        choices=["memory-mapped", "in-memory"],
        help="fingerprint database memory type (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:
    """Perform fingerprint similarity search and depict results."""
    args = parse_options()

    console = Console()

    _check_report_file(args)

    query_mol = get_molecule(args.query)

    # initialize databases
    mol_db = oechem.OEMolDatabase()
    if not mol_db.Open(args.mol):
        console.print(f"[red]Cannot open molecule database: {args.mol}[/red]")
        return os.EX_IOERR

    if args.mem_type == "in-memory":
        mem_type = oegraphsim.OEFastFPDatabaseMemoryType_InMemory
    else:
        mem_type = oegraphsim.OEFastFPDatabaseMemoryType_MemoryMapped

    fp_db = oegraphsim.OEFastFPDatabase(args.fp_database, mem_type)
    if not fp_db.IsValid():
        console.print(
            f"[red]Cannot open fingerprint database: {args.fp_database}[/red]"
        )
        return os.EX_IOERR

    if not oegraphsim.OEAreCompatibleDatabases(mol_db, fp_db):
        console.print("[red]Databases are not compatible![/red]")
        return 1

    fp_type = fp_db.GetFPTypeBase()
    console.print(f"Using fingerprint type: {fp_type.GetFPTypeString()}")

    # create report

    report_opts = oedepict.OEReportOptions()
    report_opts.SetHeaderHeight(120.0)
    report_opts.SetFooterHeight(25.0)
    report = oedepict.OEReport(report_opts)

    # search fingerprint database

    opts = oegraphsim.OEFPDatabaseOptions(
        args.num_hits, oegraphsim.OESimMeasure_Tanimoto
    )
    scores = list(fp_db.GetSortedScores(query_mol, opts))

    console.print(f"Found {len(scores)} hits in fingerprint database")

    depict_molecule_similarities(report, query_mol, mol_db, scores, fp_type)

    if args.page_by_page:
        oedepict.OEWriteReportPageByPage(args.report, report)
    else:
        oedepict.OEWriteReport(args.report, report)

    console.print(f"[green]Wrote report to {args.report}[/green]")

    return os.EX_OK


def depict_molecule_similarities(
    report: oedepict.OEReport,
    query_mol: oechem.OEMolBase,
    mol_db: oechem.OEMolDatabase,
    scores: list,
    fp_type: oegraphsim.OEFPTypeBase,
) -> None:
    """
    Depict fingerprint similarity between query and hit molecules.

    Renders a multi-page report with the query molecule in the header
    and hit molecules in grid cells, with bonds colored by fingerprint
    overlap score.
    """
    prep_opts = oedepict.OEPrepareDepictionOptions()
    prep_opts.SetDepictOrientation(oedepict.OEDepictOrientation_Horizontal)
    oedepict.OEPrepareDepiction(query_mol, prep_opts)

    width, height = report.GetHeaderWidth(), report.GetHeaderHeight()
    depict_opts = oedepict.OE2DMolDisplayOptions(
        width, height, oedepict.OEScale_AutoScale
    )
    query_disp = oedepict.OE2DMolDisplay(query_mol, depict_opts)
    scale = query_disp.GetScale()

    depict_opts.SetDimensions(report.GetCellWidth(), report.GetCellHeight(), scale)
    depict_opts.SetAtomColorStyle(oedepict.OEAtomColorStyle_WhiteMonochrome)

    self_score = _get_max_bond_self_similarity_score(query_mol, fp_type)

    tag = oechem.OEGetTag("fp-overlap")

    color_gradient = oechem.OELinearColorGradient()
    color_gradient.AddStop(oechem.OEColorStop(0.0, oechem.OEPinkTint))
    color_gradient.AddStop(oechem.OEColorStop(1.0, oechem.OEYellow))
    color_gradient.AddStop(oechem.OEColorStop(self_score, oechem.OEDarkGreen))

    bond_glyph = ColorBondByOverlapScore(color_gradient, tag)

    hit = oechem.OEGraphMol()
    for si in scores:
        if mol_db.GetMolecule(hit, si.GetIdx()):
            oedepict.OEPrepareDepiction(hit)
            _set_fingerprint_similarity(query_mol, hit, fp_type, tag)
            hit.SetTitle(f"Score = {si.GetScore():1.3f}")

            overlaps = oegraphsim.OEGetFPOverlap(query_mol, hit, fp_type)
            oedepict.OEPrepareMultiAlignedDepiction(hit, query_mol, overlaps)
            disp = oedepict.OE2DMolDisplay(hit, depict_opts)
            oegrapheme.OEAddGlyph(disp, bond_glyph, oechem.IsTrueBond())

            cell = report.NewCell()
            oedepict.OERenderMolecule(cell, disp)

    for header in report.GetHeaders():
        oedepict.OERenderMolecule(header, query_disp)
        oedepict.OEDrawCurvedBorder(header, oedepict.OELightGreyPen, 10)

    font = oedepict.OEFont(
        oedepict.OEFontFamily_Default,
        oedepict.OEFontStyle_Bold,
        12,
        oedepict.OEAlignment_Center,
        oechem.OEBlack,
    )
    for page_num, footer in enumerate(report.GetFooters(), start=1):
        text = f"Page {page_num} of {report.NumPages()}"
        oedepict.OEDrawTextToCenter(footer, text, font)


def _get_max_bond_self_similarity_score(
    mol: oechem.OEMolBase,
    fp_type: oegraphsim.OEFPTypeBase,
) -> int:
    """Calculate the maximum bond self-similarity score."""
    ref_bonds = np.zeros(mol.GetMaxBondIdx(), dtype=np.uint32)

    for match in oegraphsim.OEGetFPOverlap(mol, mol, fp_type):
        for bond in match.GetPatternBonds():
            ref_bonds[bond.GetIdx()] += 1

    return int(np.max(ref_bonds))


def _set_fingerprint_similarity(
    ref_mol: oechem.OEMolBase,
    fit_mol: oechem.OEMolBase,
    fp_type: oegraphsim.OEFPTypeBase,
    tag: int,
) -> None:
    """Store per-bond fingerprint overlap counts on the fit molecule."""
    fit_bonds = np.zeros(fit_mol.GetMaxBondIdx(), dtype=np.uint32)

    for match in oegraphsim.OEGetFPOverlap(ref_mol, fit_mol, fp_type):
        for bond in match.GetTargetBonds():
            fit_bonds[bond.GetIdx()] += 1

    for bond in fit_mol.GetBonds():
        bond.SetData(tag, int(fit_bonds[bond.GetIdx()]))


class ColorBondByOverlapScore(oegrapheme.OEBondGlyphBase):
    """Bond glyph that colors bonds by fingerprint overlap score."""

    def __init__(self, color_gradient: oechem.OELinearColorGradient, tag: int) -> None:
        """Initialize the glyph with a color gradient and SD tag for bond scores."""
        oegrapheme.OEBondGlyphBase.__init__(self)
        self._color_gradient = color_gradient
        self._tag = tag

    def RenderGlyph(  # noqa: N802
        self, disp: oedepict.OE2DMolDisplay, bond: oechem.OEBondBase
    ) -> bool:
        """Render the bond glyph."""
        bond_disp = disp.GetBondDisplay(bond)
        if bond_disp is None or not bond_disp.IsVisible():
            return False

        if not bond.HasData(self._tag):
            return False

        linewidth = disp.GetScale() / 3.0
        color = self._color_gradient.GetColorAt(bond.GetData(self._tag))
        pen = oedepict.OEPen(color, color, oedepict.OEFill_Off, linewidth)

        atom_disp_bgn = disp.GetAtomDisplay(bond.GetBgn())
        atom_disp_end = disp.GetAtomDisplay(bond.GetEnd())

        layer = disp.GetLayer(oedepict.OELayerPosition_Below)
        layer.DrawLine(atom_disp_bgn.GetCoords(), atom_disp_end.GetCoords(), pen)

        return True

    def ColorBondByOverlapScore(self) -> oegrapheme.OEBondGlyphBase:  # noqa: N802
        """Create a copy of this glyph."""
        return ColorBondByOverlapScore(self._color_gradient, self._tag).__disown__()


def get_molecule(filename: str) -> oechem.OEMolBase:
    """Read a molecule from a file."""
    ifs = oechem.oemolistream()
    mol = oechem.OEGraphMol()
    if not ifs.open(filename) or not oechem.OEReadMolecule(ifs, mol):
        oechem.OEThrow.Fatal(f"Cannot read molecule from file: {filename}")

    return mol


def _check_report_file(args: argparse.Namespace) -> bool:
    ext = pathlib.Path(args.report).suffix[1:]
    if not oedepict.OEIsRegisteredImageFile(ext):
        oechem.OEThrow.Fatal("Unknown image outout type!")

    if not args.page_by_page and not oedepict.OEIsRegisteredMultiPageImageFile(ext):
        oechem.OEThrow.Warning("Report will be generated into separate pages!")
        args.page_by_page = True

    return True


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())

Solution

The GraphSim TK not only provides functionality to encode 2D molecular graph information into fingerprints, but it also gives access to the fragments that are being enumerated during the fingerprint generation process. The OEGetFPOverlap function, used in this example, returns all common fragments found between two molecules based on the given fingerprint type. These fragments are used in the _set_fingerprint_similarity function to assess the similar parts of two molecules. Iterating over the bonds of the common fragments, the occurrence of each bond is counted and used as an overlap score. These scores are then attached to the corresponding bonds as generic data. The maximum overlap score is also calculated and returned by the function.

def _set_fingerprint_similarity(
    query_mol: oechem.OEMolBase,
    target_mol: oechem.OEMolBase,
    fp_type: oegraphsim.OEFPTypeBase,
    tag: int,
    max_value: int = 0,
) -> int:
    """
    Calculate fingerprint overlap and store per-bond overlap counts.

    Computes the fingerprint overlap between query and target molecules and
    stores the number of overlapping substructure matches on each bond.
    """
    query_bonds = np.zeros(query_mol.GetMaxBondIdx(), dtype=np.uint32)
    target_bonds = np.zeros(target_mol.GetMaxBondIdx(), dtype=np.uint32)

    for match in oegraphsim.OEGetFPOverlap(query_mol, target_mol, fp_type):
        for bond in match.GetPatternBonds():
            query_bonds[bond.GetIdx()] += 1
        for bond in match.GetTargetBonds():
            target_bonds[bond.GetIdx()] += 1

    max_value = max(max_value, int(np.max(query_bonds)))
    max_value = max(max_value, int(np.max(target_bonds)))

    for bond in query_mol.GetBonds():
        bond.SetData(tag, int(query_bonds[bond.GetIdx()]))
    for bond in target_mol.GetBonds():
        bond.SetData(tag, int(target_bonds[bond.GetIdx()]))

    return max_value

These bond overlap scores can be used to highlight the similar and dissimilar parts of the molecules. The ColorBondByOverlapScore bond annotation class takes a linear color gradient and draws a “stick” underneath each bond. The color of the “stick” is determined by the overlap score of the bond.

class ColorBondByOverlapScore(oegrapheme.OEBondGlyphBase):
    """Bond glyph that colors bonds by fingerprint overlap score."""

    def __init__(self, color_gradient: oechem.OELinearColorGradient, tag: int) -> None:
        """Initialize the glyph with a color gradient and SD tag."""
        oegrapheme.OEBondGlyphBase.__init__(self)
        self._color_gradient = color_gradient
        self._tag = tag

    def RenderGlyph(  # noqa: N802
        self, disp: oedepict.OE2DMolDisplay, bond: oechem.OEBondBase
    ) -> bool:
        """Render the bond glyph."""
        bond_disp = disp.GetBondDisplay(bond)
        if bond_disp is None or not bond_disp.IsVisible():
            return False

        if not bond.HasData(self._tag):
            return False

        linewidth = disp.GetScale() / 3.0
        color = self._color_gradient.GetColorAt(bond.GetData(self._tag))
        pen = oedepict.OEPen(color, color, oedepict.OEFill_Off, linewidth)

        atom_disp_bgn = disp.GetAtomDisplay(bond.GetBgn())
        atom_disp_end = disp.GetAtomDisplay(bond.GetEnd())

        layer = disp.GetLayer(oedepict.OELayerPosition_Below)
        layer.DrawLine(atom_disp_bgn.GetCoords(), atom_disp_end.GetCoords(), pen)

        return True

    def ColorBondByOverlapScore(self) -> oegrapheme.OEBondGlyphBase:  # noqa: N802
        """Create a copy of this glyph."""
        return ColorBondByOverlapScore(self._color_gradient, self._tag).__disown__()

The depict_molecule_overlaps function shows how to depict the 2D similarity of the two molecules:

  1. Calculate the bond overlap scores for both molecules. An OELinearColorGradient object is constructed that is used by the ColorBondByOverlapScore class to annotate the bonds based on their overlap score.

  2. Prepare both molecules for depiction. The target molecule is aligned to the query by calling the OEPrepareMultiAlignedDepiction function. The OEGetFPOverlap function is utilized to return all common fragments found between two molecules based on a given fingerprint type. These common fragments reveal the similar parts of the two molecules being compared that are used by the OEPrepareMultiAlignedDepiction function to find the best alignment between the molecules.

  3. Divide the image into two cells using the OEImageGrid class and render the molecules next to each other.

  4. Generate fingerprints and calculate the similarity score by calling the OETanimoto function.

  5. Render the score into the image.

You can see the result in Figure 1. The depict_molecule_overlaps function uses a “yellow to dark green” linear color gradient. Where 2D similarity is detected between the two molecules, the color green is used to highlight the bonds, and the color gets darker with increasing similarity. The color pink is used to highlight parts of the molecules that do not share any common fragments, i.e., they are 2D dissimilar.

def depict_molecule_overlaps(
    image: oedepict.OEImageBase,
    query_mol: oechem.OEMolBase,
    target_mol: oechem.OEMolBase,
    fp_type: oegraphsim.OEFPTypeBase,
    opts: oedepict.OE2DMolDisplayOptions,
) -> None:
    """
    Depict query and target molecules with fingerprint overlap coloring.

    Renders query and target molecules side by side in a grid, with bonds
    colored by fingerprint overlap score and a Tanimoto similarity score
    displayed below.
    """
    tag = oechem.OEGetTag("fp-overlap")
    max_value: int = _set_fingerprint_similarity(query_mol, target_mol, fp_type, tag)

    color_gradient = oechem.OELinearColorGradient()
    color_gradient.AddStop(oechem.OEColorStop(0.0, oechem.OEPinkTint))
    color_gradient.AddStop(oechem.OEColorStop(1.0, oechem.OEYellow))
    color_gradient.AddStop(oechem.OEColorStop(max_value, oechem.OEDarkGreen))
    bond_glyph = ColorBondByOverlapScore(color_gradient, tag)

    oedepict.OEPrepareDepiction(query_mol)
    overlaps = oegraphsim.OEGetFPOverlap(query_mol, target_mol, fp_type)
    oedepict.OEPrepareMultiAlignedDepiction(target_mol, query_mol, overlaps)

    grid = oedepict.OEImageGrid(image, 1, 2)
    grid.SetMargin(oedepict.OEMargin_Bottom, 10)
    opts.SetDimensions(
        grid.GetCellWidth(), grid.GetCellHeight(), oedepict.OEScale_AutoScale
    )
    opts.SetAtomColorStyle(oedepict.OEAtomColorStyle_WhiteMonochrome)

    mol_scale = min(
        oedepict.OEGetMoleculeScale(query_mol, opts),
        oedepict.OEGetMoleculeScale(target_mol, opts),
    )
    opts.SetScale(mol_scale)

    query_disp = oedepict.OE2DMolDisplay(query_mol, opts)
    oegrapheme.OEAddGlyph(query_disp, bond_glyph, oechem.IsTrueBond())
    oedepict.OERenderMolecule(grid.GetCell(1, 1), query_disp)

    target_disp = oedepict.OE2DMolDisplay(target_mol, opts)
    oegrapheme.OEAddGlyph(target_disp, bond_glyph, oechem.IsTrueBond())
    oedepict.OERenderMolecule(grid.GetCell(1, 2), target_disp)

    qfp = oegraphsim.OEFingerPrint()
    oegraphsim.OEMakeFP(qfp, query_mol, fp_type)

    tfp = oegraphsim.OEFingerPrint()
    oegraphsim.OEMakeFP(tfp, target_mol, fp_type)

    score = oegraphsim.OETanimoto(qfp, tfp)

    font = oedepict.OEFont(
        oedepict.OEFontFamily_Default,
        oedepict.OEFontStyle_Default,
        16,
        oedepict.OEAlignment_Center,
        oechem.OEBlack,
    )
    center = oedepict.OE2DPoint(image.GetWidth() / 2.0, image.GetHeight() - 10)
    image.DrawText(center, f"Tanimoto score = {score:.3f}", font)

Usage (simcalc2img)

See Download section to download the script.

> simcalc2img --help
../_images/simcalc2img-help.svg

Running the above command will generate the image shown in Figure 1 for fp-query.ism and fp-target.ism input files.

> simcalc2img --query fp-query.ism --target fp-target.ism --image output.svg

Discussion

Hint

Visualizing similarity of two molecules based on their fingerprints provides insight into molecule similarity beyond a single numerical score and reveals information about the underlying fingerprint methods.

The images in the following tables illustrate how changing a core or a terminal atom in a molecule affects the Tanimoto similarity scores.

Table 1. Example of the effects of changing a core atom using various fingerprint types

Path

Tree

Circular

../_images/simcalc2img-core-change-path.svg ../_images/simcalc2img-core-change-tree.svg ../_images/simcalc2img-core-change-circular.svg
Table 2. Example of the effects of changing a terminal atom using various fingerprint types

Path

Tree

Circular

../_images/simcalc2img-terminal-change-path.svg ../_images/simcalc2img-terminal-change-tree.svg ../_images/simcalc2img-terminal-change-circular.svg

Usage (simcalc2pdf)

See Download section to download the script.

The example above shows how to visualize the molecule similarity of two molecules, however you might want to visualize the similarity between one ‘query’ molecule against a set of ‘target’ molecules. The example below reads a pre-generated binary fingerprint file (see more details in Rapid Similarity Searching of Large Molecule Files) and then generates a multi-page document depicting the most similar hits aligned to the ‘query’ molecule.

> simcalc2pdf --help
../_images/simcalc2pdf-help.svg

Running the above command will generate the image shown in Table 3 for query.ism, targets.ism and targets-tree.fpbin input files.

> simcalc2pdf --query query.ism --mol targets.ism --fp-database targets-tree.fpbin --report report.pdf
Table 3. Example of depiction of 2D molecule similarities (The pages are reduced here for visualization convenience)

page 1

page 2

../_images/simcalc2pdf-01-01.svg ../_images/simcalc2pdf-01-02.svg

See also in OEChem TK manual

Theory

API

See also in GraphSim TK manual

Theory

API

See also in OEDepict TK manual

Theory

API

See also in GraphemeTM TK manual

Theory

API