cnotebook 1.0.1__py3-none-any.whl → 1.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.
- cnotebook/__init__.py +3 -5
- cnotebook/ipython_ext.py +2 -198
- cnotebook/marimo_ext.py +182 -11
- cnotebook/pandas_ext.py +14 -2
- cnotebook/render.py +197 -0
- {cnotebook-1.0.1.dist-info → cnotebook-1.1.0.dist-info}/METADATA +1 -1
- cnotebook-1.1.0.dist-info/RECORD +13 -0
- cnotebook-1.0.1.dist-info/RECORD +0 -13
- {cnotebook-1.0.1.dist-info → cnotebook-1.1.0.dist-info}/WHEEL +0 -0
- {cnotebook-1.0.1.dist-info → cnotebook-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {cnotebook-1.0.1.dist-info → cnotebook-1.1.0.dist-info}/top_level.txt +0 -0
cnotebook/__init__.py
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from .pandas_ext import render_dataframe
|
|
3
3
|
from .context import cnotebook_context
|
|
4
|
-
from .ipython_ext import
|
|
5
|
-
|
|
6
|
-
render_molecule_grid
|
|
7
|
-
)
|
|
4
|
+
from .ipython_ext import register_ipython_formatters as _register_ipython_formatters
|
|
5
|
+
from .render import render_molecule_grid
|
|
8
6
|
|
|
9
7
|
# Only import formatter registration from the Pandas module, otherwise have users import functionality from there
|
|
10
8
|
# to avoid confusion
|
|
11
9
|
from cnotebook.pandas_ext import register_pandas_formatters as _register_pandas_formatters
|
|
12
10
|
|
|
13
|
-
__version__ = '1.0
|
|
11
|
+
__version__ = '1.1.0'
|
|
14
12
|
|
|
15
13
|
###########################################
|
|
16
14
|
|
cnotebook/ipython_ext.py
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import Literal
|
|
3
|
-
from math import floor, ceil
|
|
4
2
|
from openeye import oechem, oedepict
|
|
5
|
-
from collections.abc import Iterable, Sequence
|
|
6
3
|
from .render import (
|
|
7
4
|
CNotebookContext,
|
|
8
5
|
pass_cnotebook_context,
|
|
9
6
|
oemol_to_html,
|
|
10
7
|
oedisp_to_html,
|
|
11
|
-
oeimage_to_html
|
|
8
|
+
oeimage_to_html,
|
|
9
|
+
render_molecule_grid # Re-export for backward compatibility
|
|
12
10
|
)
|
|
13
|
-
from .helpers import create_structure_highlighter
|
|
14
11
|
|
|
15
12
|
# Only register iPython formatters if that is present
|
|
16
13
|
try:
|
|
@@ -23,199 +20,6 @@ except ModuleNotFoundError:
|
|
|
23
20
|
log = logging.getLogger("cnotebook")
|
|
24
21
|
|
|
25
22
|
|
|
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
23
|
########################################################################################################################
|
|
220
24
|
# Register iPython formatters
|
|
221
25
|
########################################################################################################################
|
cnotebook/marimo_ext.py
CHANGED
|
@@ -1,12 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Marimo integration for CNotebook.
|
|
3
|
+
|
|
4
|
+
This module provides MIME handlers for OpenEye objects and patches Marimo's
|
|
5
|
+
internal table implementation to support molecule rendering with callbacks
|
|
6
|
+
(highlighting, alignment, etc.) in Marimo's built-in DataFrame table component.
|
|
7
|
+
"""
|
|
8
|
+
import logging
|
|
1
9
|
import pandas as pd
|
|
2
10
|
from openeye import oechem, oedepict
|
|
3
|
-
|
|
4
|
-
|
|
11
|
+
|
|
12
|
+
# Import oepandas for dtype checking
|
|
13
|
+
try:
|
|
14
|
+
import oepandas as oepd
|
|
15
|
+
oepandas_available = True
|
|
16
|
+
except ImportError:
|
|
17
|
+
oepandas_available = False
|
|
18
|
+
|
|
19
|
+
from .context import cnotebook_context, get_series_context
|
|
20
|
+
from .render import (
|
|
21
|
+
oemol_to_html,
|
|
22
|
+
oedisp_to_html,
|
|
23
|
+
oeimage_to_html,
|
|
24
|
+
oemol_to_disp,
|
|
25
|
+
render_empty_molecule,
|
|
26
|
+
render_invalid_molecule
|
|
27
|
+
)
|
|
5
28
|
from .pandas_ext import render_dataframe
|
|
6
29
|
|
|
30
|
+
log = logging.getLogger("cnotebook")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
########################################################################################################################
|
|
34
|
+
# MIME handlers for individual OpenEye objects
|
|
35
|
+
########################################################################################################################
|
|
36
|
+
|
|
7
37
|
def _display_mol(self: oechem.OEMolBase):
|
|
8
38
|
ctx = cnotebook_context.get().copy()
|
|
9
|
-
|
|
39
|
+
# Allow user's image_format preference (SVG or PNG)
|
|
10
40
|
return "text/html", oemol_to_html(self, ctx=ctx)
|
|
11
41
|
|
|
12
42
|
oechem.OEMolBase._mime_ = _display_mol
|
|
@@ -14,21 +44,162 @@ oechem.OEMolBase._mime_ = _display_mol
|
|
|
14
44
|
|
|
15
45
|
def _display_display(self: oedepict.OE2DMolDisplay):
|
|
16
46
|
ctx = cnotebook_context.get().copy()
|
|
17
|
-
|
|
47
|
+
# Allow user's image_format preference (SVG or PNG)
|
|
18
48
|
return "text/html", oedisp_to_html(self, ctx=ctx)
|
|
19
49
|
|
|
20
|
-
oedepict.OE2DMolDisplay.
|
|
50
|
+
oedepict.OE2DMolDisplay._mime_ = _display_display
|
|
21
51
|
|
|
22
52
|
|
|
23
53
|
def _display_image(self: oedepict.OEImage):
|
|
24
54
|
ctx = cnotebook_context.get().copy()
|
|
25
|
-
|
|
55
|
+
# Allow user's image_format preference (SVG or PNG)
|
|
26
56
|
return "text/html", oeimage_to_html(self, ctx=ctx)
|
|
27
57
|
|
|
28
|
-
oedepict.OEImage.
|
|
58
|
+
oedepict.OEImage._mime_ = _display_image
|
|
29
59
|
|
|
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
60
|
|
|
34
|
-
|
|
61
|
+
########################################################################################################################
|
|
62
|
+
# Formatter factories for mo.ui.table format_mapping
|
|
63
|
+
########################################################################################################################
|
|
64
|
+
|
|
65
|
+
def _create_molecule_formatter(ctx):
|
|
66
|
+
"""
|
|
67
|
+
Create a formatter closure that renders molecules with callbacks applied.
|
|
68
|
+
|
|
69
|
+
:param ctx: CNotebookContext with callbacks (e.g., highlighting)
|
|
70
|
+
:return: Formatter function for use in mo.ui.table format_mapping
|
|
71
|
+
"""
|
|
72
|
+
def formatter(mol):
|
|
73
|
+
if mol is None:
|
|
74
|
+
return ""
|
|
75
|
+
|
|
76
|
+
if not isinstance(mol, oechem.OEMolBase):
|
|
77
|
+
return str(mol)
|
|
78
|
+
|
|
79
|
+
# Handle invalid molecules
|
|
80
|
+
if not mol.IsValid():
|
|
81
|
+
return render_invalid_molecule(ctx=ctx)
|
|
82
|
+
|
|
83
|
+
# Handle empty molecules
|
|
84
|
+
if mol.NumAtoms() == 0:
|
|
85
|
+
return render_empty_molecule(ctx=ctx)
|
|
86
|
+
|
|
87
|
+
# Create display object
|
|
88
|
+
disp = oemol_to_disp(mol, ctx=ctx)
|
|
89
|
+
|
|
90
|
+
# Apply callbacks (highlighting, etc.)
|
|
91
|
+
if ctx.callbacks:
|
|
92
|
+
for callback in ctx.callbacks:
|
|
93
|
+
callback(disp)
|
|
94
|
+
|
|
95
|
+
# Return display object
|
|
96
|
+
return disp
|
|
97
|
+
|
|
98
|
+
return formatter
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _create_display_formatter(ctx):
|
|
102
|
+
"""
|
|
103
|
+
Create a formatter closure that renders OE2DMolDisplay objects.
|
|
104
|
+
|
|
105
|
+
:param ctx: CNotebookContext for rendering options
|
|
106
|
+
:return: Formatter function for use in mo.ui.table format_mapping
|
|
107
|
+
"""
|
|
108
|
+
def formatter(disp):
|
|
109
|
+
if disp is None:
|
|
110
|
+
return ""
|
|
111
|
+
|
|
112
|
+
if not isinstance(disp, oedepict.OE2DMolDisplay):
|
|
113
|
+
return str(disp)
|
|
114
|
+
|
|
115
|
+
if not disp.IsValid():
|
|
116
|
+
return str(disp)
|
|
117
|
+
|
|
118
|
+
# Copy the display to avoid modifying the original
|
|
119
|
+
disp_copy = oedepict.OE2DMolDisplay(disp)
|
|
120
|
+
|
|
121
|
+
# Apply callbacks if any
|
|
122
|
+
if ctx.callbacks:
|
|
123
|
+
for callback in ctx.callbacks:
|
|
124
|
+
callback(disp_copy)
|
|
125
|
+
|
|
126
|
+
return disp_copy
|
|
127
|
+
|
|
128
|
+
return formatter
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
########################################################################################################################
|
|
132
|
+
# Marimo DataFrame formatter registration
|
|
133
|
+
#
|
|
134
|
+
# This registers a custom formatter with Marimo's OPINIONATED_FORMATTERS registry
|
|
135
|
+
# to handle DataFrames containing molecule columns. The formatter:
|
|
136
|
+
# - Detects MoleculeDtype and DisplayDtype columns
|
|
137
|
+
# - Creates format_mapping entries that apply callbacks (highlighting, alignment, etc.)
|
|
138
|
+
# - Returns OE2DMolDisplay objects which Marimo renders via their _mime_() method
|
|
139
|
+
########################################################################################################################
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
import marimo as mo
|
|
143
|
+
# noinspection PyProtectedMember,PyUnusedImports
|
|
144
|
+
from marimo._output.formatting import OPINIONATED_FORMATTERS
|
|
145
|
+
# noinspection PyProtectedMember,PyUnusedImports
|
|
146
|
+
from marimo._plugins.ui._impl.table import table
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# 1. Define the custom formatting logic
|
|
150
|
+
def marimo_pandas_formatter(df: pd.DataFrame):
|
|
151
|
+
"""
|
|
152
|
+
Monkey patch the Marimo DataFrame formatter
|
|
153
|
+
"""
|
|
154
|
+
format_mapping = {}
|
|
155
|
+
|
|
156
|
+
# Check for MoleculeDtype / DisplayDtype (OEPandas specific)
|
|
157
|
+
if oepandas_available:
|
|
158
|
+
for col in df.columns:
|
|
159
|
+
dtype = df[col].dtype
|
|
160
|
+
|
|
161
|
+
if isinstance(dtype, oepd.MoleculeDtype):
|
|
162
|
+
arr = df[col].array
|
|
163
|
+
ctx = get_series_context(arr.metadata).copy()
|
|
164
|
+
format_mapping[col] = _create_molecule_formatter(ctx)
|
|
165
|
+
|
|
166
|
+
elif isinstance(dtype, oepd.DisplayDtype):
|
|
167
|
+
arr = df[col].array
|
|
168
|
+
ctx = get_series_context(arr.metadata).copy()
|
|
169
|
+
format_mapping[col] = _create_display_formatter(ctx)
|
|
170
|
+
|
|
171
|
+
# Return a Marimo table with our custom mapping
|
|
172
|
+
# noinspection PyProtectedMember,PyTypeChecker
|
|
173
|
+
return table(df, selection=None, format_mapping=format_mapping, pagination=True)._mime_()
|
|
174
|
+
|
|
175
|
+
# 2. Inject into the Registry
|
|
176
|
+
def install_marimo_pandas_formatter():
|
|
177
|
+
# Check if we've already installed it to avoid duplicates
|
|
178
|
+
for typ, func in OPINIONATED_FORMATTERS.formatters.items():
|
|
179
|
+
if typ is pd.DataFrame and func.__name__ == "marimo_pandas_formatter":
|
|
180
|
+
return # Already installed
|
|
181
|
+
|
|
182
|
+
OPINIONATED_FORMATTERS.formatters[pd.DataFrame] = marimo_pandas_formatter
|
|
183
|
+
|
|
184
|
+
# Do the installation
|
|
185
|
+
install_marimo_pandas_formatter()
|
|
186
|
+
|
|
187
|
+
except (ImportError, AttributeError) as ex:
|
|
188
|
+
# Marimo not installed or API changed - skip formatter registration
|
|
189
|
+
log.debug(f'Marimo formatter registration skipped: {ex}')
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
########################################################################################################################
|
|
193
|
+
# Fallback DataFrame MIME handler for non-Marimo contexts
|
|
194
|
+
########################################################################################################################
|
|
195
|
+
|
|
196
|
+
def _display_dataframe(self: pd.DataFrame):
|
|
197
|
+
"""
|
|
198
|
+
Fallback MIME hook for Pandas DataFrames in non-Marimo contexts.
|
|
199
|
+
|
|
200
|
+
In Marimo, the internal table patch handles DataFrame display.
|
|
201
|
+
This is used for static exports or other tools that check _mime_.
|
|
202
|
+
"""
|
|
203
|
+
return "text/html", render_dataframe(df=self, formatters=None, col_space=None)
|
|
204
|
+
|
|
205
|
+
pd.DataFrame._mime_ = _display_dataframe
|
cnotebook/pandas_ext.py
CHANGED
|
@@ -126,9 +126,17 @@ def render_dataframe(
|
|
|
126
126
|
# Render columns with MoleculeDtype
|
|
127
127
|
molecule_columns = set()
|
|
128
128
|
|
|
129
|
+
# Capture metadata from ORIGINAL DataFrame BEFORE copying
|
|
130
|
+
# (df.copy() may not preserve array metadata)
|
|
131
|
+
original_metadata_by_col = {}
|
|
132
|
+
|
|
129
133
|
for col in df.columns:
|
|
130
134
|
if isinstance(df.dtypes[col], oepd.MoleculeDtype):
|
|
131
135
|
molecule_columns.add(col)
|
|
136
|
+
# Get metadata from the original array before any copying
|
|
137
|
+
arr = df[col].array
|
|
138
|
+
if hasattr(arr, 'metadata') and arr.metadata:
|
|
139
|
+
original_metadata_by_col[col] = arr.metadata.copy()
|
|
132
140
|
|
|
133
141
|
# We need to copy both the DataFrame and the molecules, because we modify them in-place to render them
|
|
134
142
|
df = df.copy()
|
|
@@ -137,7 +145,11 @@ def render_dataframe(
|
|
|
137
145
|
# Direct assignment to help IDE understand this is a MoleculeArray
|
|
138
146
|
arr = df[col].array
|
|
139
147
|
assert isinstance(arr, oepd.MoleculeArray)
|
|
140
|
-
|
|
148
|
+
# Use preserved metadata from original DataFrame (not the copy which may have lost it)
|
|
149
|
+
original_metadata = original_metadata_by_col.get(col, {})
|
|
150
|
+
new_arr = arr.deepcopy()
|
|
151
|
+
new_arr.metadata.update(original_metadata)
|
|
152
|
+
df[col] = pd.Series(new_arr, index=df[col].index, dtype=oepd.MoleculeDtype())
|
|
141
153
|
|
|
142
154
|
# ---------------------------------------------------
|
|
143
155
|
# Molecule columns
|
|
@@ -522,7 +534,7 @@ class SeriesRecalculateDepictionCoordinatesAccessor:
|
|
|
522
534
|
|
|
523
535
|
|
|
524
536
|
@register_dataframe_accessor("reset_depictions")
|
|
525
|
-
class
|
|
537
|
+
class DataFrameResetDepictionsAccessor:
|
|
526
538
|
def __init__(self, pandas_obj: pd.DataFrame):
|
|
527
539
|
self._obj = pandas_obj
|
|
528
540
|
|
cnotebook/render.py
CHANGED
|
@@ -2,6 +2,9 @@ import logging
|
|
|
2
2
|
import base64
|
|
3
3
|
from .context import CNotebookContext, pass_cnotebook_context
|
|
4
4
|
from openeye import oechem, oedepict
|
|
5
|
+
from typing import Literal
|
|
6
|
+
from math import floor, ceil
|
|
7
|
+
from collections.abc import Iterable, Sequence
|
|
5
8
|
|
|
6
9
|
log = logging.getLogger("cnotebook")
|
|
7
10
|
|
|
@@ -196,3 +199,197 @@ def oeimage_to_html(image: oedepict.OEImage, *, ctx: CNotebookContext) -> str:
|
|
|
196
199
|
image_bytes=image_bytes,
|
|
197
200
|
wrap_svg=ctx.structure_scale != oedepict.OEScale_AutoScale
|
|
198
201
|
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@pass_cnotebook_context
|
|
205
|
+
def render_molecule_grid(
|
|
206
|
+
mols: Sequence[oechem.OEMolBase],
|
|
207
|
+
nrows: int | None = None,
|
|
208
|
+
ncols: int | None = None,
|
|
209
|
+
max_width: int = 1280,
|
|
210
|
+
max_columns: int = 100,
|
|
211
|
+
min_width: int | None = None,
|
|
212
|
+
min_height: int | None = None,
|
|
213
|
+
align: oechem.OEMolBase | Literal["first"] | None = None,
|
|
214
|
+
smarts: str | Iterable[str] | oechem.OESubSearch | Iterable[oechem.OESubSearch] | None = None,
|
|
215
|
+
color: oechem.OEColor = oechem.OEColor(oechem.OELightBlue),
|
|
216
|
+
style: int = oedepict.OEHighlightStyle_Stick,
|
|
217
|
+
scale: float = 1.0,
|
|
218
|
+
*,
|
|
219
|
+
ctx: CNotebookContext
|
|
220
|
+
) -> oedepict.OEImage:
|
|
221
|
+
"""
|
|
222
|
+
Convenience function to render a molecule grid
|
|
223
|
+
:param ctx: Current OpenEye rendering context
|
|
224
|
+
:param mols: Iterable of OpenEye molecules
|
|
225
|
+
:param nrows: Number of rows to display in the grid
|
|
226
|
+
:param ncols: Number of columns to display in the grid
|
|
227
|
+
:param max_width: Maximum width of the image
|
|
228
|
+
:param max_columns: Maximum number of columns (if ncols is being automatically calculated)
|
|
229
|
+
:param min_width: Minimum image width (prevents tiny images)
|
|
230
|
+
:param min_height: Minimum image height (prevents tiny images)
|
|
231
|
+
:param align: Alignment to the first molecule, or to a reference molecule
|
|
232
|
+
:param smarts: SMARTS substructure highlighting
|
|
233
|
+
:param color: SMARTS highlighting color
|
|
234
|
+
:param style: SMARTS highlighting style
|
|
235
|
+
:param scale: Image scale in the 2D grid
|
|
236
|
+
:return: Image of the molecule grid
|
|
237
|
+
"""
|
|
238
|
+
from .helpers import create_structure_highlighter
|
|
239
|
+
|
|
240
|
+
# Re-scale the images
|
|
241
|
+
ctx.display_options.SetScale(ctx.display_options.GetScale() * scale)
|
|
242
|
+
|
|
243
|
+
# ---------------------------------------------------------------
|
|
244
|
+
# Input validation
|
|
245
|
+
# ---------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
# Handle single molecules
|
|
248
|
+
if isinstance(mols, oechem.OEMolBase):
|
|
249
|
+
mols = [mols]
|
|
250
|
+
|
|
251
|
+
# Make a copy of the molecules (so we do not modify them)
|
|
252
|
+
# We can use OEGraphMol here because we don't care about conformers
|
|
253
|
+
# Filter out None values first
|
|
254
|
+
mols = [oechem.OEGraphMol(mol) for mol in mols if mol is not None]
|
|
255
|
+
|
|
256
|
+
if len(mols) == 0:
|
|
257
|
+
log.warning("No molecules or display objects to render into a grid")
|
|
258
|
+
# Return a minimal 1x1 image instead of 0x0 to avoid OpenEye bug
|
|
259
|
+
return oedepict.OEImage(1, 1)
|
|
260
|
+
|
|
261
|
+
# Get the subset of molecules that will actually be displayed
|
|
262
|
+
valid = []
|
|
263
|
+
|
|
264
|
+
for idx, mol in enumerate(mols):
|
|
265
|
+
if mol is not None:
|
|
266
|
+
if isinstance(mol, oechem.OEMolBase):
|
|
267
|
+
if mol.IsValid():
|
|
268
|
+
valid.append(mol)
|
|
269
|
+
else:
|
|
270
|
+
log.warning(f'Molecule at index {idx} is not valid')
|
|
271
|
+
|
|
272
|
+
else:
|
|
273
|
+
log.warning(f'Object at index is not a molecule but type {type(mol).__name__}')
|
|
274
|
+
|
|
275
|
+
if len(valid) == 0:
|
|
276
|
+
log.warning("No valid molecules or display objects to render into a grid")
|
|
277
|
+
# Return a minimal 1x1 image instead of 0x0 to avoid OpenEye bug
|
|
278
|
+
return oedepict.OEImage(1, 1)
|
|
279
|
+
|
|
280
|
+
# ---------------------------------------------------------------
|
|
281
|
+
# For highlighting SMARTS
|
|
282
|
+
# ---------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
highlighers = None
|
|
285
|
+
|
|
286
|
+
if smarts is not None:
|
|
287
|
+
highlighers = []
|
|
288
|
+
# Case: Single pattern
|
|
289
|
+
if isinstance(smarts, (str, oechem.OESubSearch)):
|
|
290
|
+
highlighers.append(
|
|
291
|
+
create_structure_highlighter(
|
|
292
|
+
smarts,
|
|
293
|
+
color=color,
|
|
294
|
+
style=style
|
|
295
|
+
)
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
else:
|
|
299
|
+
for pattern in smarts:
|
|
300
|
+
highlighers.append(
|
|
301
|
+
create_structure_highlighter(
|
|
302
|
+
pattern,
|
|
303
|
+
color=color,
|
|
304
|
+
style=style
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# ---------------------------------------------------------------
|
|
309
|
+
# For substructure alignment
|
|
310
|
+
# ---------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
align_mcss = None
|
|
313
|
+
|
|
314
|
+
if align is not None:
|
|
315
|
+
|
|
316
|
+
# If we are doing an alignment
|
|
317
|
+
if isinstance(align, bool) and align:
|
|
318
|
+
align_ref = mols[0]
|
|
319
|
+
|
|
320
|
+
elif isinstance(align, oechem.OEMolBase):
|
|
321
|
+
align_ref = align
|
|
322
|
+
|
|
323
|
+
else:
|
|
324
|
+
raise TypeError(f'Cannot initialize MCSS alignment reference from type {type(align).__name__}')
|
|
325
|
+
|
|
326
|
+
# Set up the MCSS
|
|
327
|
+
align_mcss = oechem.OEMCSSearch(oechem.OEMCSType_Approximate)
|
|
328
|
+
align_mcss.Init(align_ref, oechem.OEExprOpts_DefaultAtoms, oechem.OEExprOpts_DefaultBonds)
|
|
329
|
+
|
|
330
|
+
# ---------------------------------------------------------------
|
|
331
|
+
# Create the display objects for each molecule
|
|
332
|
+
# ---------------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
displays = []
|
|
335
|
+
max_disp_width = float('-inf')
|
|
336
|
+
max_disp_height = float('-inf')
|
|
337
|
+
|
|
338
|
+
for mol in valid:
|
|
339
|
+
if align_mcss is not None:
|
|
340
|
+
oedepict.OEPrepareAlignedDepiction(mol, align_mcss)
|
|
341
|
+
else:
|
|
342
|
+
oedepict.OEPrepareDepiction(mol)
|
|
343
|
+
|
|
344
|
+
# Create the display object
|
|
345
|
+
disp = ctx.create_molecule_display(mol, min_width=min_width, min_height=min_height)
|
|
346
|
+
|
|
347
|
+
# Highlight SMARTS patterns
|
|
348
|
+
if highlighers is not None:
|
|
349
|
+
for highlight in highlighers:
|
|
350
|
+
highlight(disp)
|
|
351
|
+
|
|
352
|
+
displays.append(disp)
|
|
353
|
+
|
|
354
|
+
if disp.GetWidth() > max_disp_width:
|
|
355
|
+
max_disp_width = disp.GetWidth()
|
|
356
|
+
|
|
357
|
+
if disp.GetHeight() > max_disp_height:
|
|
358
|
+
max_disp_height = disp.GetHeight()
|
|
359
|
+
|
|
360
|
+
# ---------------------------------------------------------------
|
|
361
|
+
# Figure out the geometry of the full image
|
|
362
|
+
# ---------------------------------------------------------------
|
|
363
|
+
# Case: We have one molecule
|
|
364
|
+
if len(displays) == 1:
|
|
365
|
+
ncols = 1
|
|
366
|
+
nrows = 1
|
|
367
|
+
|
|
368
|
+
# Case: We are computing based on max_width
|
|
369
|
+
elif ncols is None and nrows is None:
|
|
370
|
+
|
|
371
|
+
# Number of columns we can fit into max_width
|
|
372
|
+
ncols = min(floor(max_width / max_disp_width), max_columns, len(displays))
|
|
373
|
+
nrows = ceil(len(displays) / ncols)
|
|
374
|
+
|
|
375
|
+
elif nrows is not None:
|
|
376
|
+
ncols = ceil(len(displays) / nrows)
|
|
377
|
+
|
|
378
|
+
elif ncols is not None:
|
|
379
|
+
nrows = ceil(len(displays) / ncols)
|
|
380
|
+
|
|
381
|
+
else:
|
|
382
|
+
raise RuntimeError("Cannot determine number of rows and columns in molecule grid")
|
|
383
|
+
|
|
384
|
+
# Image width and height
|
|
385
|
+
width = max_disp_width * ncols
|
|
386
|
+
height = max_disp_height * nrows
|
|
387
|
+
|
|
388
|
+
image = oedepict.OEImage(width, height)
|
|
389
|
+
grid = oedepict.OEImageGrid(image, nrows, ncols)
|
|
390
|
+
|
|
391
|
+
# Render the molecules
|
|
392
|
+
for disp, cell in zip(displays, grid.GetCells()):
|
|
393
|
+
oedepict.OERenderMolecule(cell, disp)
|
|
394
|
+
|
|
395
|
+
return image
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
cnotebook/__init__.py,sha256=jO2L0sjW-5r1GthZ45f8zyhVdhaGBK2hjiGD1q3way4,2702
|
|
2
|
+
cnotebook/align.py,sha256=u22GEHJc8QzCb9If2mjuapKPOTBEB2iu6jtUfgmm1UQ,14086
|
|
3
|
+
cnotebook/context.py,sha256=MXOIIZ7PkWZ8Wi11L00ldAqsaNCBmJRc76bETelIq9w,17493
|
|
4
|
+
cnotebook/helpers.py,sha256=r5-CIcmomd8vbd6ILGU6uW3sAjWZPcng82cikZ0ZDZ8,2193
|
|
5
|
+
cnotebook/ipython_ext.py,sha256=mu9eMQiYFEQeT8pS_xh-8Qx6N4dyn6_9u0YwlEudQXo,1989
|
|
6
|
+
cnotebook/marimo_ext.py,sha256=SPvej9LsRIFz8ljujMjppkm92BWTR8xgX9SqFmkY6VA,7169
|
|
7
|
+
cnotebook/pandas_ext.py,sha256=enGXKWDHOv6TK-MbA2H_KsP2IrAEViftyDvCKIjkTX8,35411
|
|
8
|
+
cnotebook/render.py,sha256=S7N7hPF61tppSAo7WN0kF89pWVJWKVMiDczy7RkOltA,13025
|
|
9
|
+
cnotebook-1.1.0.dist-info/licenses/LICENSE,sha256=HbIgeZz-pWGC7BEnYFCQ-jfD1m_BfiosF9qjgWw64GU,1080
|
|
10
|
+
cnotebook-1.1.0.dist-info/METADATA,sha256=fuK5_QxxkcqisT5x2FPc-_WRnYPsD_6j81KMN0uW68A,8575
|
|
11
|
+
cnotebook-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
+
cnotebook-1.1.0.dist-info/top_level.txt,sha256=jzkieTjQwdNKfMwnoElvDDtNPkeLMjbvWbsbkSsboo8,10
|
|
13
|
+
cnotebook-1.1.0.dist-info/RECORD,,
|
cnotebook-1.0.1.dist-info/RECORD
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
cnotebook/__init__.py,sha256=cdQtNy_bYo3ezhhLrSfQidCaRRAGVfqeIJA6XCh2wV0,2695
|
|
2
|
-
cnotebook/align.py,sha256=u22GEHJc8QzCb9If2mjuapKPOTBEB2iu6jtUfgmm1UQ,14086
|
|
3
|
-
cnotebook/context.py,sha256=MXOIIZ7PkWZ8Wi11L00ldAqsaNCBmJRc76bETelIq9w,17493
|
|
4
|
-
cnotebook/helpers.py,sha256=r5-CIcmomd8vbd6ILGU6uW3sAjWZPcng82cikZ0ZDZ8,2193
|
|
5
|
-
cnotebook/ipython_ext.py,sha256=T6kggGkT7YtDfW7XL3kM_wIhac9jVBQaeDgSrjlQp1g,8869
|
|
6
|
-
cnotebook/marimo_ext.py,sha256=b7TJU_Wj-UvfeCkvFWKzn28dPR4olbsd4auWG3UpzQ8,1055
|
|
7
|
-
cnotebook/pandas_ext.py,sha256=9ED28VrxOzdCELIpUpHminbUrccsNcqDFRMLNc26wVA,34795
|
|
8
|
-
cnotebook/render.py,sha256=N5W8-QQGD3d149K6E15MkUvnYtIyFeJ5o4Gphf8Bcuo,6141
|
|
9
|
-
cnotebook-1.0.1.dist-info/licenses/LICENSE,sha256=HbIgeZz-pWGC7BEnYFCQ-jfD1m_BfiosF9qjgWw64GU,1080
|
|
10
|
-
cnotebook-1.0.1.dist-info/METADATA,sha256=MvrUjSCF8Up_dJOF1dQ1pwlo_LZv0Ft9MStmFDPXQV4,8575
|
|
11
|
-
cnotebook-1.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
-
cnotebook-1.0.1.dist-info/top_level.txt,sha256=jzkieTjQwdNKfMwnoElvDDtNPkeLMjbvWbsbkSsboo8,10
|
|
13
|
-
cnotebook-1.0.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|