cnotebook 1.0.1__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.
- cnotebook/__init__.py +104 -0
- cnotebook/align.py +390 -0
- cnotebook/context.py +491 -0
- cnotebook/helpers.py +69 -0
- cnotebook/ipython_ext.py +255 -0
- cnotebook/marimo_ext.py +34 -0
- cnotebook/pandas_ext.py +900 -0
- cnotebook/render.py +198 -0
- cnotebook-1.0.1.dist-info/METADATA +275 -0
- cnotebook-1.0.1.dist-info/RECORD +13 -0
- cnotebook-1.0.1.dist-info/WHEEL +5 -0
- cnotebook-1.0.1.dist-info/licenses/LICENSE +21 -0
- cnotebook-1.0.1.dist-info/top_level.txt +1 -0
cnotebook/ipython_ext.py
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Literal
|
|
3
|
+
from math import floor, ceil
|
|
4
|
+
from openeye import oechem, oedepict
|
|
5
|
+
from collections.abc import Iterable, Sequence
|
|
6
|
+
from .render import (
|
|
7
|
+
CNotebookContext,
|
|
8
|
+
pass_cnotebook_context,
|
|
9
|
+
oemol_to_html,
|
|
10
|
+
oedisp_to_html,
|
|
11
|
+
oeimage_to_html
|
|
12
|
+
)
|
|
13
|
+
from .helpers import create_structure_highlighter
|
|
14
|
+
|
|
15
|
+
# Only register iPython formatters if that is present
|
|
16
|
+
try:
|
|
17
|
+
# noinspection PyProtectedMember,PyPackageRequirements
|
|
18
|
+
from IPython import get_ipython
|
|
19
|
+
ipython_present = True
|
|
20
|
+
except ModuleNotFoundError:
|
|
21
|
+
ipython_present = False
|
|
22
|
+
|
|
23
|
+
log = logging.getLogger("cnotebook")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pass_cnotebook_context
|
|
27
|
+
def render_molecule_grid(
|
|
28
|
+
mols: Sequence[oechem.OEMolBase],
|
|
29
|
+
nrows: int | None = None,
|
|
30
|
+
ncols: int | None = None,
|
|
31
|
+
max_width: int = 1280,
|
|
32
|
+
max_columns: int = 100,
|
|
33
|
+
min_width: int | None = None,
|
|
34
|
+
min_height: int | None = None,
|
|
35
|
+
align: oechem.OEMolBase | Literal["first"] | None = None,
|
|
36
|
+
smarts: str | Iterable[str] | oechem.OESubSearch | Iterable[oechem.OESubSearch] | None = None,
|
|
37
|
+
color: oechem.OEColor = oechem.OEColor(oechem.OELightBlue),
|
|
38
|
+
style: int = oedepict.OEHighlightStyle_Stick,
|
|
39
|
+
scale: float = 1.0,
|
|
40
|
+
*,
|
|
41
|
+
ctx: CNotebookContext
|
|
42
|
+
) -> oedepict.OEImage:
|
|
43
|
+
"""
|
|
44
|
+
Convenience function to render a molecule grid
|
|
45
|
+
:param ctx: Current OpenEye rendering context
|
|
46
|
+
:param mols: Iterable of OpenEye molecules
|
|
47
|
+
:param nrows: Number of rows to display in the grid
|
|
48
|
+
:param ncols: Number of columns to display in the grid
|
|
49
|
+
:param max_width: Maximum width of the image
|
|
50
|
+
:param max_columns: Maximum number of columns (if ncols is being automatically calculated)
|
|
51
|
+
:param min_width: Minimum image width (prevents tiny images)
|
|
52
|
+
:param min_height: Minimum image height (prevents tiny images)
|
|
53
|
+
:param align: Alignment to the first molecule, or to a reference molecule
|
|
54
|
+
:param smarts: SMARTS substructure highlighting
|
|
55
|
+
:param color: SMARTS highlighting color
|
|
56
|
+
:param style: SMARTS highlighting style
|
|
57
|
+
:param scale: Image scale in the 2D grid
|
|
58
|
+
:return: Image of the molecule grid
|
|
59
|
+
"""
|
|
60
|
+
# Re-scale the images
|
|
61
|
+
ctx.display_options.SetScale(ctx.display_options.GetScale() * scale)
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------
|
|
64
|
+
# Input validation
|
|
65
|
+
# ---------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
# Handle single molecules
|
|
68
|
+
if isinstance(mols, oechem.OEMolBase):
|
|
69
|
+
mols = [mols]
|
|
70
|
+
|
|
71
|
+
# Make a copy of the molecules (so we do not modify them)
|
|
72
|
+
# We can use OEGraphMol here because we don't care about conformers
|
|
73
|
+
# Filter out None values first
|
|
74
|
+
mols = [oechem.OEGraphMol(mol) for mol in mols if mol is not None]
|
|
75
|
+
|
|
76
|
+
if len(mols) == 0:
|
|
77
|
+
log.warning("No molecules or display objects to render into a grid")
|
|
78
|
+
# Return a minimal 1x1 image instead of 0x0 to avoid OpenEye bug
|
|
79
|
+
return oedepict.OEImage(1, 1)
|
|
80
|
+
|
|
81
|
+
# Get the subset of molecules that will actually be displayed
|
|
82
|
+
valid = []
|
|
83
|
+
|
|
84
|
+
for idx, mol in enumerate(mols):
|
|
85
|
+
if mol is not None:
|
|
86
|
+
if isinstance(mol, oechem.OEMolBase):
|
|
87
|
+
if mol.IsValid():
|
|
88
|
+
valid.append(mol)
|
|
89
|
+
else:
|
|
90
|
+
log.warning(f'Molecule at index {idx} is not valid')
|
|
91
|
+
|
|
92
|
+
else:
|
|
93
|
+
log.warning(f'Object at index is not a molecule but type {type(mol).__name__}')
|
|
94
|
+
|
|
95
|
+
if len(valid) == 0:
|
|
96
|
+
log.warning("No valid molecules or display objects to render into a grid")
|
|
97
|
+
# Return a minimal 1x1 image instead of 0x0 to avoid OpenEye bug
|
|
98
|
+
return oedepict.OEImage(1, 1)
|
|
99
|
+
|
|
100
|
+
# ---------------------------------------------------------------
|
|
101
|
+
# For highlighting SMARTS
|
|
102
|
+
# ---------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
highlighers = None
|
|
105
|
+
|
|
106
|
+
if smarts is not None:
|
|
107
|
+
highlighers = []
|
|
108
|
+
# Case: Single pattern
|
|
109
|
+
if isinstance(smarts, (str, oechem.OESubSearch)):
|
|
110
|
+
highlighers.append(
|
|
111
|
+
create_structure_highlighter(
|
|
112
|
+
smarts,
|
|
113
|
+
color=color,
|
|
114
|
+
style=style
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
else:
|
|
119
|
+
for pattern in smarts:
|
|
120
|
+
highlighers.append(
|
|
121
|
+
create_structure_highlighter(
|
|
122
|
+
pattern,
|
|
123
|
+
color=color,
|
|
124
|
+
style=style
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# ---------------------------------------------------------------
|
|
129
|
+
# For substructure alignment
|
|
130
|
+
# ---------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
align_mcss = None
|
|
133
|
+
|
|
134
|
+
if align is not None:
|
|
135
|
+
|
|
136
|
+
# If we are doing an alignment
|
|
137
|
+
if isinstance(align, bool) and align:
|
|
138
|
+
align_ref = mols[0]
|
|
139
|
+
|
|
140
|
+
elif isinstance(align, oechem.OEMolBase):
|
|
141
|
+
align_ref = align
|
|
142
|
+
|
|
143
|
+
else:
|
|
144
|
+
raise TypeError(f'Cannot initialize MCSS alignment reference from type {type(align).__name__}')
|
|
145
|
+
|
|
146
|
+
# Set up the MCSS
|
|
147
|
+
align_mcss = oechem.OEMCSSearch(oechem.OEMCSType_Approximate)
|
|
148
|
+
align_mcss.Init(align_ref, oechem.OEExprOpts_DefaultAtoms, oechem.OEExprOpts_DefaultBonds)
|
|
149
|
+
|
|
150
|
+
# ---------------------------------------------------------------
|
|
151
|
+
# Create the display objects for each molecule
|
|
152
|
+
# ---------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
displays = []
|
|
155
|
+
max_disp_width = float('-inf')
|
|
156
|
+
max_disp_height = float('-inf')
|
|
157
|
+
|
|
158
|
+
for mol in valid:
|
|
159
|
+
if align_mcss is not None:
|
|
160
|
+
oedepict.OEPrepareAlignedDepiction(mol, align_mcss)
|
|
161
|
+
else:
|
|
162
|
+
oedepict.OEPrepareDepiction(mol)
|
|
163
|
+
|
|
164
|
+
# Create the display object
|
|
165
|
+
disp = ctx.create_molecule_display(mol, min_width=min_width, min_height=min_height)
|
|
166
|
+
# disp = oedepict.OE2DMolDisplay(mol, ctx.display_options)
|
|
167
|
+
|
|
168
|
+
# Highlight SMARTS patterns
|
|
169
|
+
if highlighers is not None:
|
|
170
|
+
for highlight in highlighers:
|
|
171
|
+
highlight(disp)
|
|
172
|
+
|
|
173
|
+
displays.append(disp)
|
|
174
|
+
|
|
175
|
+
if disp.GetWidth() > max_disp_width:
|
|
176
|
+
max_disp_width = disp.GetWidth()
|
|
177
|
+
|
|
178
|
+
if disp.GetHeight() > max_disp_height:
|
|
179
|
+
max_disp_height = disp.GetHeight()
|
|
180
|
+
|
|
181
|
+
# ---------------------------------------------------------------
|
|
182
|
+
# Figure out the geometry of the full image
|
|
183
|
+
# ---------------------------------------------------------------
|
|
184
|
+
# Case: We have one molecule
|
|
185
|
+
if len(displays) == 1:
|
|
186
|
+
ncols = 1
|
|
187
|
+
nrows = 1
|
|
188
|
+
|
|
189
|
+
# Case: We are computing based on max_width
|
|
190
|
+
elif ncols is None and nrows is None:
|
|
191
|
+
|
|
192
|
+
# Number of columns we can fit into max_width
|
|
193
|
+
ncols = min(floor(max_width / max_disp_width), max_columns, len(displays))
|
|
194
|
+
nrows = ceil(len(displays) / ncols)
|
|
195
|
+
|
|
196
|
+
elif nrows is not None:
|
|
197
|
+
ncols = ceil(len(displays) / nrows)
|
|
198
|
+
|
|
199
|
+
elif ncols is not None:
|
|
200
|
+
nrows = ceil(len(displays) / ncols)
|
|
201
|
+
|
|
202
|
+
else:
|
|
203
|
+
raise RuntimeError("Cannot determine number of rows and columns in molecule grid")
|
|
204
|
+
|
|
205
|
+
# Image width and height
|
|
206
|
+
width = max_disp_width * ncols
|
|
207
|
+
height = max_disp_height * nrows
|
|
208
|
+
|
|
209
|
+
image = oedepict.OEImage(width, height)
|
|
210
|
+
grid = oedepict.OEImageGrid(image, nrows, ncols)
|
|
211
|
+
|
|
212
|
+
# Render the molecules
|
|
213
|
+
for disp, cell in zip(displays, grid.GetCells()):
|
|
214
|
+
oedepict.OERenderMolecule(cell, disp)
|
|
215
|
+
|
|
216
|
+
return image
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
########################################################################################################################
|
|
220
|
+
# Register iPython formatters
|
|
221
|
+
########################################################################################################################
|
|
222
|
+
|
|
223
|
+
if ipython_present:
|
|
224
|
+
|
|
225
|
+
def register_ipython_formatters():
|
|
226
|
+
"""
|
|
227
|
+
Register formatters for OpenEye types here that can be rendered. Calls to this function are idempotent.
|
|
228
|
+
"""
|
|
229
|
+
ipython_instance = get_ipython()
|
|
230
|
+
|
|
231
|
+
if ipython_instance is not None:
|
|
232
|
+
html_formatter = ipython_instance.display_formatter.formatters['text/html']
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
_ = html_formatter.lookup(oechem.OEMolBase)
|
|
236
|
+
except KeyError:
|
|
237
|
+
html_formatter.for_type(oechem.OEMolBase, oemol_to_html)
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
_ = html_formatter.lookup(oedepict.OE2DMolDisplay)
|
|
241
|
+
except KeyError:
|
|
242
|
+
html_formatter.for_type(oedepict.OE2DMolDisplay, oedisp_to_html)
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
_ = html_formatter.lookup(oedepict.OEImage)
|
|
246
|
+
except KeyError:
|
|
247
|
+
html_formatter.for_type(oedepict.OEImage, oeimage_to_html)
|
|
248
|
+
else:
|
|
249
|
+
log.debug("[cnotebook] iPython installed but not in use - cannot register iPython extension")
|
|
250
|
+
|
|
251
|
+
else:
|
|
252
|
+
|
|
253
|
+
# iPython is not present, so we do not register formatters for OpenEye objects
|
|
254
|
+
def register_ipython_formatters():
|
|
255
|
+
pass
|
cnotebook/marimo_ext.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from openeye import oechem, oedepict
|
|
3
|
+
from .context import cnotebook_context
|
|
4
|
+
from .render import oemol_to_html, oedisp_to_html, oeimage_to_html
|
|
5
|
+
from .pandas_ext import render_dataframe
|
|
6
|
+
|
|
7
|
+
def _display_mol(self: oechem.OEMolBase):
|
|
8
|
+
ctx = cnotebook_context.get().copy()
|
|
9
|
+
ctx.image_format = "png"
|
|
10
|
+
return "text/html", oemol_to_html(self, ctx=ctx)
|
|
11
|
+
|
|
12
|
+
oechem.OEMolBase._mime_ = _display_mol
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _display_display(self: oedepict.OE2DMolDisplay):
|
|
16
|
+
ctx = cnotebook_context.get().copy()
|
|
17
|
+
ctx.image_format = "png"
|
|
18
|
+
return "text/html", oedisp_to_html(self, ctx=ctx)
|
|
19
|
+
|
|
20
|
+
oedepict.OE2DMolDisplay.__mime__ = _display_display
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _display_image(self: oedepict.OEImage):
|
|
24
|
+
ctx = cnotebook_context.get().copy()
|
|
25
|
+
ctx.image_format = "png"
|
|
26
|
+
return "text/html", oeimage_to_html(self, ctx=ctx)
|
|
27
|
+
|
|
28
|
+
oedepict.OEImage.__mime__ = _display_image
|
|
29
|
+
|
|
30
|
+
def display_dataframe(self: pd.DataFrame):
|
|
31
|
+
ctx = cnotebook_context.get().copy()
|
|
32
|
+
return render_dataframe(df=self, formatters=None, col_space=None)
|
|
33
|
+
|
|
34
|
+
pd.DataFrame.__mime__ = display_dataframe
|