# -*- coding: utf-8 -*-
"""Contains classes for finding custom fonts & rendering molecules into figures."""
import os
import re
import sys
from io import BytesIO
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Union
import matplotlib.font_manager as fm
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import numpy as np
from job_tqdflex import ParallelApplier
from matplotlib import cm
from PIL import Image
from rdkit import Chem
from rdkit.Chem import AllChem, Draw
from .chem.interface import MoleculeHandler
from .logger import logger
if sys.version_info >= (3, 9):
from importlib.resources import files
else:
from importlib_resources import files
[docs]
class FontManager:
[docs]
def __init__(self, wsl: Union[bool, str] = "auto"):
"""Initialize the FontManager class. It will search for fonts in the system and
in matplotlib.
Args:
wsl: If you're using windows subsystem for linux. Allows for using fonts
installed in windows. Defaults to "auto".
"""
self.wsl = wsl
self.fonts = None
@property
def operating_system(self):
if os.name == "nt":
return "windows"
elif os.name == "posix":
if sys.platform.startswith("linux"):
if self.wsl == "auto":
# Auto-detect WSL
try:
wsl_pattern = re.compile(r"wsl|windows", re.IGNORECASE)
with open("/proc/version", "r") as f:
content = f.read()
if wsl_pattern.findall(content):
return "wsl"
except (FileNotFoundError, PermissionError):
pass
elif self.wsl:
return "wsl"
return "linux"
elif sys.platform.startswith("darwin"):
return "macos"
return "Unknown"
@property
def available_fonts(self) -> Dict[str, Path]:
font_dict = dict()
font_extensions = ["*.ttf", "*.otf", "*.eot", "*.woff", "*.woff2", "*.svg"]
if self.operating_system == "windows":
default_font_dir = Path("C:/Windows/Fonts")
if default_font_dir.exists():
for ext in font_extensions:
font_paths = sorted(list(default_font_dir.glob(ext)))
font_dict.update({_path.stem: _path for _path in font_paths})
font_dir = Path.home() / "AppData/Local/Microsoft/Windows/Fonts"
if font_dir.exists():
for ext in font_extensions:
font_paths = sorted(list(font_dir.glob(ext)))
font_dict.update({_path.stem: _path for _path in font_paths})
elif self.operating_system == "wsl":
# Search within the default windows path
default_font_dir = Path("/mnt/c/Windows/Fonts")
if default_font_dir.exists():
for ext in font_extensions:
font_paths = sorted(list(default_font_dir.glob(ext)))
font_dict.update({_path.stem: _path for _path in font_paths})
user_dir = Path("/mnt/c/Users/")
font_dirs = list(user_dir.glob("*/AppData/Local/Microsoft/Windows/Fonts"))
if len(font_dirs) > 1:
print("More than one user font path found.")
for _dir in font_dirs:
if _dir.exists():
for ext in font_extensions:
font_paths = sorted(list(_dir.glob(ext)))
font_dict.update({_path.stem: _path for _path in font_paths})
elif self.operating_system == "linux":
font_dirs = [
Path("/usr/share/fonts"),
Path("/usr/local/share/fonts"),
Path("~/.fonts").expanduser(),
]
for _dir in font_dirs:
if _dir.exists():
for ext in font_extensions:
font_paths = sorted(list(_dir.rglob(ext)))
font_dict.update({_path.stem: _path for _path in font_paths})
elif self.operating_system == "macos":
font_dirs = [
Path("/System/Library/Fonts"),
Path("/Library/Fonts"),
Path("~/Library/Fonts").expanduser(),
]
for _dir in font_dirs:
if _dir.exists():
for ext in font_extensions:
font_paths = sorted(list(_dir.rglob(ext)))
font_dict.update({_path.stem: _path for _path in font_paths})
# Add fonts from matplotlib
try:
font_dict.update(
{font.name: Path(font.fname) for font in fm.fontManager.ttflist}
)
except Exception as e:
print(f"Warning: Could not load matplotlib fonts: {e}")
# Add fonts from RDKit
try:
from rdkit import RDPaths
rdkit_data_dir = Path(RDPaths.RDDataDir)
rdkit_font_dir = rdkit_data_dir / "Fonts"
if rdkit_font_dir.exists():
font_paths = sorted(list(rdkit_font_dir.glob("*.ttf")))
font_dict.update({_path.stem: _path for _path in font_paths})
except Exception as e:
print(f"Warning: Could not load RDKit fonts: {e}")
return font_dict
[docs]
def get_font_path(self, font_name: str) -> Union[Path, None]:
"""Get the path to a specific font by name.
Args:
font_name: Name of the font to find
Returns:
Path to the font file or None if not found
"""
fonts = self.available_fonts
return fonts.get(font_name)
[docs]
def list_available_fonts(self) -> list:
"""Return a list of all available font names."""
return list(self.available_fonts.keys())
[docs]
def has_font(self, font_name: str) -> bool:
"""Check if a font is available.
Args:
font_name: Name of the font to check
Returns:
True if font is available, False otherwise
"""
return font_name in self.available_fonts
[docs]
class MolPlotter(MoleculeHandler):
"""MolPlotter class to render molecules into images. It uses RDKit's
DrawMolecule method to generate the images. The images can be rendered with
labels and/or substructure matches.
Attributes:
_from_smi: output images directly from smiles. Defaults to False.
_cmap: colormap for `render_with_color` method. Defaults to "rainbow".
_size: size of the output images. Defaults to (300, 300).
_mol_font_size: custom font size on the rendered molecules. Defaults to None.
_label_font_size: custom font size for the label of the molecules, if any.
Defaults to None.
_annotation_font_scale: scale factor for the font size of the annotations.
_font_name: : font name found by img_render.FondManager class.
Defaults to "DejaVu Sans".
_n_jobs: number of jobs to run in parallel. Defaults to 1.
_bg_transparent: render the background as transparent. Defaults to False.
d2d_params: dictionary with the parameters to be passed to get_d2d method.
available_fonts: dictionary with fonts available in the system and in matplotlib
"""
[docs]
def __init__(
self,
from_smi: bool = False,
size: tuple = (300, 300),
cmap: str = "rainbow",
font_name: str = "Telex-Regular",
label_font_size: int = None,
mol_font_size: int = None,
n_jobs: int = 1,
chunk_size: int = None,
bond_line_width: float = 2.0,
no_atom_labels: bool = False,
add_atom_indices: bool = False,
add_bond_indices: bool = False,
annotation_font_scale: float = None,
explicit_methyl: bool = False,
unspecified_stereo_unknown: bool = False,
bg_transparent: bool = False,
bw: bool = False,
**kwargs,
) -> None:
"""Initialize the MolPlotter class. The class is used to render molecules into
either .sdf or .png files. It uses RDKit's DrawMolecule method to generate the
images and `self.d2d_params` to set the parameters for the rendering process.
Args:
from_smi: if true, methods will treat inputs as smiles. Defaults to False.
size: size of the image to de displayed. Defaults to (300, 300).
cmap: colormap for the structure matching. Defaults to "rainbow".
font_name: font used on molecules' legends. Defaults to "DejaVu Sans".
label_font_size: custom font size for the label of the molecules, if any.
Defaults to None.
mol_font_size: custom font size on the rendered molecules. Defaults to None.
n_jobs: number of jobs to run in parallel. Defaults to 1.
chunk_size: size of chunks for ParallelApplier. If None, auto-calculated.
Defaults to None.
bond_line_width (MolDraw2DCairo): width of bond lines. Defaults to 2.0.
no_atom_labels (MolDraw2DCairo): do not add atom labels to the rendered mol.
add_atom_indices (MolDraw2DCairo): add atom indices to the rendered mols.
Defaults to False.
add_bond_indices (MolDraw2DCairo): add bond indices to the rendered mols.
Defaults to False.
annotation_font_scale: scale factor for the font size of the annotations.
Defaults to None
explicit_methyl (MolDraw2DCairo): explicitly displays a methil as CH3.
Defaults to False.
unspecified_stereo_unknown (MolDraw2DCairo): unspecified stereo atoms/bonds
are drawn as if they were unknown. Defaults to False.
bg_transparent: render the background as transparent. Defaults to False.
bw: render the molecule as black and white. Defaults to False.
kwargs: additional keyword arguments defining extra MolDraw2D attributes, set
by passing them to `self.get_d2d` method.
"""
self._cmap = cmap
self._size = size
self._mol_font_size = mol_font_size
self._label_font_size = label_font_size
self._annotation_font_scale = annotation_font_scale
self._font_name = font_name
self._n_jobs = n_jobs
self._chunk_size = chunk_size
self._bg_transparent = bg_transparent
self.d2d_params = {
"bondLineWidth": bond_line_width,
"noAtomLabels": no_atom_labels,
"addAtomIndices": add_atom_indices,
"addBondIndices": add_bond_indices,
"explicitMethyl": explicit_methyl,
"unspecifiedStereoIsUnknown": unspecified_stereo_unknown,
"bw": bw,
**kwargs,
}
self._check_font(font_name)
super().__init__(from_smi)
@property
def available_fonts(self):
fm = FontManager()
return fm.available_fonts
def _check_font(self, font_name):
if self._font_name not in self.available_fonts.keys():
logger.warning(
f"Font {self._font_name} not found. Check `available_fonts` attribute."
)
return font_name
[docs]
@staticmethod
def geometric_mean(arr, axis=0):
"""Calculate the geometric mean of an array. Adapted from SciPy:
https://github.com/scipy/blob/v1.10.1/scipy/stats/_stats.py.py#L199=L269"""
with np.errstate(divide="ignore"):
log_a = np.log(arr)
return np.exp(np.average(log_a, axis=axis))
[docs]
def get_d2d(
self,
bondLineWidth: float = 2.0,
noAtomLabels: bool = False,
addAtomIndices: bool = False,
addBondIndices: bool = False,
explicitMethyl: bool = False,
unspecifiedStereoIsUnknown: bool = False,
bw: bool = False,
svg: bool = False,
**kwargs,
) -> Draw.MolDraw2DCairo:
"""Function to get the rdkit's MolDraw2DCairo object with the desired options.
Args:
bondLineWidth: width of the bonds in the drawing. RDKit defaults to 2.0.
noAtomLabels: do not add atom labels to the rendered mol. Defaults to False.
addAtomIndices: add the atom indices to the rendered mol. Defaults to False.
addBondIndices: add the bond indices to the rendered mol. Defaults to False.
explicitMethyl: explicitly displays a methil as CH3. Defaults to False.
unspecifiedStereoIsUnknown: unspecified stereo atoms/bonds are drawn as if
they were unknown. Defaults to False.
bw: render the molecule as black and white. Defaults to False.
svg: if True, returns a Draw.MolDraw2DSVG object. Defaults to False.
kwargs: additional keyword arguments defining extra MolDraw2D attributes.
Returns:
Draw.MolDraw2DCairo object with the desired options.
"""
# TODO: for every kwarg passed here, take the attribute and set the value.
if svg:
d2d = Draw.MolDraw2DSVG(*self._size)
else:
d2d = Draw.MolDraw2DCairo(*self._size)
dopts = d2d.drawOptions()
if self._mol_font_size is not None:
dopts.fixedFontSize = self._mol_font_size
if self._label_font_size is not None:
dopts.legendFontSize = self._label_font_size
if self._annotation_font_scale is not None:
dopts.annotationFontScale = self._annotation_font_scale
if self._bg_transparent:
dopts.setBackgroundColour((0, 0, 0, 0))
dopts.bondLineWidth = bondLineWidth
if noAtomLabels:
dopts.noAtomLabels = True
if addAtomIndices:
dopts.addAtomIndices = True
if addBondIndices:
dopts.addBondIndices = True
if explicitMethyl:
dopts.explicitMethyl = True
if unspecifiedStereoIsUnknown:
dopts.unspecifiedStereoIsUnknown = True
if bw:
dopts.useBWAtomPalette()
if kwargs:
for key, value in kwargs.items():
logger.trace(f"Setting {key} to {value} in MolDraw2D options.")
setattr(dopts, key, value)
font_path = self.available_fonts.get(self._font_name)
dopts.fontFile = str(font_path)
return d2d
def _stdin_to_mol(self, stdin):
if self._from_smi:
return self._output_mol(stdin)
else:
return stdin
def _substructure_palette(
self, n_substruct: int, cmap: str, alpha: float = 1.0
) -> List[Tuple[float, float, float, float]]:
""" """
qualitative_cmaps = [
"Pastel1",
"Pastel2",
"Paired",
"Accent",
"Dark2",
"Set1",
"Set2",
"Set3",
"tab10",
"tab20",
"tab20b",
"tab20c",
]
if cmap in qualitative_cmaps:
# Unpack colors and add alpha
colors = [tuple([*col] + [alpha]) for col in cm.get_cmap(cmap).colors]
else:
scalar_mappable = cm.ScalarMappable(cmap=cmap)
colors = scalar_mappable.to_rgba(range(n_substruct), alpha=alpha).tolist()
return colors
def _images_to_grid(self, images: List[Image.Image], n_cols: int) -> Image.Image:
"""Helper function to organize a list of images into a single image, as a grid.
Args:
images: Images to be added into the grid.
n_cols: number of columns of the grid.
Returns:
The image collage as a PIL.Image.Image object.
"""
def list_split(list_a, chunk_size):
"""
Returns a generator with a determined chunk size over list_a.
Used to organize the figures in the correct `n_cols` format.
"""
for i in range(0, len(list_a), chunk_size):
yield list_a[i : i + chunk_size]
# Splitting the list of images into a list of lists with n_cols
if n_cols is None:
n_cols = len(images)
images = list(list_split(images, n_cols))
# Appending blank figures so we have the correct vector shapes
while len(images[-1]) < len(images[0]):
images[-1].append(Image.new("RGB", self._size, color=(255, 255, 255)))
list_of_hstacked = list()
# Creating list of horizontally stacked arrays
for sublist in images:
list_of_hstacked.append(np.hstack([np.asarray(img) for img in sublist]))
# Vertically stacking horizontal arrays
for _item in list_of_hstacked:
final_img = np.vstack([hstack for hstack in list_of_hstacked])
# Creating and returning image from array
final_img = Image.fromarray(final_img)
return final_img
[docs]
def render_ACS1996(
self,
mol: Chem.Mol,
mean_bond_length: float = None,
label: Optional[str] = None,
return_svg=False,
**kwargs,
) -> Union[Image.Image, str]:
"""Render molecules using the ACS 1996 style. This is a mode which is designed
to produce images compatible with the drawing standards for American Chemical
Society (ACS) journals.
Args:
mol: rdkit mol object.
mean_bond_length: the mean bond length of the molecule. Defaults to None.
label: the label to be added to the molecule. Defaults to "".
return_svg: whether to return an svg image instead. Defaults to False.
kwargs: additional keyword arguments to be passed to the DrawMolecule method
Returns:
a PIL.Image.Image object or a string with the SVG representation in case
return_svg is True.
"""
if label is None:
label = ""
mol = self._stdin_to_mol(mol)
d2d = self.get_d2d(svg=return_svg, **self.d2d_params)
Chem.rdDepictor.Compute2DCoords(mol)
if mean_bond_length is None:
mean_bond_length = Draw.MeanBondLength(mol)
dopts = d2d.drawOptions()
Draw.SetACS1996Mode(dopts, mean_bond_length)
if self._bg_transparent:
dopts.setBackgroundColour((0, 0, 0, 0))
d2d.DrawMolecule(mol, legend=label, **kwargs)
d2d.FinishDrawing()
if not return_svg:
img = Image.open(BytesIO(d2d.GetDrawingText()))
else:
img = d2d.GetDrawingText()
return img
[docs]
def render_mol(
self,
mol: Chem.Mol,
label: Optional[str] = None,
match_pose: Optional[Union[str, Chem.Mol]] = None,
return_svg: bool = False,
**kwargs,
) -> Union[Image.Image, str]:
"""Plot molecules using rdMolDraw2D.MolDraw2DCairo. Keyword arguments provided
will be passed into DrawMolecule method of the same class.
Args:
mol: rdkit mol object.
label: the label to be added to the molecule. Defaults to None.
match_pose: if desired, input a SMILES, a SMARTS or a rdkit mol object to
render `mol` into a matching position. Defaults to None.
return_svg: if you'd like to return an svg image instead. Defaults to False.
kwargs: additional keyword arguments to be passed to the DrawMolecule method
Raises:
ValueError: _description_
Returns:
a PIL.Image.Image object or a string with the SVG representation in case
return_svg is True.
"""
if label is None:
label = ""
mol = self._stdin_to_mol(mol)
if match_pose is not None:
if isinstance(match_pose, Chem.rdchem.Mol):
AllChem.Compute2DCoords(match_pose)
else:
if "#" in match_pose:
logger.debug('Trying "match_pose" from SMARTS...')
ref_mol = Chem.MolFromSmarts(match_pose)
if ref_mol is None:
raise ValueError(
'Error: "match_pose" from SMARTS RDKit Mol is invalid.'
)
else:
logger.debug('Trying "match_pose" from SMILES...')
ref_mol = Chem.MolFromSmiles(match_pose)
if ref_mol is None:
logger.warning(
'Error: "match_pose" invalid from SMARTS & SMILES.'
)
match_pose = ref_mol
AllChem.Compute2DCoords(match_pose)
AllChem.GenerateDepictionMatching2DStructure(
mol, match_pose, acceptFailure=True
)
d2d = self.get_d2d(**self.d2d_params, svg=return_svg)
d2d.DrawMolecule(mol, legend=label, **kwargs)
d2d.FinishDrawing()
if return_svg:
img = d2d.GetDrawingText()
else:
img = Image.open(BytesIO(d2d.GetDrawingText()))
return img
[docs]
def render_with_matches(
self,
mol: Chem.Mol,
substructs,
label: Optional[str] = None,
match_pose: Optional[Union[str, Chem.Mol]] = None,
**kwargs,
) -> Image.Image:
"""Render the substructures on the molecules using default RDKit coloring.
Args:
mol: molecule to be rendered.
substructs: substructures output from RDKitFilters.filter_mols.
label: if desired, add a label to the molecule. Defaults to None.
match_pose: a common structure between `smiles` used to generate the
matching 2D images. If None, 2D rendering won't match. Defaults to None.
kwargs: additional keyword arguments to be passed to the DrawMolecule method
Returns:
PIL.Image.Image with the molecule and the highlighted substructures.
"""
mol = self._output_mol(mol)
hit_bonds = []
hit_atoms = []
if "__iter__" not in dir(substructs):
substructs = [substructs]
for patt in substructs:
smarts = Chem.MolToSmarts(patt)
pttrn = Chem.MolFromSmarts(smarts)
hit_ats = list(mol.GetSubstructMatch(pttrn))
if hit_ats == []:
continue
hit_atoms = hit_atoms + hit_ats
for bond in pttrn.GetBonds():
aid1 = hit_ats[bond.GetBeginAtomIdx()]
aid2 = hit_ats[bond.GetEndAtomIdx()]
hit_bonds.append(mol.GetBondBetweenAtoms(aid1, aid2).GetIdx())
img = self.render_mol(
mol,
highlightAtoms=hit_atoms,
highlightBonds=hit_bonds,
label=label,
match_pose=match_pose,
**kwargs,
)
return img
[docs]
def render_with_colored_matches(
self,
mol: Chem.Mol,
descriptions: List[str],
substructs: List[Chem.Mol],
label: Optional[str] = None,
cmap: str = "rainbow",
alpha: float = 0.5,
match_pose: Optional[Union[str, Chem.Mol]] = None,
**kwargs,
):
"""Take descriptions and substructures output from RDKitFilters.filter_mols
and renders them on the molecular structure, coloring the substructures and
adding labels according to the descriptions.
Note: in case of overlap between colors, the final color corresponds to the
their respective geometric mean.
Args:
mol: rdkit molecule object.
descriptions: descriptions (`output[0]`) from RDKitFilters.filter_mols.
substructs: substructures (`output[1]`) from RDKitFilters.filter_mols.
label: if desired, add a label to the molecule. Defaults to None.
cmap: colormap for the substructures. Defaults to "viridis".
alpha: transparency of the colors. Defaults to 0.5.
match_pose: a common structure between `smiles` used to generate the
matching 2D images. If None, 2D rendering won't match. Defaults to None.
kwargs: additional keyword arguments to be passed to the DrawMolecule method
Returns:
matplotlib.figure.Figure and Axis with the molecule and the
highlighted substructures.
"""
mol = self._output_mol(mol)
smarts = [Chem.MolToSmarts(sub) for sub in substructs]
unique_smarts, indices = np.unique(smarts, return_index=True)
unique_descrip = [descriptions[i] for i in indices]
color_dict = {}
colors = self._substructure_palette(len(unique_smarts), cmap=cmap, alpha=alpha)
for smarts, _descr, color in zip(unique_smarts, unique_descrip, colors):
# Create the patches that are used for the labels
pttrn = Chem.MolFromSmarts(smarts)
matches = mol.GetSubstructMatch(pttrn)
for match in matches:
if match in color_dict.keys():
color_dict.update({match: np.vstack([color_dict[match], color])})
else:
color_dict.update({match: np.array(color)})
color_dict = {
k: tuple(self.geometric_mean(v)) if v.ndim > 1 else tuple(v)
for k, v in color_dict.items()
} # should be tuples
img = self.render_mol(
mol,
label=label,
highlightAtoms=color_dict.keys(),
highlightAtomColors=color_dict,
match_pose=match_pose,
**kwargs,
)
return img
[docs]
def colored_matches_legend(
self, descriptions, substructures, cmap="rainbow", alpha=0.5, ax=None
):
if ax is not None:
ax = plt.gca()
else:
fig, ax = plt.subplots(figsize=(5, 5))
patches = []
colors = self._substructure_palette(len(substructures), cmap=cmap, alpha=alpha)
for _smarts, descr, color in zip(substructures, descriptions, colors):
patches.append(
mpatches.Patch(facecolor=color, label=descr, edgecolor="black")
)
ax.legend(
handles=patches,
bbox_to_anchor=(1.05, 0.25),
loc="lower left",
borderaxespad=0,
frameon=False,
)
ax.set_axis_off()
if ax is None:
return fig, ax
[docs]
class MolGridPlotter(MolPlotter):
"""MolGridPlotter class to render molecules into grids of images. It uses RDKit's
DrawMolecule method to generate the images and PIL to patch images together.
The images can be rendered with labels and/or substructure matches, similar to its
parent class.
Attributes:
_from_smi: output images directly from smiles. Defaults to False.
_cmap: colormap for `render_with_color` method. Defaults to "rainbow".
_size: size of the output images. Defaults to (300, 300).
_mol_font_size: custom font size on the rendered molecules. Defaults to None.
_label_font_size: custom font size for the label of the molecules, if any.
Defaults to None.
_annotation_font_scale: scale factor for the font size of the annotations.
_font_name: : font name found by img_render.FondManager class.
Defaults to "DejaVu Sans".
_n_jobs: number of jobs to run in parallel. Defaults to 1.
_bg_transparent: render the background as transparent. Defaults to False.
d2d_params: dictionary with the parameters to be passed to get_d2d method.
available_fonts: dictionary with fonts available in the system and in matplotlib
"""
[docs]
def __init__(
self,
from_smi: bool = False,
size: tuple = (300, 300),
cmap: str = "rainbow",
font_name: str = "Telex-Regular",
label_font_size: int = None,
mol_font_size: int = None,
n_jobs: int = 1,
chunk_size: int = None,
bond_line_width: float = 2.0,
no_atom_labels: bool = False,
add_atom_indices: bool = False,
add_bond_indices: bool = False,
annotation_font_scale: float = None,
explicit_methyl: bool = False,
unspecified_stereo_unknown: bool = False,
bg_transparent: bool = False,
bw: bool = False,
**kwargs,
) -> None:
"""initialize the MolGridPlotter class. The class is used to render molecules
into either .sdf or .png files. It uses RDKit's DrawMolecule method to generate
the images and `self.d2d_params` to set the parameters for the rendering process
Args:
from_smi: if true, methods will treat inputs as smiles. Defaults to False.
size: size of the image to de displayed. Defaults to (300, 300).
cmap: colormap for the structure matching. Defaults to "rainbow".
font_name: font used on molecules' legends. Defaults to "DejaVu Sans".
label_font_size: custom font size for the label of the molecules, if any.
Defaults to None.
mol_font_size: custom font size on the rendered molecules. Defaults to None.
n_jobs: number of jobs to run in parallel. Defaults to 1.
chunk_size: size of chunks for ParallelApplier. If None, auto-calculated.
Defaults to None.
bond_line_width (MolDraw2DCairo): width of bond lines. Defaults to 2.0.
no_atom_labels (MolDraw2DCairo): do not add atom labels to the rendered mol.
add_atom_indices (MolDraw2DCairo): add atom indices to the rendered mols.
Defaults to False.
add_bond_indices (MolDraw2DCairo): add bond indices to the rendered mols.
Defaults to False.
annotation_font_scale: scale factor for the font size of the annotations.
Defaults to None
explicit_methyl (MolDraw2DCairo): explicitly displays a methil as CH3.
Defaults to False.
unspecified_stereo_unknown (MolDraw2DCairo): unspecified stereo atoms/bonds
are drawn as if they were unknown. Defaults to False.
bg_transparent: render the background as transparent. Defaults to False.
bw: render the molecule as black and white. Defaults to False.
kwargs: additional keyword arguments defining extra MolDraw2D attributes, set
by passing them to `self.get_d2d` method.
"""
super().__init__(
from_smi,
size,
cmap,
font_name,
label_font_size,
mol_font_size,
n_jobs,
chunk_size,
bond_line_width,
no_atom_labels,
add_atom_indices,
add_bond_indices,
annotation_font_scale,
explicit_methyl,
unspecified_stereo_unknown,
bg_transparent,
bw,
**kwargs,
)
[docs]
def mol_grid_png(
self,
mols: list,
labels: Optional[list] = None,
n_cols: int = None,
match_pose: Optional[Union[str, Chem.Mol]] = None,
**kwargs,
) -> Image.Image:
"""
Args:
mols: list of molecules to display.
labels: list of labels to be written on the mol images. Defaults to None
n_cols: N columns with molecules. If None, n_cols = len(smiles).
Defaults to None.
match_pose: a common structure between `smiles` used to generate the
matching 2D images. If None, 2D rendering won't match. Defaults to None.
kwargs: additional keyword arguments to be passed to the DrawMolecule method
Returns:
PIL.Image.Image
"""
if labels is not None:
assert len(labels) == len(mols), "Labels and mols must have the same length"
variables = list(zip(mols, labels))
else:
variables = list(zip(mols))
def _unpack_render_mol(args):
"""Wrapper to unpack tuple arguments for ParallelApplier."""
return self.render_mol(*args, match_pose=match_pose, **kwargs)
try:
applier = ParallelApplier(
_unpack_render_mol,
variables,
n_jobs=self._n_jobs,
show_progress=False,
backend="loky",
custom_desc="Rendering molecule grid",
chunk_size=self._chunk_size,
)
images = applier()
except RuntimeError as e:
logger.error(
"Runtime errors can occur when the molecule is too big for "
"the canvas size set by the user. Consider changing the `self.size` "
f"parameter. Error message:\n{e}"
)
raise e
image = self._images_to_grid(images, n_cols)
return image
[docs]
def mol_structmatch_grid_png(
self,
mols: list,
substructs: list,
labels: Optional[list] = None,
n_cols: int = None,
match_pose: Optional[Union[str, Chem.Mol]] = None,
**kwargs,
) -> Image.Image:
"""
Args:
mols: list of molecules to display.
substructs: list of substructures to be highlighted on the mol images.
labels: list of labels to be written on the mol images. Defaults to None
n_cols: N columns with molecules. If None, n_cols = len(smiles).
Defaults to None.
match_pose: a common structure between `smiles` used to generate the
matching 2D images. If None, 2D rendering won't match. Defaults to None.
kwargs: additional keyword arguments to be passed to the DrawMolecule method
Returns:
PIL.Image.Image
"""
if labels is not None:
assert len(labels) == len(mols), "Labels and mols must have the same length"
variables = list(zip(mols, substructs, labels))
else:
variables = list(zip(mols, substructs))
def _unpack_render_with_matches(args):
"""Wrapper to unpack tuple arguments for ParallelApplier."""
return self.render_with_matches(*args, match_pose=match_pose, **kwargs)
try:
applier = ParallelApplier(
_unpack_render_with_matches,
variables,
n_jobs=self._n_jobs,
show_progress=False,
backend="loky",
custom_desc="Rendering molecules with highlighted substructures",
chunk_size=self._chunk_size,
)
images = applier()
except RuntimeError as e:
logger.error(
"Runtime errors can occur when the molecule is too big for "
"the canvas size set by the user. Consider changing the `self.size` "
f"parameter. Error message:\n{e}"
)
raise e
image = self._images_to_grid(images, n_cols)
return image
[docs]
def mol_structmatch_color_grid_png(
self,
mols: list,
descriptions: List[str],
substructs: List[Chem.Mol],
labels: Optional[list] = None,
n_cols: int = None,
match_pose: Optional[Union[str, Chem.Mol]] = None,
**kwargs,
) -> Image.Image:
"""
Args:
mols: list of molecules to display.
descriptions: descriptions (`output[0]`) from RDKitFilters.filter_mols
substructs: substructures (`output[1]`) from RDKitFilters.filter_mols.
labels: list of labels to be written on the mol images. Defaults to None
n_cols: N columns with molecules. If None, n_cols = len(smiles).
Defaults to None.
match_pose: a common structure between `smiles` used to generate the
matching 2D images. If None, 2D rendering won't match. Defaults to None.
kwargs: additional keyword arguments to be passed to the DrawMolecule method
Returns:
PIL.Image.Image
"""
mols = self._stdin_to_mol(mols)
if labels is not None:
assert len(labels) == len(mols), "Labels and mols must have the same length"
variables = list(zip(mols, descriptions, substructs, labels))
else:
variables = list(zip(mols, descriptions, substructs))
def _unpack_render_with_colored_matches(args):
"""Wrapper to unpack tuple arguments for ParallelApplier."""
return self.render_with_colored_matches(
*args, match_pose=match_pose, **kwargs
)
try:
applier = ParallelApplier(
_unpack_render_with_colored_matches,
variables,
n_jobs=self._n_jobs,
show_progress=False,
backend="loky",
custom_desc="Rendering molecules with colored matches",
chunk_size=self._chunk_size,
)
images = applier()
except RuntimeError as e:
logger.error(
"Runtime errors can occur when the molecule is too big for "
"the canvas size set by the user. Consider changing the `self.size` "
f"parameter. Error message:\n{e}"
)
raise e
image = self._images_to_grid(images, n_cols)
return image