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 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
- register_ipython_formatters as _register_ipython_formatters,
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.1'
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
- from .context import cnotebook_context
4
- from .render import oemol_to_html, oedisp_to_html, oeimage_to_html
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
- ctx.image_format = "png"
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
- ctx.image_format = "png"
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.__mime__ = _display_display
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
- ctx.image_format = "png"
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.__mime__ = _display_image
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
- pd.DataFrame.__mime__ = display_dataframe
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
- df[col] = pd.Series(arr.deepcopy(), index=df[col].index, dtype=oepd.MoleculeDtype())
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 SeriesResetDepictionsAccessor:
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cnotebook
3
- Version: 1.0.1
3
+ Version: 1.1.0
4
4
  Summary: Chemistry visualization in Jupyter Notebooks with the OpenEye Toolkits
5
5
  Author-email: Scott Arne Johnson <scott.arne.johnson@gmail.com>
6
6
  License-Expression: MIT
@@ -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,,
@@ -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,,