rdkit-dof 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
rdkit_dof/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ from .config import DofDrawSettings, dofconfig
2
+ from .core import MolGridToDofImage, MolToDofImage
3
+
4
+ __all__ = [
5
+ "MolGridToDofImage",
6
+ "MolToDofImage",
7
+ "DofDrawSettings",
8
+ "dofconfig"
9
+ ]
10
+
11
+ __version__ = "0.1.0"
rdkit_dof/config.py ADDED
@@ -0,0 +1,119 @@
1
+ """
2
+ Author: TMJ
3
+ Date: 2025-12-01 12:38:03
4
+ LastEditors: TMJ
5
+ LastEditTime: 2025-12-01 15:48:00
6
+ Description: 请填写简介
7
+ """
8
+
9
+ from typing import Literal, Union
10
+
11
+ from pydantic import Field, model_validator
12
+ from pydantic_settings import BaseSettings, SettingsConfigDict
13
+ from rdkit.Chem import Mol, RWMol
14
+ from typing_extensions import Self
15
+
16
+ from .palettes import DARK_NEON_STYLE, DEFAULT_STYLE, JACS_STYLE, NATURE_STYLE
17
+
18
+ StyleName = Literal["default", "nature", "jacs", "dark"]
19
+
20
+
21
+ class DofDrawSettings(BaseSettings):
22
+ """
23
+ Logic:
24
+ 1. load preset style
25
+ 2. if dark mode, and fog color is default, set fog color to (0.1, 0.1, 0.1)
26
+ 3. override specific atoms with user-provided atom_colors
27
+ """
28
+
29
+ preset_style: StyleName = "default"
30
+
31
+ fog_color: tuple[float, float, float] = (0.95, 0.95, 0.95)
32
+ min_alpha: float = 0.4
33
+ default_size: tuple[int, int] = (800, 800)
34
+
35
+ enable_ipython: bool = True
36
+
37
+ atom_colors: dict[int, tuple[float, float, float]] = Field(default_factory=dict)
38
+
39
+ model_config = SettingsConfigDict(
40
+ env_file=".env", env_prefix="MOL_DOF_", extra="ignore"
41
+ )
42
+
43
+ @model_validator(mode="after")
44
+ def _init_merge_configuration(self) -> Self:
45
+ self._apply_style_logic(self.preset_style)
46
+ return self
47
+
48
+ def _apply_style_logic(self, style: str):
49
+ style_map = {
50
+ "default": DEFAULT_STYLE,
51
+ "nature": NATURE_STYLE,
52
+ "jacs": JACS_STYLE,
53
+ "dark": DARK_NEON_STYLE,
54
+ }
55
+ # If preset_style is specified, use it; otherwise, use default
56
+ base_colors = style_map.get(style, DEFAULT_STYLE).copy()
57
+
58
+ if self.preset_style == "dark":
59
+ if self.fog_color == (0.95, 0.95, 0.95):
60
+ self.fog_color = (0.1, 0.1, 0.1)
61
+
62
+ if self.atom_colors:
63
+ base_colors.update(self.atom_colors)
64
+
65
+ self.atom_colors = base_colors
66
+
67
+ def get_atom_color(self, atomic_num: int) -> tuple[float, float, float]:
68
+ return self.atom_colors.get(
69
+ atomic_num, self.atom_colors.get(6, (0.2, 0.2, 0.2))
70
+ )
71
+
72
+ def use_style(self, style: StyleName):
73
+ """
74
+ Switch to a different preset style, resetting any custom atom colors.
75
+ """
76
+ self.preset_style = style
77
+ self.atom_colors = {}
78
+ self._apply_style_logic(style)
79
+
80
+ def enable_ipython_integration(self, enable: bool = True):
81
+ """
82
+ Toggle whether to use the DOF effect drawer as the default renderer
83
+ for RDKit Mol objects in Jupyter/IPython.
84
+
85
+ Args:
86
+ enable (bool): True to enable DOF rendering, False to restore RDKit default.
87
+ """
88
+ try:
89
+ from IPython.core.getipython import get_ipython
90
+ except ImportError:
91
+ return
92
+ ip = get_ipython()
93
+ if ip is None:
94
+ return
95
+ svg_formatter = ip.display_formatter.formatters["image/svg+xml"] # type: ignore
96
+
97
+ if enable:
98
+ from .core import MolToDofImage
99
+
100
+ def _dof_drawer_hook(mol: Union[Mol, RWMol]) -> str:
101
+ return MolToDofImage(
102
+ mol,
103
+ use_svg=True,
104
+ return_image=False, # 必须为 False,返回 SVG 源码字符串
105
+ settings=self, # 绑定当前配置实例
106
+ )
107
+
108
+ svg_formatter.for_type(Mol, _dof_drawer_hook)
109
+ svg_formatter.for_type(RWMol, _dof_drawer_hook)
110
+ else:
111
+ if Mol in svg_formatter.type_printers:
112
+ svg_formatter.type_printers.pop(Mol)
113
+ if RWMol in svg_formatter.type_printers:
114
+ svg_formatter.type_printers.pop(RWMol)
115
+
116
+
117
+ dofconfig = DofDrawSettings()
118
+ if dofconfig.enable_ipython:
119
+ dofconfig.enable_ipython_integration(True)
rdkit_dof/core.py ADDED
@@ -0,0 +1,384 @@
1
+ import io
2
+ import math
3
+ from collections.abc import Sequence
4
+ from functools import lru_cache
5
+ from typing import Any, Literal, Optional, Union, overload
6
+
7
+ import numpy as np
8
+ from PIL import Image
9
+ from rdkit import Chem
10
+ from rdkit.Chem.Draw import (
11
+ IPythonConsole, # type: ignore
12
+ MolDrawOptions,
13
+ rdMolDraw2D,
14
+ )
15
+ from rdkit.Chem.rdDepictor import Compute2DCoords
16
+
17
+ from .config import DofDrawSettings, dofconfig
18
+
19
+ try:
20
+ from IPython.display import SVG
21
+ except ImportError:
22
+ svg_support = False
23
+ else:
24
+ svg_support = True
25
+
26
+
27
+ @lru_cache(maxsize=4096)
28
+ def _get_atom_dof_color_cached(
29
+ base_color: tuple[float, float, float],
30
+ proximity: float,
31
+ min_alpha: float,
32
+ fog_color: tuple[float, float, float],
33
+ ) -> tuple[float, float, float, float]:
34
+ """Calculate the RGBA color of an atom with depth-of-field effect."""
35
+ base_rgb = np.array(base_color)
36
+ fog_rgb = np.array(fog_color)
37
+ dark_color_rgba = np.array([*base_rgb, 1.0])
38
+ light_rgb = base_rgb * 0.2 + fog_rgb * 0.8
39
+ light_color_rgba = np.array([*light_rgb, min_alpha])
40
+ final_color = light_color_rgba + proximity * (dark_color_rgba - light_color_rgba)
41
+ return tuple(final_color.tolist())
42
+
43
+
44
+ def _apply_rdkit_global_options(target_dopts: MolDrawOptions):
45
+ """Reflect global options from IPythonConsole.drawOptions."""
46
+ if IPythonConsole is None or not hasattr(IPythonConsole, "drawOptions"):
47
+ return
48
+ source_dopts: MolDrawOptions = IPythonConsole.drawOptions
49
+ if source_dopts is None:
50
+ return
51
+ for attr in dir(source_dopts):
52
+ if attr.startswith("_"):
53
+ continue
54
+ try:
55
+ val = getattr(source_dopts, attr)
56
+ if callable(val):
57
+ continue
58
+ if hasattr(target_dopts, attr):
59
+ setattr(target_dopts, attr, val)
60
+ except Exception:
61
+ pass
62
+
63
+
64
+ def _prepare_mol_data(
65
+ mol: Union[Chem.Mol, Chem.RWMol],
66
+ settings: DofDrawSettings,
67
+ keep_key_atom_colors: bool = True,
68
+ ) -> tuple[
69
+ Chem.Mol,
70
+ dict[int, tuple[float, float, float, float]],
71
+ dict[int, tuple[float, float, float, float]],
72
+ ]:
73
+ """
74
+ Internal Helper: Process a single molecule for DOF drawing.
75
+ Returns: (Prepared Molecule, Atom Colors Dict, Bond Colors Dict)
76
+ """
77
+ if not mol:
78
+ raise ValueError("Invalid molecule")
79
+
80
+ mol_copy = Chem.Mol(mol)
81
+
82
+ if mol_copy.GetNumConformers() == 0:
83
+ Compute2DCoords(mol_copy)
84
+
85
+ conf = mol_copy.GetConformer()
86
+ pos = conf.GetPositions()
87
+ z_coords = pos[:, 2]
88
+
89
+ if z_coords.size > 1 and z_coords.max() != z_coords.min():
90
+ norm_z = (z_coords - z_coords.min()) / (z_coords.max() - z_coords.min())
91
+ proximity = norm_z
92
+ else:
93
+ proximity = np.full(z_coords.shape, 1.0)
94
+
95
+ highlight_atom_colors: dict[int, tuple[float, float, float, float]] = {}
96
+ carbon_base_color = settings.get_atom_color(6)
97
+
98
+ for i in range(mol_copy.GetNumAtoms()):
99
+ atom = mol_copy.GetAtomWithIdx(i)
100
+ atomic_num = atom.GetAtomicNum()
101
+ base_color = settings.get_atom_color(atomic_num)
102
+ target_color = base_color if keep_key_atom_colors else carbon_base_color
103
+
104
+ highlight_atom_colors[i] = _get_atom_dof_color_cached(
105
+ base_color=target_color,
106
+ proximity=proximity[i],
107
+ min_alpha=settings.min_alpha,
108
+ fog_color=settings.fog_color,
109
+ )
110
+
111
+ highlight_bond_colors: dict[int, tuple[float, float, float, float]] = {}
112
+ for i in range(mol_copy.GetNumBonds()):
113
+ bond = mol_copy.GetBondWithIdx(i)
114
+ atom1_idx = bond.GetBeginAtomIdx()
115
+ atom2_idx = bond.GetEndAtomIdx()
116
+ c1 = _get_atom_dof_color_cached(
117
+ carbon_base_color,
118
+ proximity[atom1_idx],
119
+ settings.min_alpha,
120
+ settings.fog_color,
121
+ )
122
+ c2 = _get_atom_dof_color_cached(
123
+ carbon_base_color,
124
+ proximity[atom2_idx],
125
+ settings.min_alpha,
126
+ settings.fog_color,
127
+ )
128
+ bond_color_arr = (np.array(c1) + np.array(c2)) / 2
129
+ highlight_bond_colors[i] = tuple(bond_color_arr.tolist())
130
+
131
+ return mol_copy, highlight_atom_colors, highlight_bond_colors
132
+
133
+
134
+ # =============================================================================
135
+ # Single Molecule Drawer
136
+ # =============================================================================
137
+
138
+
139
+ @overload
140
+ def MolToDofImage(
141
+ mol: Union[Chem.Mol, Chem.RWMol],
142
+ size: Optional[tuple[int, int]] = None,
143
+ legend: str = "",
144
+ use_svg: Literal[True] = True,
145
+ return_image: Literal[True] = True,
146
+ *,
147
+ settings: Optional[DofDrawSettings] = None,
148
+ **kwargs: Any,
149
+ ) -> "SVG": ...
150
+ @overload
151
+ def MolToDofImage(
152
+ mol: Union[Chem.Mol, Chem.RWMol],
153
+ size: Optional[tuple[int, int]] = None,
154
+ legend: str = "",
155
+ use_svg: Literal[False] = False,
156
+ return_image: Literal[True] = True,
157
+ *,
158
+ settings: Optional[DofDrawSettings] = None,
159
+ **kwargs: Any,
160
+ ) -> Image.Image: ...
161
+ @overload
162
+ def MolToDofImage(
163
+ mol: Union[Chem.Mol, Chem.RWMol],
164
+ size: Optional[tuple[int, int]] = None,
165
+ legend: str = "",
166
+ use_svg: Literal[True] = True,
167
+ return_image: Literal[False] = False,
168
+ *,
169
+ settings: Optional[DofDrawSettings] = None,
170
+ **kwargs: Any,
171
+ ) -> str: ...
172
+ @overload
173
+ def MolToDofImage(
174
+ mol: Union[Chem.Mol, Chem.RWMol],
175
+ size: Optional[tuple[int, int]] = None,
176
+ legend: str = "",
177
+ use_svg: Literal[False] = False,
178
+ return_image: Literal[False] = False,
179
+ *,
180
+ settings: Optional[DofDrawSettings] = None,
181
+ **kwargs: Any,
182
+ ) -> bytes: ...
183
+
184
+
185
+ def MolToDofImage( # noqa: N802
186
+ mol: Union[Chem.Mol, Chem.RWMol],
187
+ size: Optional[tuple[int, int]] = None,
188
+ legend: str = "",
189
+ use_svg: bool = True,
190
+ return_image: bool = True,
191
+ *,
192
+ settings: Optional[DofDrawSettings] = None,
193
+ **kwargs: Any,
194
+ ) -> Union["SVG", str, Image.Image, bytes]:
195
+ """Draw a single molecule with DOF effect."""
196
+ if settings is None:
197
+ settings = dofconfig
198
+
199
+ draw_size = size if size else settings.default_size
200
+
201
+ ready_mol, atom_colors, bond_colors = _prepare_mol_data(mol, settings)
202
+
203
+ if use_svg:
204
+ drawer = rdMolDraw2D.MolDraw2DSVG(draw_size[0], draw_size[1])
205
+ else:
206
+ drawer = rdMolDraw2D.MolDraw2DCairo(draw_size[0], draw_size[1])
207
+
208
+ dopts = drawer.drawOptions()
209
+ _apply_rdkit_global_options(dopts)
210
+ dopts.continuousHighlight = False
211
+ dopts.circleAtoms = False
212
+ for k, v in kwargs.items():
213
+ if hasattr(dopts, k):
214
+ setattr(dopts, k, v)
215
+
216
+ drawer.DrawMolecule(
217
+ ready_mol,
218
+ legend=legend,
219
+ highlightAtoms=list(atom_colors.keys()),
220
+ highlightAtomColors=atom_colors,
221
+ highlightBonds=list(bond_colors.keys()),
222
+ highlightBondColors=bond_colors,
223
+ )
224
+ drawer.FinishDrawing()
225
+
226
+ if use_svg:
227
+ svg_text: str = drawer.GetDrawingText()
228
+ if return_image:
229
+ if not svg_support:
230
+ raise ImportError("IPython required for SVG.")
231
+ return SVG(svg_text)
232
+ return svg_text
233
+ else:
234
+ png_data: bytes = drawer.GetDrawingText() # type: ignore
235
+ if return_image:
236
+ return Image.open(io.BytesIO(png_data))
237
+ return png_data
238
+
239
+
240
+ # =============================================================================
241
+ # Grid Drawer
242
+ # =============================================================================
243
+
244
+
245
+ @overload
246
+ def MolGridToDofImage(
247
+ mols: Sequence[Union[Chem.Mol, Chem.RWMol, None]],
248
+ molsPerRow: int = 3, # noqa: N803
249
+ subImgSize: tuple[int, int] = (300, 300), # noqa: N803
250
+ legends: Optional[Sequence[Union[str, None]]] = None,
251
+ use_svg: Literal[True] = True,
252
+ return_image: Literal[True] = True,
253
+ *,
254
+ settings: Optional[DofDrawSettings] = None,
255
+ **kwargs: Any,
256
+ ) -> "SVG": ...
257
+ @overload
258
+ def MolGridToDofImage(
259
+ mols: Sequence[Union[Chem.Mol, Chem.RWMol, None]],
260
+ molsPerRow: int = 3, # noqa: N803
261
+ subImgSize: tuple[int, int] = (300, 300), # noqa: N803
262
+ legends: Optional[Sequence[Union[str, None]]] = None,
263
+ use_svg: Literal[False] = False,
264
+ return_image: Literal[True] = True,
265
+ *,
266
+ settings: Optional[DofDrawSettings] = None,
267
+ **kwargs: Any,
268
+ ) -> Image.Image: ...
269
+ @overload
270
+ def MolGridToDofImage(
271
+ mols: Sequence[Union[Chem.Mol, Chem.RWMol, None]],
272
+ molsPerRow: int = 3, # noqa: N803
273
+ subImgSize: tuple[int, int] = (300, 300), # noqa: N803
274
+ legends: Optional[Sequence[Union[str, None]]] = None,
275
+ use_svg: Literal[True] = True,
276
+ return_image: Literal[False] = False,
277
+ *,
278
+ settings: Optional[DofDrawSettings] = None,
279
+ **kwargs: Any,
280
+ ) -> str: ...
281
+ @overload
282
+ def MolGridToDofImage(
283
+ mols: Sequence[Union[Chem.Mol, Chem.RWMol, None]],
284
+ molsPerRow: int = 3, # noqa: N803
285
+ subImgSize: tuple[int, int] = (300, 300), # noqa: N803
286
+ legends: Optional[Sequence[Union[str, None]]] = None,
287
+ use_svg: Literal[False] = False,
288
+ return_image: Literal[False] = False,
289
+ *,
290
+ settings: Optional[DofDrawSettings] = None,
291
+ **kwargs: Any,
292
+ ) -> bytes: ...
293
+ def MolGridToDofImage( # noqa: N802
294
+ mols: Sequence[Union[Chem.Mol, Chem.RWMol, None]],
295
+ molsPerRow: int = 3, # noqa: N803
296
+ subImgSize: tuple[int, int] = (300, 300), # noqa: N803
297
+ legends: Optional[Sequence[Union[str, None]]] = None,
298
+ use_svg: bool = True,
299
+ return_image: bool = True,
300
+ *,
301
+ settings: Optional[DofDrawSettings] = None,
302
+ **kwargs: Any,
303
+ ) -> Union["SVG", str, Image.Image, bytes]:
304
+ """
305
+ Draw a grid of molecules with DOF effect.
306
+ Compatible with RDKit's Chem.Draw.MolsToGridImage arguments.
307
+ """
308
+ if settings is None:
309
+ settings = dofconfig
310
+
311
+ valid_mols = []
312
+ valid_legends = []
313
+
314
+ all_atom_colors = []
315
+ all_bond_colors = []
316
+ all_highlight_atoms = []
317
+ all_highlight_bonds = []
318
+
319
+ for i, m in enumerate(mols):
320
+ if m is None:
321
+ m = Chem.Mol()
322
+
323
+ try:
324
+ ready_mol, atom_cols, bond_cols = _prepare_mol_data(m, settings)
325
+ except Exception:
326
+ ready_mol = m
327
+ atom_cols, bond_cols = {}, {}
328
+
329
+ valid_mols.append(ready_mol)
330
+
331
+ if legends and i < len(legends) and legends[i]:
332
+ valid_legends.append(str(legends[i]))
333
+ else:
334
+ valid_legends.append("")
335
+
336
+ all_atom_colors.append(atom_cols)
337
+ all_bond_colors.append(bond_cols)
338
+ all_highlight_atoms.append(list(atom_cols.keys()))
339
+ all_highlight_bonds.append(list(bond_cols.keys()))
340
+
341
+ n_mols = len(valid_mols)
342
+ n_rows = math.ceil(n_mols / molsPerRow)
343
+ full_width = subImgSize[0] * molsPerRow
344
+ full_height = subImgSize[1] * n_rows
345
+
346
+ if use_svg:
347
+ drawer = rdMolDraw2D.MolDraw2DSVG(
348
+ full_width, full_height, subImgSize[0], subImgSize[1]
349
+ )
350
+ else:
351
+ drawer = rdMolDraw2D.MolDraw2DCairo(
352
+ full_width, full_height, subImgSize[0], subImgSize[1]
353
+ )
354
+
355
+ dopts = drawer.drawOptions()
356
+ _apply_rdkit_global_options(dopts)
357
+ dopts.continuousHighlight = False
358
+ dopts.circleAtoms = False
359
+
360
+ for k, v in kwargs.items():
361
+ if hasattr(dopts, k):
362
+ setattr(dopts, k, v)
363
+
364
+ drawer.DrawMolecules(
365
+ valid_mols,
366
+ legends=valid_legends,
367
+ highlightAtoms=all_highlight_atoms,
368
+ highlightAtomColors=all_atom_colors,
369
+ highlightBonds=all_highlight_bonds,
370
+ highlightBondColors=all_bond_colors,
371
+ )
372
+ drawer.FinishDrawing()
373
+ if use_svg:
374
+ svg_text: str = drawer.GetDrawingText()
375
+ if return_image:
376
+ if not svg_support:
377
+ raise ImportError("IPython required for SVG.")
378
+ return SVG(svg_text)
379
+ return svg_text
380
+ else:
381
+ png_data: bytes = drawer.GetDrawingText() # type: ignore
382
+ if return_image:
383
+ return Image.open(io.BytesIO(png_data))
384
+ return png_data
rdkit_dof/palettes.py ADDED
@@ -0,0 +1,59 @@
1
+ """
2
+ Author: TMJ
3
+ Date: 2025-12-01 12:45:28
4
+ LastEditors: TMJ
5
+ LastEditTime: 2025-12-01 14:13:11
6
+ Description: 请填写简介
7
+ """
8
+
9
+ AtomColorMap = dict[int, tuple[float, float, float]]
10
+
11
+ # Basic RDKit-style color map
12
+ DEFAULT_STYLE: AtomColorMap = {
13
+ 1: (0.55, 0.55, 0.55), # H (Gray)
14
+ 6: (0.2, 0.2, 0.2), # C (Dark Gray)
15
+ 7: (0.0, 0.0, 1.0), # N (Blue)
16
+ 8: (1.0, 0.0, 0.0), # O (Red)
17
+ 9: (0.2, 0.8, 0.8), # F
18
+ 15: (1.0, 0.5, 0.0), # P
19
+ 16: (0.8, 0.8, 0.0), # S
20
+ 17: (0.0, 0.8, 0.0), # Cl
21
+ 35: (0.5, 0.3, 0.1), # Br
22
+ 53: (0.63, 0.12, 0.94), # I
23
+ }
24
+
25
+ # Nature-style color map: distinct, with blue and red more modern
26
+ NATURE_STYLE: AtomColorMap = {
27
+ 1: (0.9, 0.9, 0.9), # H (Light Gray)
28
+ 6: (0.25, 0.25, 0.25), # C (Dark Gray, not pitch black)
29
+ 7: (0.19, 0.51, 0.74), # N (Nature Blue - slightly lighter)
30
+ 8: (0.89, 0.10, 0.11), # O (Nature Red - less neon)
31
+ 9: (0.50, 0.70, 0.90), # F (Sky Blue)
32
+ 15: (1.0, 0.6, 0.0), # P (Orange)
33
+ 16: (0.9, 0.8, 0.2), # S (Yellow)
34
+ 17: (0.1, 0.7, 0.3), # Cl (Green)
35
+ 35: (0.6, 0.2, 0.2), # Br (Dark Red/Brown)
36
+ }
37
+
38
+ # JACS-style color map: deep, close to print colors, high contrast
39
+ JACS_STYLE: AtomColorMap = {
40
+ 1: (1.0, 1.0, 1.0), # H (White)
41
+ 6: (0.1, 0.1, 0.1), # C (Almost Black)
42
+ 7: (0.05, 0.20, 0.60), # N (Navy Blue)
43
+ 8: (0.75, 0.05, 0.05), # O (Crimson Red)
44
+ 9: (0.4, 0.8, 0.4), # F (Pale Green)
45
+ 15: (0.8, 0.4, 0.0), # P (Dark Orange)
46
+ 16: (0.8, 0.8, 0.0), # S (Standard Yellow)
47
+ 17: (0.0, 0.5, 0.0), # Cl (Dark Green)
48
+ }
49
+
50
+ # dark neon-style color map: high contrast, pop with black background
51
+ DARK_NEON_STYLE: AtomColorMap = {
52
+ 1: (0.8, 0.8, 0.8), # H (Light Gray)
53
+ 6: (0.9, 0.9, 0.9), # C (White/Light Gray to pop on black)
54
+ 7: (0.3, 0.6, 1.0), # N (Bright Blue)
55
+ 8: (1.0, 0.4, 0.4), # O (Bright Red/Pink)
56
+ 9: (0.2, 1.0, 1.0), # F (Cyan)
57
+ 15: (1.0, 0.7, 0.2), # P (Bright Orange)
58
+ 16: (1.0, 1.0, 0.4), # S (Bright Yellow)
59
+ }
@@ -0,0 +1,234 @@
1
+ Metadata-Version: 2.4
2
+ Name: rdkit-dof
3
+ Version: 0.1.0
4
+ Summary: RDKit molecule drawing with Depth of Field (DOF) effects.
5
+ Project-URL: Homepage, https://github.com/gentle1999/rdkit-dof
6
+ Project-URL: Bug Tracker, https://github.com/gentle1999/rdkit-dof/issues
7
+ Project-URL: Repository, https://github.com/gentle1999/rdkit-dof.git
8
+ Author-email: Miao-Jiong Tang <mj_t@zju.edu.cn>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: DOF,cheminformatics,chemistry,drawing,molecule,rdkit,visualization
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
22
+ Classifier: Topic :: Scientific/Engineering :: Visualization
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.9
25
+ Requires-Dist: pydantic-settings>=2.11.0
26
+ Requires-Dist: pydantic>=2.12.5
27
+ Requires-Dist: rdkit>=2023.9.6
28
+ Description-Content-Type: text/markdown
29
+
30
+ # rdkit-dof
31
+
32
+ [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
33
+
34
+ [简体中文](README_zh.md)
35
+
36
+ `rdkit-dof` is a Python toolkit that uses RDKit to generate beautiful 2D images of molecules with a "Depth of Field" (DOF) or "fog" effect. Based on the molecule's 3D conformation, atoms and bonds farther from the viewer are drawn with higher transparency and lower saturation, creating a sense of visual depth in the 2D image.
37
+
38
+ ## Comparison
39
+
40
+ To better showcase the effect of `rdkit-dof`, we have compared it with RDKit's default drawing function.
41
+
42
+ ### Single Molecule Comparison
43
+
44
+ | Default RDKit | rdkit-dof Effect |
45
+ | :---------------------------------------------------------: | :-------------------------------------------------: |
46
+ | ![Paclitaxel Default](assets/comparison_single_default.svg) | ![Paclitaxel DOF](assets/comparison_single_dof.svg) |
47
+
48
+ ### Grid Mode Comparison
49
+
50
+ | Default RDKit | rdkit-dof Effect |
51
+ | :-------------------------------------------------: | :-----------------------------------------: |
52
+ | ![Grid Default](assets/comparison_grid_default.svg) | ![Grid DOF](assets/comparison_grid_dof.svg) |
53
+
54
+ ## Tech Stack
55
+
56
+ - **Core:** Python 3.9+
57
+ - **Cheminformatics:** RDKit
58
+ - **Image Processing:** Pillow
59
+ - **Configuration:** Pydantic
60
+
61
+ ## Installation
62
+
63
+ ```bash
64
+ pip install rdkit-dof
65
+ ```
66
+
67
+ ## Usage
68
+
69
+ Here is a basic example of how to generate an image with a depth-of-field effect for a molecule.
70
+
71
+ ```python
72
+ from rdkit import Chem
73
+ from rdkit.Chem.rdDistGeom import EmbedMolecule
74
+ from rdkit.Chem.rdForceFieldHelpers import MMFFOptimizeMolecule
75
+ from rdkit_dof import MolToDofImage, dofconfig
76
+
77
+ # 1. Create an RDKit molecule object and generate a 3D conformation
78
+ smiles = "CC1=C2[C@@]([C@]([C@H]([C@@H]3[C@]4([C@H](OC4)C[C@@H]([C@]3(C(=O)[C@@H]2OC(=O)C)C)O)OC(=O)C)OC(=O)c5ccccc5)(C[C@@H]1OC(=O)[C@H](O)[C@@H](NC(=O)c6ccccc6)c7ccccc7)C)C"
79
+ mol = Chem.MolFromSmiles(smiles)
80
+ mol = Chem.AddHs(mol)
81
+ EmbedMolecule(mol, randomSeed=42)
82
+ MMFFOptimizeMolecule(mol)
83
+
84
+ # 2. (Optional) Switch to a preset theme
85
+ dofconfig.use_style("default")
86
+
87
+ # 3. Call the core function to generate the image (returns SVG text)
88
+ svg_data = MolToDofImage(
89
+ mol,
90
+ size=(1000, 800),
91
+ legend="Paclitaxel (Taxol)",
92
+ use_svg=True,
93
+ return_image=False, # Set to False to get raw data (str or bytes)
94
+ )
95
+
96
+ # 4. Save to a file
97
+ with open("paclitaxel.svg", "w") as f:
98
+ f.write(svg_data)
99
+
100
+ print("Image saved to paclitaxel.svg")
101
+
102
+ # 5. (Optional) Display the image (in a Jupyter Notebook)
103
+ svg_img = MolToDofImage(
104
+ mol,
105
+ size=(1000, 800),
106
+ legend="Paclitaxel (Taxol)",
107
+ use_svg=True,
108
+ return_image=True, # Set to True to get an IPython/Pillow image object
109
+ )
110
+ svg_img # Display the image
111
+ ```
112
+
113
+ ## API
114
+
115
+ This toolkit provides two core drawing functions:
116
+
117
+ ### `MolToDofImage`
118
+
119
+ Generates a DOF image for a single molecule.
120
+
121
+ ```python
122
+ MolToDofImage(
123
+ mol: Chem.Mol,
124
+ size: Optional[tuple[int, int]] = None,
125
+ legend: str = "",
126
+ use_svg: bool = True,
127
+ return_image: bool = True,
128
+ *,
129
+ settings: Optional[DofDrawSettings] = None,
130
+ **kwargs: Any,
131
+ ) -> Union["SVG", str, Image.Image, bytes]
132
+ ```
133
+
134
+ - **`mol`**: RDKit molecule object, which must contain a 3D conformation.
135
+ - **`size`**: Image dimensions `(width, height)`.
136
+ - **`legend`**: Legend text below the image.
137
+ - **`use_svg`**: `True` returns SVG, `False` returns PNG.
138
+ - **`return_image`**: `True` returns an IPython/Pillow image object, `False` returns raw data (string for SVG, bytes for PNG).
139
+ - **`settings`**: A `DofDrawSettings` instance for local configuration.
140
+ - **`**kwargs`**: Other RDKit `MolDrawOptions` parameters.
141
+
142
+ ### `MolGridToDofImage`
143
+
144
+ Generates a DOF image for a grid of molecules, with parameters similar to RDKit's `MolsToGridImage`.
145
+
146
+ ```python
147
+ MolGridToDofImage(
148
+ mols: Sequence[Union[Chem.Mol, Chem.RWMol, None]],
149
+ molsPerRow: int = 3,
150
+ subImgSize: tuple[int, int] = (300, 300),
151
+ legends: Optional[Sequence[Union[str, None]]] = None,
152
+ use_svg: bool = True,
153
+ return_image: bool = True,
154
+ *,
155
+ settings: Optional[DofDrawSettings] = None,
156
+ **kwargs: Any,
157
+ ) -> Union["SVG", str, Image.Image, bytes]
158
+ ```
159
+
160
+ ## Configuration
161
+
162
+ You can customize the drawing style via a global `dofconfig` object or environment variables (`.env`).
163
+
164
+ ### Global Config Object
165
+
166
+ Modify the properties of the `rdkit_dof.dofconfig` object directly in your code.
167
+
168
+ ```python
169
+ from rdkit_dof import dofconfig
170
+
171
+ # Use a preset style
172
+ dofconfig.use_style("nature") # Options: 'default', 'nature', 'jacs', 'dark'
173
+
174
+ # Customize colors and effects
175
+ dofconfig.fog_color = (0.1, 0.1, 0.1) # Background fog color (RGB, 0-1)
176
+ dofconfig.min_alpha = 0.3 # Minimum alpha for the farthest atoms
177
+ dofconfig.default_size = (500, 500) # Default image size
178
+
179
+ # Override colors for specific atoms (atomic number -> RGB)
180
+ dofconfig.atom_colors[8] = (1.0, 0.2, 0.2) # Set Oxygen to bright red
181
+ ```
182
+
183
+ ### Environment Variables
184
+
185
+ You can also customize the drawing style by setting environment variables. All configuration properties have a corresponding environment variable, named `MOL_DOF_` plus the property name (in upper snake case).
186
+
187
+ For example, to set `fog_color` to dark gray (RGB 0.1, 0.1, 0.1) and `min_alpha` to 0.2, you can add this to your `.env` file:
188
+
189
+ ```env
190
+ # Set fog color to dark gray
191
+ MOL_DOF_FOG_COLOR=[0.1, 0.1, 0.1]
192
+ # Adjust minimum alpha
193
+ MOL_DOF_MIN_ALPHA=0.2
194
+ ```
195
+
196
+ ### Main Configuration Properties
197
+
198
+ | Property | Description | Default (Light Theme) |
199
+ | ---------------- | -------------------------------------------------------- | ----------------------- |
200
+ | `preset_style` | Name of the preset style | `"default"` |
201
+ | `fog_color` | Fog/background color (RGB, 0-1) | `(0.95, 0.95, 0.95)` |
202
+ | `min_alpha` | Minimum alpha for the farthest atoms | `0.4` |
203
+ | `default_size` | Default image size `(width, height)` for `MolToDofImage` | `(800, 800)` |
204
+ | `enable_ipython` | Whether to enable IPython image display | `True` |
205
+ | `atom_colors` | Atom color map `(dict[int, tuple])` | Based on `preset_style` |
206
+
207
+ ## Running the Examples
208
+
209
+ The script `scripts/_generate_comparison_images.py` is a complete example for generating all the images in this document. You can run it to reproduce them:
210
+
211
+ ```bash
212
+ # Generate comparison images
213
+ python scripts/_generate_comparison_images.py
214
+ ```
215
+
216
+ ## Compatibility
217
+
218
+ - **OS:** This project is OS-independent and runs on Linux, macOS, and Windows.
219
+ - **Python Version:** Requires Python 3.9 or higher.
220
+ - **RDKit Version:** Recommended to use `2023.09` or higher.
221
+
222
+ ## Contribution Guide
223
+
224
+ Contributions of any kind are welcome!
225
+
226
+ 1. Fork the repository.
227
+ 2. Create your feature branch (`git checkout -b feature/AmazingFeature`).
228
+ 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`).
229
+ 4. Push to the branch (`git push origin feature/AmazingFeature`).
230
+ 5. Open a Pull Request.
231
+
232
+ ## License
233
+
234
+ This project is distributed under the MIT License. See the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,8 @@
1
+ rdkit_dof/__init__.py,sha256=atx1GCMVjPBpvmqfoC_O3cRYOVDnDm4I5CCBl_tPKdU,221
2
+ rdkit_dof/config.py,sha256=RijM7_lZksXG8xb_ml--RwTPlDnKFf_8WLngvi2j0Hs,3776
3
+ rdkit_dof/core.py,sha256=DtyTfq2jB_ILKhzhn8mkvGqGsANl862oYokUqkhd4t4,11853
4
+ rdkit_dof/palettes.py,sha256=NZ1FtKmQeSRuCNMJklkUJPHuUdr4zUNQ91sZSlql4S0,1992
5
+ rdkit_dof-0.1.0.dist-info/METADATA,sha256=SFdiUZlULIyVduf9aCNxMvlRWbvELaK7uD4SGs7_SbM,8529
6
+ rdkit_dof-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
+ rdkit_dof-0.1.0.dist-info/licenses/LICENSE,sha256=GWLWR3AErH2wmYmfIfUuM_pHsqyWgwI7sbNc5PDppbc,1069
8
+ rdkit_dof-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,18 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 tmj
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
6
+ associated documentation files (the "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
9
+ following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all copies or substantial
12
+ portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
15
+ LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
16
+ EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
18
+ USE OR OTHER DEALINGS IN THE SOFTWARE.