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.
Figure 1. Example of depiction of 2D molecule similarity
Ingredients
|
Difficulty level
🌶️ 🌶️
Download
Download code
See also the Usage (simcalc2img) subsection.
Download code
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:
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.
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.
Divide the image into two cells using the OEImageGrid class and render the molecules next to each other.
Generate fingerprints and calculate the similarity score by calling the OETanimoto function.
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
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.
Path |
Tree |
Circular |
Path |
Tree |
Circular |
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
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
page 1 |
page 2 |
See also in OEChem TK manual
Theory
Generic Data chapter
API
OELinearColorGradient class
See also in GraphSim TK manual
Theory
Fingerprint Generation chapter
Similarity Measures chapter
Fingerprint Overlap chapter
API
OEFingerPrint class
OEGetFPOverlap function
OEMakeFP function
OETanimoto function
See also in OEDepict TK manual
Theory
Molecule Depiction chapter
API
OE2DMolDisplay class
OE2DMolDisplayOptions class
OEImage class
OEImageGrid class
OEPrepareDepiction function
OEPrepareMultiAlignedDepiction function
OERenderMolecule function
See also in GraphemeTM TK manual
Theory
Annotating Atoms and Bonds chapter
API
OEAddGlyph function
OEBondGlyphBase abstract base class