Skip to content

datamol.viz

Vizualize molecule in 2D or 3D

Generate an image out of a molecule or a list of molecules.

Parameters:

Name Type Description Default
mols Union[List[Union[Mol, str]], Mol, str]

One or a list of molecules.

required
legends Union[List[Union[str, None]], str, None]

A string or a list of string as legend for every molecules.

None
n_cols int

Number of molecules per column.

4
use_svg bool

Whether to ouput an SVG (or a PNG).

True
mol_size Union[Tuple[int, int], int]

A int or a tuple of int defining the size per molecule.

(300, 300)
highlight_atom Optional[List[List[int]]]

the atoms to highlight.

None
highlight_bond Optional[List[List[int]]]

The bonds to highlight.

None
outfile Optional[str]

Path where to save the image (local or remote path).

None
max_mols int

The maximum number of molecules to display.

32
max_mols_ipython int

The maximum number of molecules to display when running within an IPython environment.

50
copy bool

Whether to copy the molecules or not.

True
indices bool

Whether to draw the atom indices.

False
bond_indices bool

Whether to draw the bond indices.

False
bond_line_width int

The width of the bond lines.

2
legend_fontsize int

Font size for the legend.

16
kekulize bool

Run kekulization routine on molecules. Skipped if fails.

True
align Union[Mol, str, bool]

Whether to align the 2D coordinates of the molecules. - If set to True, align all molecules with dm.align.auto_align_many(). - If set to a molecule, it is used as a template for alignment with dm.align.template_align(). - If set to False, no alignment is performed. For a more custom alignment, we suggest using directly the module dm.align instead.

False
**kwargs Any

Additional arguments to pass to the drawing function. See RDKit documentation related to MolDrawOptions for more details at https://www.rdkit.org/docs/source/rdkit.Chem.Draw.rdMolDraw2D.html.

{}
Source code in datamol/viz/_viz.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
def to_image(
    mols: Union[List[Union[dm.Mol, str]], dm.Mol, str],
    legends: Union[List[Union[str, None]], str, None] = None,
    n_cols: int = 4,
    use_svg: bool = True,
    mol_size: Union[Tuple[int, int], int] = (300, 300),
    highlight_atom: Optional[List[List[int]]] = None,
    highlight_bond: Optional[List[List[int]]] = None,
    outfile: Optional[str] = None,
    max_mols: int = 32,
    max_mols_ipython: int = 50,
    copy: bool = True,
    indices: bool = False,
    bond_indices: bool = False,
    bond_line_width: int = 2,
    stereo_annotations: bool = True,
    legend_fontsize: int = 16,
    kekulize: bool = True,
    align: Union[dm.Mol, str, bool] = False,
    **kwargs: Any,
):
    """Generate an image out of a molecule or a list of molecules.

    Args:
        mols: One or a list of molecules.
        legends: A string or a list of string as legend for every molecules.
        n_cols: Number of molecules per column.
        use_svg: Whether to ouput an SVG (or a PNG).
        mol_size: A int or a tuple of int defining the size per molecule.
        highlight_atom: the atoms to highlight.
        highlight_bond: The bonds to highlight.
        outfile: Path where to save the image (local or remote path).
        max_mols: The maximum number of molecules to display.
        max_mols_ipython: The maximum number of molecules to display when running within an IPython environment.
        copy: Whether to copy the molecules or not.
        indices: Whether to draw the atom indices.
        bond_indices: Whether to draw the bond indices.
        bond_line_width: The width of the bond lines.
        legend_fontsize: Font size for the legend.
        kekulize: Run kekulization routine on molecules. Skipped if fails.
        align: Whether to align the 2D coordinates of the molecules.
            - If set to True, align all molecules with `dm.align.auto_align_many()`.
            - If set to a molecule, it is used as a template for alignment with `dm.align.template_align()`.
            - If set to False, no alignment is performed.
            For a more custom alignment, we suggest using directly the module `dm.align` instead.
        **kwargs: Additional arguments to pass to the drawing function. See RDKit
            documentation related to `MolDrawOptions` for more details at
            https://www.rdkit.org/docs/source/rdkit.Chem.Draw.rdMolDraw2D.html.
    """

    if isinstance(mol_size, int):
        mol_size = (mol_size, mol_size)

    if isinstance(mols, (dm.Mol, str)):
        mols = [mols]

    # Convert smiles to molecules if strings are provided as input for API consistency
    mols = mols[:]  # avoid in place modification
    for i in range(len(mols)):
        if isinstance(mols[i], str):
            mols[i] = dm.to_mol(mols[i])

    if isinstance(legends, str):
        legends = [legends]

    if copy:
        mols = [dm.copy_mol(mol) for mol in mols]

    if max_mols is not None:
        mols = mols[:max_mols]

        if legends is not None:
            legends = legends[:max_mols]

    # Whether to align the molecules
    if isinstance(align, (dm.Mol, str)):
        mols = [dm.align.template_align(mol, template=align) for mol in mols]
    elif align is True:
        mols = dm.align.auto_align_many(mols)

    # Prepare molecules before drawing
    mols = [prepare_mol_for_drawing(mol, kekulize=kekulize) for mol in mols]

    _highlight_atom = highlight_atom
    if highlight_atom is not None and isinstance(highlight_atom[0], int):
        _highlight_atom = [highlight_atom]

    _highlight_bond = highlight_bond
    if highlight_bond is not None and isinstance(highlight_bond[0], int):
        _highlight_bond = [highlight_bond]

    # Don't make the image bigger than it
    if len(mols) < n_cols:
        n_cols = len(mols)

    draw_options = Draw.rdMolDraw2D.MolDrawOptions()
    draw_options.legendFontSize = legend_fontsize
    draw_options.addAtomIndices = indices
    draw_options.addBondIndices = bond_indices
    draw_options.addStereoAnnotation = stereo_annotations
    draw_options.bondLineWidth = bond_line_width

    # Add the custom drawing options.
    _kwargs = {}
    for k, v in kwargs.items():
        if hasattr(draw_options, k):
            setattr(draw_options, k, v)
        else:
            _kwargs[k] = v

    # Check if we are in a Jupyter notebook or IPython display context
    # If so, conditionally add the maxMols argument
    in_notebook = dm.viz.utils.is_ipython_session()

    if in_notebook:
        _kwargs["maxMols"] = max_mols_ipython
        if max_mols > max_mols_ipython:
            logger.warning(
                f"You have set max_mols to {max_mols}, which is higher than max_mols_ipython ({max_mols_ipython}). "
                "Consider increasing max_mols_ipython if you want to display all molecules in an IPython environment."
            )

    image = Draw.MolsToGridImage(
        mols,
        legends=legends,
        molsPerRow=n_cols,
        useSVG=use_svg,
        subImgSize=mol_size,
        highlightAtomLists=_highlight_atom,
        highlightBondLists=_highlight_bond,
        drawOptions=draw_options,
        **_kwargs,
    )

    if outfile is not None:
        image_to_file(image, outfile, as_svg=use_svg)
    return image

Visualize the conformer(s) of a molecule.

Parameters:

Name Type Description Default
mol Mol

a molecule.

required
conf_id int

The ID of the conformer to show. -1 shows the first conformer. Only works if n_confs is None.

-1
n_confs Optional[Union[int, List[int]]]

Can be a number of conformers to shows or a list of conformer indices. When None, only the first conformer is displayed. When -1, show all conformers.

None
align_conf bool

Whether to align conformers together.

True
n_cols int

Number of columns. Defaults to 3.

3
sync_views bool

Wether to sync the multiple views.

True
remove_hs bool

Wether to remove the hydrogens of the conformers.

True
width str

The width of the returned view. Defaults to "auto".

'auto'
Source code in datamol/viz/_conformers.py
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
def conformers(
    mol: Chem.rdchem.Mol,
    conf_id: int = -1,
    n_confs: Optional[Union[int, List[int]]] = None,
    align_conf: bool = True,
    n_cols: int = 3,
    sync_views: bool = True,
    remove_hs: bool = True,
    width: str = "auto",
):
    """Visualize the conformer(s) of a molecule.

    Args:
        mol: a molecule.
        conf_id: The ID of the conformer to show. -1 shows
            the first conformer. Only works if `n_confs` is None.
        n_confs: Can be a number of conformers
            to shows or a list of conformer indices. When None, only the first
            conformer is displayed. When -1, show all conformers.
        align_conf: Whether to align conformers together.
        n_cols: Number of columns. Defaults to 3.
        sync_views: Wether to sync the multiple views.
        remove_hs: Wether to remove the hydrogens of the conformers.
        width: The width of the returned view. Defaults to "auto".
    """

    widgets = _get_ipywidgets()
    nv = _get_nglview()

    if mol.GetNumConformers() == 0:
        raise ValueError(
            "The molecule has 0 conformers. You can generate conformers with `dm.conformers.generate(mol)`."
        )

    # Clone the molecule
    mol = copy.deepcopy(mol)

    if remove_hs:
        mol = Chem.RemoveHs(mol)  # type: ignore
    else:
        mol = Chem.AddHs(mol)  # type: ignore

    if n_confs is None:
        return nv.show_rdkit(mol, conf_id=conf_id)

    # If n_confs is int, convert to list of conformer IDs
    if n_confs == -1:
        n_confs = [conf.GetId() for conf in mol.GetConformers()]
    elif isinstance(n_confs, int):
        if n_confs > mol.GetNumConformers():
            n_confs = mol.GetNumConformers()
        n_confs = list(range(n_confs))  # type: ignore

    if align_conf:
        rdMolAlign.AlignMolConformers(mol, confIds=n_confs)

    # Get number of rows
    n_rows = len(n_confs) // n_cols
    n_rows += 1 if (len(n_confs) % n_cols) > 0 else 0

    # Create a grid
    grid = widgets.GridspecLayout(n_rows, n_cols)  # type: ignore

    # Create and add views to the grid.
    widget_coords = itertools.product(range(n_rows), range(n_cols))
    views = []
    for i, (conf_id, (x, y)) in enumerate(zip(n_confs, widget_coords)):
        view = nv.show_rdkit(mol, conf_id=conf_id)
        view.layout.width = width
        view.layout.align_self = "stretch"
        grid[x, y] = view
        views.append(view)

    # Sync views
    if sync_views:
        for view in views:
            view._set_sync_camera(views)

    return grid

Specific plotting functions

Source code in datamol/viz/_circle_grid.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
class MolsCircleGrid:
    def __init__(
        self,
        center_mol: Union[Mol, str],
        ring_mols: List[List[Union[Mol, str]]],
        legend: Optional[str] = None,
        margin: int = 50,
        ring_scaler: float = 1.0,
        act_mapper: Optional[dict] = None,
        align: Optional[Union[Mol, str, bool]] = None,
        use_svg: bool = True,
        line_width: Optional[float] = None,
        ring_color: Optional[DatamolColor] = None,
        ring_mol_start_angles_degrees: Optional[List[float]] = None,
        center_mol_highlight_atoms: Optional[List[int]] = None,
        center_mol_highlight_bonds: Optional[List[int]] = None,
        ring_mol_highlight_atoms: Optional[List[List[int]]] = None,
        ring_mol_highlight_bonds: Optional[List[List[int]]] = None,
        kekulize: bool = True,
        layout_random_seed: Optional[int] = 19,
        **kwargs: Any,
    ):
        """Show molecules in concentric rings, with one molecule at the center

        Args:
            center_mol: Molecule at center of the rings
            ring_mols: List of molecule for each level of concentric rings around the center mol
            legend: optional global legend for the figure
            margin: Margin between the circle layers
            ring_scaler: Scale the size of the molecules in each circle by this factor compared to the center molecule
            act_mapper: dictionary of activity for each molecule
            align: Whether to align the 2D coordinates of the molecules.
                - If set to True, align all molecules with `dm.align.auto_align_many()`.
                - If set to a molecule, it is used as a template for alignment with `dm.align.template_align()`.
                - If set to False, no alignment is performed.
                - If set to None (default), the ring (peripheral) molecules are aligned to the center molecules. This is the default behaviour.
            use_svg: Whether to use SVG or use PNG
            line_width: Width of the lines to draw
            center_mol_highlight_atoms: List of atom indices to highlight for the center molecule
            center_mol_highlight_bonds: List of bond indices to highlight for the center molecule
            ring_mol_highlight_atoms: List of list of atom indices to highlight for molecules at each level of the concentric rings
            ring_mol_highlight_bonds: List of list of bond indices to highlight for molecules at each level of the concentric rings
            ring_color: Color of the concentric rings. Set to None to not draw any ring.
            ring_mol_start_angles_degrees: List of angles in degrees to start drawing the molecules at each level of the concentric
                rings. If None then a random position will be used.
            kekulize: Whether to kekulize the molecules before drawing.
            layout_random_seed: Random seed for the layout of the molecules. Set to None for no seed.
            **kwargs: Additional arguments to pass to the drawing function. See RDKit
                documentation related to `MolDrawOptions` for more details at
                https://www.rdkit.org/docs/source/rdkit.Chem.Draw.rdMolDraw2D.html.
        """
        assert dm.is_greater_than_current_rdkit_version(
            "2023.03"
        ), "MolsCircleGrid requires RDKit version >= 2022.09"
        self.center_mol = center_mol
        self.ring_mols = ring_mols
        if not isinstance(self.ring_mols[0], (tuple, list, np.ndarray)):
            self.ring_mols = [self.ring_mols]
        self.ring_count = len(self.ring_mols)
        self.legend = legend or None
        self.margin = margin
        self.ring_scaler = ring_scaler
        self.align = align
        self.use_svg = use_svg
        self.line_width = line_width
        self.ring_color = ring_color
        self.ring_mol_start_angles_degrees = ring_mol_start_angles_degrees
        self.ring_color_rdkit: Optional[RDKitColor] = to_rdkit_color(ring_color)
        self.ring_mol_highlight_atoms = ring_mol_highlight_atoms
        self.ring_mol_highlight_bonds = ring_mol_highlight_bonds
        self.center_mol_highlight_atoms = center_mol_highlight_atoms
        self.center_mol_highlight_bonds = center_mol_highlight_bonds
        self.kekulize = kekulize
        self.layout_random_seed = layout_random_seed
        self._global_legend_size = 0
        if self.legend is not None:
            self._global_legend_size = max(25, self.margin)
        self._format_activity_mapper(act_mapper)
        self._initialize_drawing(**kwargs)

    def _format_activity_mapper(self, act_mapper: Optional[dict] = None) -> None:
        """Format the activity mapper to a dictionary of legends

        Args:
            act_mapper: dictionary of activity for each molecule
        """
        self._legends = {}
        self.act_mapper = {}
        if act_mapper is None:
            return
        m_0 = list(act_mapper.keys())[0]
        if isinstance(m_0, Mol) or dm.to_mol(m_0) is not None:
            act_mapper = {dm.unique_id(k): v for k, v in act_mapper.items()}
        self.act_mapper = act_mapper
        for k, v in self.act_mapper.items():
            txt = []
            for prop, propval in v.items():
                if not isinstance(propval, str):
                    propval = "{:.2f}".format(propval)
                txt.append(f"{prop}: {propval}")
            txt = "\n".join(txt)
            self._legends[k] = txt

    def _initialize_drawing(self, **drawing_options) -> None:
        """Initialize the canvas for drawing and perform all preparation for input molecules

        Args:
            drawing_options: drawing options for the canvas
        """

        # prepare and align all the molecules first
        all_mols = [self.center_mol] + [mol for mols in self.ring_mols for mol in mols]
        all_mols = [dm.to_mol(mol) if isinstance(mol, str) else mol for mol in all_mols]
        if self.align is None:
            # align to center mol
            all_mols = [
                dm.align.template_align(mol, template=self.center_mol, use_depiction=False)
                for mol in all_mols
            ]
        elif isinstance(self.align, (dm.Mol, str)):
            # align to template
            all_mols = [
                dm.align.template_align(mol, template=self.align, use_depiction=False)
                for mol in all_mols
            ]
        elif self.align is True:
            all_mols = dm.align.auto_align_many(all_mols)

        # prepare all the molecules for drawing
        all_mols = [prepare_mol_for_drawing(mol, kekulize=self.kekulize) for mol in all_mols]

        # put back center and peripheral mols after preparing them for drawing
        self.center_mol = all_mols[0]
        cur_circle_start = 1
        for i, mols in enumerate(self.ring_mols):
            self.ring_mols[i] = all_mols[cur_circle_start : cur_circle_start + len(mols)]
            cur_circle_start += len(mols)

        # get the size of each molecule, including the scaling of the ring molecules
        self._size_info = {}
        sizer = Draw.rdMolDraw2D.MolDraw2DSVG(-1, -1)
        sizer_options = sizer.drawOptions()
        current_scaling_factor = sizer_options.scalingFactor
        for ind, mol in enumerate(all_mols):
            if ind > 0:
                sizer_options.scalingFactor = current_scaling_factor * self.ring_scaler
            mol_id = dm.unique_id(mol)
            self._size_info[mol_id] = sizer.GetMolSize(mol, legend=self._legends.get(mol_id, ""))

        # compute the minimum size of the canvas based on the size of the molecules
        max_width = max([x[0] for x in self._size_info.values()])
        max_height = max([x[1] for x in self._size_info.values()])
        self.mol_size = (max_width, max_height)
        self.size = (
            max_width + (self.margin + max_width) * 2 * self.ring_count,
            max_height + (self.margin + max_height) * 2 * self.ring_count,
        )
        self.size = (
            self.size[0],
            self.size[1] + self._global_legend_size * 2,
        )
        self.midpoint = Point2D(self.size[0] // 2, self.size[1] // 2)

        if self.use_svg:
            self.canvas = Draw.rdMolDraw2D.MolDraw2DSVG(*self.size)
        else:
            self.canvas = Draw.rdMolDraw2D.MolDraw2DCairo(*self.size)
        # use fixed parameters for bond length and font size.
        self.canvas.SetFlexiMode(True)
        self.canvas.ClearDrawing()

        # Setting the drawing options
        self.draw_options = self.canvas.drawOptions()
        for k, v in drawing_options.items():
            if hasattr(self.draw_options, k):
                setattr(self.draw_options, k, v)
            else:
                raise ValueError(f"Unknown drawing option: {k}")
        if self.line_width is not None:
            self.canvas.SetLineWidth(self.line_width)
        self.canvas.SetFillPolys(False)

    def __call__(self, outfile: Optional[str] = None):
        """Draw the circular molecule and save to file if outfile is provided

        Args:
            outfile: output file name
        """
        image = self.draw()
        if outfile is not None:
            image_to_file(image, outfile, as_svg=self.use_svg)
        return image

    def draw(self):
        """Create and draw the circular molecule image"""
        radius_list = self._draw_circles()

        # draw the center mol
        self._draw_mol_at(
            self.center_mol,
            self.midpoint,
            highlight_atom=self.center_mol_highlight_atoms,
            highlight_bond=self.center_mol_highlight_bonds,
        )

        rng = random.Random(self.layout_random_seed)

        # draw the ring mols
        self.draw_options.scalingFactor *= self.ring_scaler
        for i, mols in enumerate(self.ring_mols):
            radius = radius_list[i]
            ni = len(mols)

            if self.ring_mol_start_angles_degrees is not None:
                rand_unit = np.deg2rad(self.ring_mol_start_angles_degrees[i])
            else:
                rand_unit = rng.random() * 2 * math.pi

            for k, mol in enumerate(mols):
                center_x = radius * math.cos(2 * k * math.pi / ni + rand_unit) + self.midpoint.x
                center_y = radius * math.sin(2 * k * math.pi / ni + rand_unit) + self.midpoint.y
                center = Point2D(center_x, center_y)
                ring_atom_highlight = None
                if self.ring_mol_highlight_atoms is not None:
                    ring_atom_highlight = self.ring_mol_highlight_atoms[i][k]
                ring_bond_highlight = None
                if self.ring_mol_highlight_bonds is not None:
                    ring_bond_highlight = self.ring_mol_highlight_bonds[i][k]
                self._draw_mol_at(
                    mol,
                    center,
                    highlight_atom=ring_atom_highlight,
                    highlight_bond=ring_bond_highlight,
                )

        self.draw_options.scalingFactor /= self.ring_scaler
        # draw global legend if there is any
        if self.legend is not None:
            self.canvas.SetFontSize(self.canvas.FontSize() * 3)
            text_position = Point2D(
                self.size[0] // 2, self.size[-1] - self._global_legend_size // 2
            )
            self.canvas.DrawString(self.legend, text_position, align=0, rawCoords=True)

        self.canvas.FinishDrawing()
        return drawer_to_image(self.canvas)

    def _draw_circles(self):
        """Drawing the circular rings around the center molecule"""
        if self.ring_count <= 0:
            return []
        radius_step = (min(self.size) - min(self.mol_size) - 2 * self._global_legend_size) // (
            self.ring_count * 2
        )
        radius_list = []
        full_range = range(0, min(self.size) // 2, radius_step)
        for _, radius in enumerate(full_range):
            radius += self.margin // 2
            if radius > self.margin:
                if self.ring_color_rdkit is not None:
                    self.canvas.SetColour(self.ring_color_rdkit)
                    self.canvas.DrawArc(self.midpoint, radius, 0, 360, rawCoords=True)
            radius_list.append(radius + radius_step)
        return radius_list

    def _draw_mol_at(
        self,
        mol: dm.Mol,
        mol_center: Point2D,
        highlight_atom: Optional[List[int]] = None,
        highlight_bond: Optional[List[int]] = None,
    ):
        """Draw molecule at a given position

        Args:
            mol: input molecule
            mol_center: coordinate of the center of the molecule
            highlight_atom: list of atom indices to be highlighted
            highlight_bond: list of bond indices to be highlighted
        """
        # we offset the drawer such that the image will be drawn at the center
        width, height = self._size_info.get(dm.unique_id(mol))
        self.canvas.SetOffset(int(mol_center.x - width / 2), int(mol_center.y - height / 2))
        self.canvas.SetColour((0, 0, 0, 1))
        self.canvas.DrawMolecule(
            mol,
            legend=self._legends.get(dm.unique_id(mol), ""),
            highlightAtoms=highlight_atom,
            highlightBonds=highlight_bond,
        )

__call__(outfile=None)

Draw the circular molecule and save to file if outfile is provided

Parameters:

Name Type Description Default
outfile Optional[str]

output file name

None
Source code in datamol/viz/_circle_grid.py
278
279
280
281
282
283
284
285
286
287
def __call__(self, outfile: Optional[str] = None):
    """Draw the circular molecule and save to file if outfile is provided

    Args:
        outfile: output file name
    """
    image = self.draw()
    if outfile is not None:
        image_to_file(image, outfile, as_svg=self.use_svg)
    return image

__init__(center_mol, ring_mols, legend=None, margin=50, ring_scaler=1.0, act_mapper=None, align=None, use_svg=True, line_width=None, ring_color=None, ring_mol_start_angles_degrees=None, center_mol_highlight_atoms=None, center_mol_highlight_bonds=None, ring_mol_highlight_atoms=None, ring_mol_highlight_bonds=None, kekulize=True, layout_random_seed=19, **kwargs)

Show molecules in concentric rings, with one molecule at the center

Parameters:

Name Type Description Default
center_mol Union[Mol, str]

Molecule at center of the rings

required
ring_mols List[List[Union[Mol, str]]]

List of molecule for each level of concentric rings around the center mol

required
legend Optional[str]

optional global legend for the figure

None
margin int

Margin between the circle layers

50
ring_scaler float

Scale the size of the molecules in each circle by this factor compared to the center molecule

1.0
act_mapper Optional[dict]

dictionary of activity for each molecule

None
align Optional[Union[Mol, str, bool]]

Whether to align the 2D coordinates of the molecules. - If set to True, align all molecules with dm.align.auto_align_many(). - If set to a molecule, it is used as a template for alignment with dm.align.template_align(). - If set to False, no alignment is performed. - If set to None (default), the ring (peripheral) molecules are aligned to the center molecules. This is the default behaviour.

None
use_svg bool

Whether to use SVG or use PNG

True
line_width Optional[float]

Width of the lines to draw

None
center_mol_highlight_atoms Optional[List[int]]

List of atom indices to highlight for the center molecule

None
center_mol_highlight_bonds Optional[List[int]]

List of bond indices to highlight for the center molecule

None
ring_mol_highlight_atoms Optional[List[List[int]]]

List of list of atom indices to highlight for molecules at each level of the concentric rings

None
ring_mol_highlight_bonds Optional[List[List[int]]]

List of list of bond indices to highlight for molecules at each level of the concentric rings

None
ring_color Optional[DatamolColor]

Color of the concentric rings. Set to None to not draw any ring.

None
ring_mol_start_angles_degrees Optional[List[float]]

List of angles in degrees to start drawing the molecules at each level of the concentric rings. If None then a random position will be used.

None
kekulize bool

Whether to kekulize the molecules before drawing.

True
layout_random_seed Optional[int]

Random seed for the layout of the molecules. Set to None for no seed.

19
**kwargs Any

Additional arguments to pass to the drawing function. See RDKit documentation related to MolDrawOptions for more details at https://www.rdkit.org/docs/source/rdkit.Chem.Draw.rdMolDraw2D.html.

{}
Source code in datamol/viz/_circle_grid.py
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
def __init__(
    self,
    center_mol: Union[Mol, str],
    ring_mols: List[List[Union[Mol, str]]],
    legend: Optional[str] = None,
    margin: int = 50,
    ring_scaler: float = 1.0,
    act_mapper: Optional[dict] = None,
    align: Optional[Union[Mol, str, bool]] = None,
    use_svg: bool = True,
    line_width: Optional[float] = None,
    ring_color: Optional[DatamolColor] = None,
    ring_mol_start_angles_degrees: Optional[List[float]] = None,
    center_mol_highlight_atoms: Optional[List[int]] = None,
    center_mol_highlight_bonds: Optional[List[int]] = None,
    ring_mol_highlight_atoms: Optional[List[List[int]]] = None,
    ring_mol_highlight_bonds: Optional[List[List[int]]] = None,
    kekulize: bool = True,
    layout_random_seed: Optional[int] = 19,
    **kwargs: Any,
):
    """Show molecules in concentric rings, with one molecule at the center

    Args:
        center_mol: Molecule at center of the rings
        ring_mols: List of molecule for each level of concentric rings around the center mol
        legend: optional global legend for the figure
        margin: Margin between the circle layers
        ring_scaler: Scale the size of the molecules in each circle by this factor compared to the center molecule
        act_mapper: dictionary of activity for each molecule
        align: Whether to align the 2D coordinates of the molecules.
            - If set to True, align all molecules with `dm.align.auto_align_many()`.
            - If set to a molecule, it is used as a template for alignment with `dm.align.template_align()`.
            - If set to False, no alignment is performed.
            - If set to None (default), the ring (peripheral) molecules are aligned to the center molecules. This is the default behaviour.
        use_svg: Whether to use SVG or use PNG
        line_width: Width of the lines to draw
        center_mol_highlight_atoms: List of atom indices to highlight for the center molecule
        center_mol_highlight_bonds: List of bond indices to highlight for the center molecule
        ring_mol_highlight_atoms: List of list of atom indices to highlight for molecules at each level of the concentric rings
        ring_mol_highlight_bonds: List of list of bond indices to highlight for molecules at each level of the concentric rings
        ring_color: Color of the concentric rings. Set to None to not draw any ring.
        ring_mol_start_angles_degrees: List of angles in degrees to start drawing the molecules at each level of the concentric
            rings. If None then a random position will be used.
        kekulize: Whether to kekulize the molecules before drawing.
        layout_random_seed: Random seed for the layout of the molecules. Set to None for no seed.
        **kwargs: Additional arguments to pass to the drawing function. See RDKit
            documentation related to `MolDrawOptions` for more details at
            https://www.rdkit.org/docs/source/rdkit.Chem.Draw.rdMolDraw2D.html.
    """
    assert dm.is_greater_than_current_rdkit_version(
        "2023.03"
    ), "MolsCircleGrid requires RDKit version >= 2022.09"
    self.center_mol = center_mol
    self.ring_mols = ring_mols
    if not isinstance(self.ring_mols[0], (tuple, list, np.ndarray)):
        self.ring_mols = [self.ring_mols]
    self.ring_count = len(self.ring_mols)
    self.legend = legend or None
    self.margin = margin
    self.ring_scaler = ring_scaler
    self.align = align
    self.use_svg = use_svg
    self.line_width = line_width
    self.ring_color = ring_color
    self.ring_mol_start_angles_degrees = ring_mol_start_angles_degrees
    self.ring_color_rdkit: Optional[RDKitColor] = to_rdkit_color(ring_color)
    self.ring_mol_highlight_atoms = ring_mol_highlight_atoms
    self.ring_mol_highlight_bonds = ring_mol_highlight_bonds
    self.center_mol_highlight_atoms = center_mol_highlight_atoms
    self.center_mol_highlight_bonds = center_mol_highlight_bonds
    self.kekulize = kekulize
    self.layout_random_seed = layout_random_seed
    self._global_legend_size = 0
    if self.legend is not None:
        self._global_legend_size = max(25, self.margin)
    self._format_activity_mapper(act_mapper)
    self._initialize_drawing(**kwargs)

draw()

Create and draw the circular molecule image

Source code in datamol/viz/_circle_grid.py
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
def draw(self):
    """Create and draw the circular molecule image"""
    radius_list = self._draw_circles()

    # draw the center mol
    self._draw_mol_at(
        self.center_mol,
        self.midpoint,
        highlight_atom=self.center_mol_highlight_atoms,
        highlight_bond=self.center_mol_highlight_bonds,
    )

    rng = random.Random(self.layout_random_seed)

    # draw the ring mols
    self.draw_options.scalingFactor *= self.ring_scaler
    for i, mols in enumerate(self.ring_mols):
        radius = radius_list[i]
        ni = len(mols)

        if self.ring_mol_start_angles_degrees is not None:
            rand_unit = np.deg2rad(self.ring_mol_start_angles_degrees[i])
        else:
            rand_unit = rng.random() * 2 * math.pi

        for k, mol in enumerate(mols):
            center_x = radius * math.cos(2 * k * math.pi / ni + rand_unit) + self.midpoint.x
            center_y = radius * math.sin(2 * k * math.pi / ni + rand_unit) + self.midpoint.y
            center = Point2D(center_x, center_y)
            ring_atom_highlight = None
            if self.ring_mol_highlight_atoms is not None:
                ring_atom_highlight = self.ring_mol_highlight_atoms[i][k]
            ring_bond_highlight = None
            if self.ring_mol_highlight_bonds is not None:
                ring_bond_highlight = self.ring_mol_highlight_bonds[i][k]
            self._draw_mol_at(
                mol,
                center,
                highlight_atom=ring_atom_highlight,
                highlight_bond=ring_bond_highlight,
            )

    self.draw_options.scalingFactor /= self.ring_scaler
    # draw global legend if there is any
    if self.legend is not None:
        self.canvas.SetFontSize(self.canvas.FontSize() * 3)
        text_position = Point2D(
            self.size[0] // 2, self.size[-1] - self._global_legend_size // 2
        )
        self.canvas.DrawString(self.legend, text_position, align=0, rawCoords=True)

    self.canvas.FinishDrawing()
    return drawer_to_image(self.canvas)

Show molecules in concentric rings, with one molecule at the center Args: center_mol: Molecule at center of the rings ring_mols: List of molecule for each level of concentric rings around the center mol legend: optional global legend for the figure margin: Margin between the circle layers ring_scaler: Scale the size of the molecules in each circle by this factor compared to the center molecule act_mapper: dictionary of activity for each molecule align: Whether to align the 2D coordinates of the molecules. - If set to True, align all molecules with dm.align.auto_align_many(). - If set to a molecule, it is used as a template for alignment with dm.align.template_align(). - If set to False, no alignment is performed. - If set to None (default), the ring (peripheral) molecules are aligned to the center molecules. use_svg: Whether to use SVG or use PNG center_mol_highlight_atoms: List of atom indices to highlight for the center molecule center_mol_highlight_bonds: List of bond indices to highlight for the center molecule ring_mol_highlight_atoms: List of list of atom indices to highlight for molecules at each level of the concentric rings ring_mol_highlight_bonds: List of list of bond indices to highlight for molecules at each level of the concentric rings ring_color: Color of the concentric rings. Set to None to not draw any ring. ring_mol_start_angles_degrees: List of angles in degrees to start drawing the molecules at each level of the concentric rings. If None then a random position will be used. kekulize: Whether to kekulize the molecules before drawing. layout_random_seed: Random seed for the layout of the molecules. Set to None for no seed. outfile: Optional path to the save the output file. **kwargs: Additional arguments to pass to the drawing function. See RDKit documentation related to MolDrawOptions for more details at https://www.rdkit.org/docs/source/rdkit.Chem.Draw.rdMolDraw2D.html.

Source code in datamol/viz/_circle_grid.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
def circle_grid(
    center_mol: Union[Mol, str],
    ring_mols: List[List[Union[Mol, str]]],
    act_mapper: Optional[dict] = None,
    margin: int = 50,
    legend: Optional[str] = None,
    ring_scaler: float = 1.0,
    align: Optional[Union[Mol, str, bool]] = None,
    use_svg: bool = True,
    ring_color: Optional[DatamolColor] = None,
    ring_mol_start_angles_degrees: Optional[List[float]] = None,
    center_mol_highlight_atoms: Optional[List[int]] = None,
    center_mol_highlight_bonds: Optional[List[int]] = None,
    ring_mol_highlight_atoms: Optional[List[List[int]]] = None,
    ring_mol_highlight_bonds: Optional[List[List[int]]] = None,
    outfile: Optional[str] = None,
    kekulize: bool = True,
    layout_random_seed: Optional[int] = 19,
    **kwargs: Any,
):
    """Show molecules in concentric rings, with one molecule at the center
    Args:
        center_mol: Molecule at center of the rings
        ring_mols: List of molecule for each level of concentric rings around the center mol
        legend: optional global legend for the figure
        margin: Margin between the circle layers
        ring_scaler: Scale the size of the molecules in each circle by this factor compared to the center molecule
        act_mapper: dictionary of activity for each molecule
        align: Whether to align the 2D coordinates of the molecules.
            - If set to True, align all molecules with `dm.align.auto_align_many()`.
            - If set to a molecule, it is used as a template for alignment with `dm.align.template_align()`.
            - If set to False, no alignment is performed.
            - If set to None (default), the ring (peripheral) molecules are aligned to the center molecules.
        use_svg: Whether to use SVG or use PNG
        center_mol_highlight_atoms: List of atom indices to highlight for the center molecule
        center_mol_highlight_bonds: List of bond indices to highlight for the center molecule
        ring_mol_highlight_atoms: List of list of atom indices to highlight for molecules at each level of the concentric rings
        ring_mol_highlight_bonds: List of list of bond indices to highlight for molecules at each level of the concentric rings
        ring_color: Color of the concentric rings. Set to None to not draw any ring.
        ring_mol_start_angles_degrees: List of angles in degrees to start drawing the molecules at each level of the concentric
            rings. If None then a random position will be used.
        kekulize: Whether to kekulize the molecules before drawing.
        layout_random_seed: Random seed for the layout of the molecules. Set to None for no seed.
        outfile: Optional path to the save the output file.
        **kwargs: Additional arguments to pass to the drawing function. See RDKit
            documentation related to `MolDrawOptions` for more details at
            https://www.rdkit.org/docs/source/rdkit.Chem.Draw.rdMolDraw2D.html.

    """

    grid = MolsCircleGrid(
        center_mol,
        ring_mols,
        legend=legend,
        margin=margin,
        ring_scaler=ring_scaler,
        act_mapper=act_mapper,
        align=align,
        use_svg=use_svg,
        ring_color=ring_color,
        ring_mol_start_angles_degrees=ring_mol_start_angles_degrees,
        center_mol_highlight_atoms=center_mol_highlight_atoms,
        center_mol_highlight_bonds=center_mol_highlight_bonds,
        ring_mol_highlight_atoms=ring_mol_highlight_atoms,
        ring_mol_highlight_bonds=ring_mol_highlight_bonds,
        kekulize=kekulize,
        layout_random_seed=layout_random_seed,
        **kwargs,
    )
    return grid(outfile=outfile)

Vizualize 2D molecule with highlighted substructures

Create an image of a list of molecules with substructure matches using lasso-based highlighting. Substructure matching is optional and it's also possible to pass a list of list of atom indices to highlight.

Parameters:

Name Type Description Default
target_molecules Union[str, Mol, List[Union[str, Mol]]]

One or a list of molecules to be highlighted.

required
search_molecules Union[str, List[str], Mol, List[Mol]]

The substructure to be highlighted.

None
atom_indices Optional[Union[List[int], List[List[int]]]]

Atom indices to be highlighted as substructure using the lasso visualization.

None
legends Union[List[Union[str, None]], str, None]

A string or a list of string as legend for every molecules.

None
n_cols int

Number of molecules per column.

4
mol_size Tuple[int, int]

The size of the image to be returned

(300, 300)
use_svg Optional[bool]

Whether to return an svg or png image

True
draw_mols_same_scale bool

Whether to draw all the molecules on the same scale. This has the same effect as the drawMolsSameScale of the drawing options (which cannot be applied).

True
r_min float

Radius of the smallest circle around atoms. Length is relative to average bond length (1 = avg. bond len).

0.3
r_dist float

Incremental increase of radius for the next substructure.

0.13
relative_bond_width float

Distance of line to "bond" (line segment between the two atoms). Size is relative to atom_radius.

0.5
color_list Optional[List[DatamolColor]]

List of tuples with RGBA or RGB values specifying the color of the highlighting.

None
line_width int

width of drawn lines.

2
scale_padding float

Padding around the molecule when drawing to scale.

1.0
verbose bool

Whether to print the verbose information.

False
highlight_atoms Optional[List[List[int]]]

The atoms to highlight, a list for each molecule. It's the highlightAtoms argument of the RDKit drawer object.

None
highlight_bonds Optional[List[List[int]]]

The bonds to highlight, a list for each molecule. It's the highlightBonds argument of the RDKit drawer object.

None
highlight_atom_colors Optional[List[Dict[int, DatamolColor]]]

The colors to use for highlighting atoms, a list of dict mapping atom index to color for each molecule.

None
highlight_bond_colors Optional[List[Dict[int, DatamolColor]]]

The colors to use for highlighting bonds, a list of dict mapping bond index to color for each molecule.

None
**kwargs Any

Additional arguments to pass to the drawing function. See RDKit documentation related to MolDrawOptions for more details at https://www.rdkit.org/docs/source/rdkit.Chem.Draw.rdMolDraw2D.html.

{}
Source code in datamol/viz/_lasso_highlight.py
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
def lasso_highlight_image(
    target_molecules: Union[str, dm.Mol, List[Union[str, dm.Mol]]],
    search_molecules: Union[str, List[str], dm.Mol, List[dm.Mol]] = None,
    atom_indices: Optional[Union[List[int], List[List[int]]]] = None,
    legends: Union[List[Union[str, None]], str, None] = None,
    n_cols: int = 4,
    mol_size: Tuple[int, int] = (300, 300),
    use_svg: Optional[bool] = True,
    draw_mols_same_scale: bool = True,
    r_min: float = 0.3,
    r_dist: float = 0.13,
    relative_bond_width: float = 0.5,
    color_list: Optional[List[DatamolColor]] = None,
    line_width: int = 2,
    scale_padding: float = 1.0,
    verbose: bool = False,
    highlight_atoms: Optional[List[List[int]]] = None,
    highlight_bonds: Optional[List[List[int]]] = None,
    highlight_atom_colors: Optional[List[Dict[int, DatamolColor]]] = None,
    highlight_bond_colors: Optional[List[Dict[int, DatamolColor]]] = None,
    **kwargs: Any,
):
    """Create an image of a list of molecules with substructure matches using lasso-based highlighting.
    Substructure matching is optional and it's also possible to pass a list of list of atom indices to highlight.

    Args:
        target_molecules:  One or a list of molecules to be highlighted.
        search_molecules: The substructure to be highlighted.
        atom_indices: Atom indices to be highlighted as substructure using the lasso visualization.
        legends: A string or a list of string as legend for every molecules.
        n_cols: Number of molecules per column.
        mol_size: The size of the image to be returned
        use_svg: Whether to return an svg or png image
        draw_mols_same_scale: Whether to draw all the molecules on the same scale. This has the same effect as the `drawMolsSameScale` of the drawing options (which cannot be applied).
        r_min: Radius of the smallest circle around atoms. Length is relative to average bond length (1 = avg. bond len).
        r_dist: Incremental increase of radius for the next substructure.
        relative_bond_width: Distance of line to "bond" (line segment between the two atoms). Size is relative to `atom_radius`.
        color_list: List of tuples with RGBA or RGB values specifying the color of the highlighting.
        line_width: width of drawn lines.
        scale_padding: Padding around the molecule when drawing to scale.
        verbose: Whether to print the verbose information.
        highlight_atoms: The atoms to highlight, a list for each molecule. It's the `highlightAtoms` argument of the RDKit drawer object.
        highlight_bonds: The bonds to highlight, a list for each molecule. It's the `highlightBonds` argument of the RDKit drawer object.
        highlight_atom_colors: The colors to use for highlighting atoms, a list of dict mapping atom index to color for each molecule.
        highlight_bond_colors: The colors to use for highlighting bonds, a list of dict mapping bond index to color for each molecule.
        **kwargs: Additional arguments to pass to the drawing function. See RDKit
            documentation related to `MolDrawOptions` for more details at
            https://www.rdkit.org/docs/source/rdkit.Chem.Draw.rdMolDraw2D.html.
    """

    ## Step 0: Input validation

    if search_molecules is None:
        search_molecules = []

    # Make `search_molecules` a list if it is not already
    if not isinstance(search_molecules, (list, tuple)):
        search_molecules = [search_molecules]

    # Convert search molecules into RDKit patterns
    search_molecules = list(search_molecules)
    for i, search_mol in enumerate(search_molecules):
        if isinstance(search_mol, str):
            search_molecules[i] = dm.from_smarts(search_mol)

        if search_molecules[i] is None or not isinstance(search_molecules[i], dm.Mol):
            raise ValueError(
                f"Please enter valid search molecules or smarts: {search_molecules[i]}"
            )

    if not isinstance(target_molecules, (list, tuple)):
        target_molecules = [target_molecules]

    if n_cols is None:
        n_cols = 4

    n_cols = min(n_cols, len(target_molecules))
    n_rows = len(target_molecules) // n_cols
    if len(target_molecules) % n_cols:
        n_rows += 1

    if legends is None:
        legends = [""] * len(target_molecules)
    elif isinstance(legends, str):
        legends = [legends] * len(target_molecules)

    ## Step 1: setup drawer and canvas
    if use_svg:
        drawer = rdMolDraw2D.MolDraw2DSVG(
            mol_size[0] * n_cols,
            mol_size[1] * n_rows,
            mol_size[0],
            mol_size[1],
        )
    else:
        drawer = rdMolDraw2D.MolDraw2DCairo(
            mol_size[0] * n_cols,
            mol_size[1] * n_rows,
            mol_size[0],
            mol_size[1],
        )

    # Setting the drawing options
    draw_options = drawer.drawOptions()
    kwargs = kwargs.copy()
    del_attr = []
    for k, v in kwargs.items():
        if hasattr(draw_options, k):
            setattr(draw_options, k, v)
            del_attr.append(k)
    kwargs = {k: v for k, v in kwargs.items() if k not in del_attr}
    # we will pass the remaining kwargs to the draw function later on

    if color_list is None:
        color_list = DEFAULT_LASSO_COLORS

    mols_to_draw = []
    atoms_idx_list = []
    # scaling parameters
    min_scale_val = Point2D(np.inf, np.inf)
    max_scale_val = Point2D(-np.inf, -np.inf)
    color_list_indexes = []
    ## Step 2: Prepare all the inputs for drawing
    for mol_idx, target_molecule in enumerate(target_molecules):
        # check if the input is valid
        if target_molecule is None:
            raise ValueError("Please enter a valid target molecule or smiles")

        if isinstance(target_molecule, str):
            target_molecule = dm.to_mol(target_molecule)

        # Always make the type checker happy
        target_mol = cast(dm.Mol, target_molecule)
        # Match the search molecules or SMARTS to the target molecule
        atom_idx_list = []
        target_color_list = []
        for color_idx, search_mol in enumerate(search_molecules):
            matches = target_mol.GetSubstructMatches(search_mol)
            if matches:
                matched_atoms = set.union(*[set(x) for x in matches])
                atom_idx_list.append(matched_atoms)
                target_color_list.append(color_idx % len(color_list))
            elif verbose:
                logger.warning(f"No matching substructures found for {dm.to_smarts(search_mol)}")

        ## Add the atom indices to the list if any
        if atom_indices is not None:
            if not isinstance(atom_indices[0], (list, tuple)):
                atom_indices_list_of_list = [atom_indices]
            else:
                atom_indices_list_of_list = atom_indices
            atom_idx_list += atom_indices_list_of_list

        atoms_idx_list.append(atom_idx_list)
        color_list_indexes.append(target_color_list)

        ## Prepare the molecule for drawing and draw it
        mol = prepare_mol_for_drawing(target_molecule, kekulize=True)
        if mol is None:
            raise ValueError(
                f"A molecule {mol_idx} has failed to be prepared by `prepare_mol_for_drawing`."
            )

        conf = mol.GetConformer()
        coordinates = conf.GetPositions()
        min_scale_val.x = min(min_scale_val.x, min(coordinates[:, 0]))
        min_scale_val.y = min(min_scale_val.y, min(coordinates[:, 1]))
        max_scale_val.x = max(max_scale_val.x, max(coordinates[:, 0]))
        max_scale_val.y = max(max_scale_val.y, max(coordinates[:, 1]))
        mols_to_draw.append(mol)

    # Setting up the coordinate system by drawing the molecules as a grid
    # EN: the following is edge-case free after trying 6 different logics, but may break if RDKit changes the way it draws molecules
    scaling_val = Point2D(scale_padding, scale_padding)

    if isinstance(highlight_atoms, list) and isinstance(highlight_atoms[0], int):
        highlight_atoms = [highlight_atoms] * len(target_molecules)
    if isinstance(highlight_bonds, list) and isinstance(highlight_bonds[0], int):
        highlight_bonds = [highlight_bonds] * len(target_molecules)
    if isinstance(highlight_atom_colors, dict):
        highlight_atom_colors = [highlight_atom_colors] * len(target_molecules)
    if isinstance(highlight_bond_colors, dict):
        highlight_bond_colors = [highlight_bond_colors] * len(target_molecules)

    # make sure we are using rdkit colors
    if highlight_atom_colors is not None:
        highlight_atom_colors = [
            {k: to_rdkit_color(v) for k, v in _.items()} for _ in highlight_atom_colors
        ]
    if highlight_bond_colors is not None:
        highlight_bond_colors = [
            {k: to_rdkit_color(v) for k, v in _.items()} for _ in highlight_bond_colors
        ]

    kwargs["highlightAtoms"] = highlight_atoms
    kwargs["highlightBonds"] = highlight_bonds
    kwargs["highlightAtomColors"] = highlight_atom_colors
    kwargs["highlightBondColors"] = highlight_bond_colors

    try:
        drawer.DrawMolecules(
            mols_to_draw,
            legends=legends,
            **kwargs,
        )
    except Exception as e:
        logger.error(e)
        raise ValueError(
            "Failed to draw molecules. Some arguments neither match expected MolDrawOptions, nor DrawMolecule inputs. Please check the input arguments."
        )
    drawer.ClearDrawing()
    if draw_mols_same_scale:
        drawer.SetScale(
            mol_size[0], mol_size[1], min_scale_val - scaling_val, max_scale_val + scaling_val
        )

    for ind, (mol, atom_idx_list) in enumerate(zip(mols_to_draw, atoms_idx_list)):
        h_pos, w_pos = np.unravel_index(ind, (n_rows, n_cols))
        offset_x = int(w_pos * mol_size[0])
        offset_y = int(h_pos * mol_size[1])

        ind_kwargs = kwargs.copy()
        if isinstance(ind_kwargs["highlightAtoms"], list):
            ind_kwargs["highlightAtoms"] = ind_kwargs["highlightAtoms"][ind]
        if isinstance(ind_kwargs["highlightAtomColors"], list):
            ind_kwargs["highlightAtomColors"] = ind_kwargs["highlightAtomColors"][ind]
        if isinstance(ind_kwargs["highlightBonds"], list):
            ind_kwargs["highlightBonds"] = ind_kwargs["highlightBonds"][ind]
        if isinstance(ind_kwargs["highlightBondColors"], list):
            ind_kwargs["highlightBondColors"] = ind_kwargs["highlightBondColors"][ind]
        drawer.SetOffset(offset_x, offset_y)
        drawer.DrawMolecule(mol, legend=legends[ind], **ind_kwargs)
        offset = None
        if draw_mols_same_scale:
            offset = drawer.Offset()
            # EN: if the molecule has a legend we need to offset the highlight by the height of the legend
            # we also need to account for the padding around the molecule
            if legends[ind]:
                # geometry is hard
                padding_fraction = (drawer.drawOptions().legendFraction * mol_size[1]) // 2
                offset -= Point2D(0, padding_fraction)

        if len(atom_idx_list) > 0:
            dm.viz._lasso_highlight._draw_multi_matches(
                drawer,
                mol,
                atom_idx_list,
                r_min=r_min,
                r_dist=r_dist,
                relative_bond_width=relative_bond_width,
                line_width=line_width,
                color_list=[color_list[i] for i in color_list_indexes[ind]],
                offset=offset,
            )
        elif verbose:
            logger.warning("No matches found for the given search molecules")

    drawer.FinishDrawing()
    # NOTE(hadim): process the drawer object to return the image type matching the same behavior as RDkit and `datamol.to_image()`
    return drawer_to_image(drawer)