tonigi commited on
Commit
024b09d
·
1 Parent(s): 11b47b0
Files changed (3) hide show
  1. app.py +107 -58
  2. file_helpers.py +2 -0
  3. rotatable_bonds.py +14 -9
app.py CHANGED
@@ -1,6 +1,6 @@
1
  import gradio as gr
2
  from rdkit import Chem
3
- from rdkit.Chem.Scaffolds import MurckoScaffold
4
  from rdkit.Chem import AllChem # Add this import
5
  from rotatable_bonds import process_rotatable
6
  from dimorphite_dl import dimorphite_dl
@@ -38,7 +38,6 @@ compiled_interligand_patterns = {
38
  }
39
 
40
 
41
-
42
  # -----------------------------
43
  # Generic Highlighting Function
44
  # -----------------------------
@@ -50,6 +49,7 @@ def process_by_patterns(smiles: str, patterns: dict, not_found_msg: str):
50
  return [], not_found_msg
51
  return images, f"Found {len(images)} match(es)."
52
 
 
53
  # Modified process_functional_groups: removed SMILES validity check
54
  def functional_groups(smiles: str):
55
  images = highlight_by_patterns(smiles, compiled_patterns)
@@ -57,12 +57,20 @@ def functional_groups(smiles: str):
57
  return [], "No functional groups recognized."
58
  return images, f"Found {len(images)} match(es)."
59
 
 
60
  def interligand_moieties(smiles: str):
61
- return process_by_patterns(smiles, compiled_interligand_patterns, "No interligand moieties recognized or invalid SMILES.")
 
 
 
 
 
62
 
63
  def daylight_smarts_examples(smiles: str):
64
  patterns = load_yaml_smarts()
65
- return process_by_patterns(smiles, patterns, "No SMARTS examples recognized or invalid SMILES.")
 
 
66
 
67
 
68
  # -----------------------------
@@ -76,19 +84,22 @@ def highlight_chiral_centers(smiles: str):
76
  if not chiral_centers:
77
  return None
78
  highlight_atoms = [idx for idx, _ in chiral_centers]
79
-
80
  # Create labels dictionary for chiral centers
81
  atom_labels = {}
82
  for idx, chirality in chiral_centers:
83
  atom_labels[idx] = chirality # Will show R or S (or ?)
84
-
85
  legend = "Chiral Centers: " + ", ".join(
86
  f"{idx} ({ch})" for idx, ch in chiral_centers
87
  )
88
- img = mol_to_svg(mol, IMAGE_SIZE,
89
- highlightAtoms=highlight_atoms,
90
- legend=legend,
91
- atomLabels=atom_labels)
 
 
 
92
  return img
93
 
94
 
@@ -110,20 +121,25 @@ def stereocenters(smiles: str):
110
  potential_stereocenters = Chem.FindPotentialStereo(mol)
111
  if not potential_stereocenters:
112
  return None, "No potential stereo centers found."
113
-
114
  highlight_atoms = []
115
  atom_labels = {}
116
  for sinfo in potential_stereocenters:
117
  highlight_atoms.append(sinfo.centeredOn)
118
  atom_labels[sinfo.centeredOn] = sinfo.type.name
119
-
120
  # Create single image with all centers highlighted
121
- svg = mol_to_svg(mol, IMAGE_SIZE,
122
- highlightAtoms=highlight_atoms,
123
- legend="Potential Stereogenic Centers",
124
- atomLabels=atom_labels)
125
-
126
- return [(svg, "Potential Stereogenic Centers")], f"Found {len(potential_stereocenters)} potential stereogenic center(s)."
 
 
 
 
 
127
 
128
 
129
  # -----------------------------
@@ -145,7 +161,13 @@ def scaffold(smiles: str):
145
  if bond.GetBeginAtomIdx() in match and bond.GetEndAtomIdx() in match:
146
  highlight_bonds.append(bond.GetIdx())
147
  # Modified to output SVG
148
- img = mol_to_svg(mol, IMAGE_SIZE, highlightAtoms=list(match), highlightBonds=highlight_bonds, legend="Murcko Scaffold")
 
 
 
 
 
 
149
  return [(img, "Murcko Scaffold")], "Scaffold highlighted."
150
 
151
 
@@ -156,7 +178,7 @@ def hybridization(smiles: str):
156
  mol = Chem.MolFromSmiles(smiles)
157
  if mol is None:
158
  return [], "Invalid SMILES."
159
-
160
  # Create atom labels dictionary with hybridization states
161
  atom_labels = {}
162
  highlight_atoms = []
@@ -166,16 +188,19 @@ def hybridization(smiles: str):
166
  if hyb != Chem.HybridizationType.UNSPECIFIED:
167
  atom_labels[atom.GetIdx()] = hyb.name
168
  highlight_atoms.append(atom.GetIdx())
169
-
170
  if not highlight_atoms:
171
  return [], "No hybridization states to display."
172
-
173
  # Generate image with hybridization labels
174
- img = mol_to_svg(mol, IMAGE_SIZE,
175
- highlightAtoms=highlight_atoms,
176
- legend="Hybridization States",
177
- atomLabels=atom_labels)
178
-
 
 
 
179
  return [(img, "Hybridization States")], "Hybridization states highlighted."
180
 
181
 
@@ -186,31 +211,36 @@ def gasteiger_charges(smiles: str):
186
  mol = Chem.MolFromSmiles(smiles)
187
  if mol is None:
188
  return [], "Invalid SMILES."
189
-
190
  # Add explicit hydrogens to the molecule
191
  mol = Chem.AddHs(mol)
192
-
193
  # Compute Gasteiger charges using AllChem
194
  AllChem.ComputeGasteigerCharges(mol)
195
-
196
  # Create atom labels dictionary with charges
197
  atom_labels = {}
198
  highlight_atoms = []
199
  for atom in mol.GetAtoms():
200
- charge = atom.GetDoubleProp('_GasteigerCharge')
201
  atom_labels[atom.GetIdx()] = f"{charge:.3f}"
202
  highlight_atoms.append(atom.GetIdx())
203
-
204
  if not highlight_atoms:
205
  return [], "Could not compute Gasteiger charges."
206
-
207
  # Generate image with charge labels, showing hydrogens
208
- img = mol_to_svg(mol, IMAGE_SIZE,
209
- highlightAtoms=highlight_atoms,
210
- legend="Gasteiger Charges (including H)",
211
- atomLabels=atom_labels)
212
-
213
- return [(img, "Gasteiger Charges")], "Gasteiger charges computed and displayed (including hydrogens)."
 
 
 
 
 
214
 
215
 
216
  # -----------------------------
@@ -223,20 +253,26 @@ def protonate_ph(smiles: str, min_ph: float, max_ph: float):
223
  [Chem.MolFromSmiles(smiles)],
224
  min_ph=min_ph,
225
  max_ph=max_ph,
226
- pka_precision=1.0
227
  )
228
-
229
  if not protonated_mols:
230
  return [], "No protonation variants found."
231
-
232
  images = []
233
  for i, mol in enumerate(protonated_mols):
234
  # Generate SVG for each protonated variant
235
- svg = mol_to_svg(mol, IMAGE_SIZE,
236
- legend=f"Protonated variant {i+1} at pH {min_ph}-{max_ph}")
237
- images.append((svg, f"Variant {i+1}"))
238
-
239
- return images, f"Found {len(images)} protonation variant(s) at pH {min_ph}-{max_ph}."
 
 
 
 
 
 
240
  except Exception as e:
241
  print(traceback.format_exc())
242
  return [], f"Error during protonation: {str(e)}"
@@ -246,7 +282,9 @@ def protonate_ph(smiles: str, min_ph: float, max_ph: float):
246
  # Combined Processing Function
247
  # -----------------------------
248
  # Modified process_smiles_mode: add SMILES validity check for Functional Groups
249
- def process_smiles_main(smiles: str, mode: str, min_ph: float = 7.0, max_ph: float = 7.0):
 
 
250
  if Chem.MolFromSmiles(smiles) is None:
251
  return [], "Invalid SMILES."
252
 
@@ -310,7 +348,7 @@ with gr.Blocks() as demo:
310
  "Murcko Scaffold",
311
  "Hybridization", # Add new mode
312
  "Gasteiger Charges", # Add new mode
313
- "Protonation" # Modified mode name
314
  ],
315
  value="Functional Groups",
316
  )
@@ -318,17 +356,19 @@ with gr.Blocks() as demo:
318
  # Add pH controls in accordion
319
  with gr.Accordion("pH Settings", visible=False) as ph_accordion:
320
  with gr.Row():
321
- min_ph = gr.Slider(minimum=0, maximum=14, value=7.0, step=0.5, label="Minimum pH")
322
- max_ph = gr.Slider(minimum=0, maximum=14, value=7.0, step=0.5, label="Maximum pH")
 
 
 
 
323
 
324
  # Update visibility of pH controls based on mode
325
  def update_accordion_visibility(mode):
326
  return gr.update(visible=(mode == "Protonation"))
327
 
328
  mode_dropdown.change(
329
- update_accordion_visibility,
330
- inputs=[mode_dropdown],
331
- outputs=[ph_accordion]
332
  )
333
 
334
  # Update gr.Examples component with new examples
@@ -347,9 +387,18 @@ with gr.Blocks() as demo:
347
  ["C1=CC=CC=C1", "Hybridization"], # Benzene ring showing SP2
348
  ["CCO", "Gasteiger Charges"], # Simple alcohol showing charge distribution
349
  ["CC(=O)O", "Gasteiger Charges"], # Acetic acid showing polar groups
350
- ["O=C(O)C1N2C(=O)C3C(N=CN3C)C2=O", "Interligand Moieties"], # Caffeine-like structure
351
- ["CC(Cl)CC(F)CN", "Potential Stereogenic Centers"], # Multiple potential stereocenters
352
- ["c1ccc2c(c1)cccc2", "DAYLIGHT SMARTS Examples"], # Naphthalene for aromatic patterns
 
 
 
 
 
 
 
 
 
353
  ["CC1=C(C2=C(C=C1)C=CC=C2)CC(=O)O", "Murcko Scaffold"], # Naproxen scaffold
354
  ["CC(=O)O", "Protonation"], # Acetic acid
355
  ["NCc1ccccc1", "Protonation"], # Benzylamine
@@ -370,7 +419,7 @@ with gr.Blocks() as demo:
370
  "Naphthalene",
371
  "Naproxen",
372
  "Acetic acid protonation",
373
- "Benzylamine protonation"
374
  ],
375
  inputs=[smiles_input, mode_dropdown],
376
  label="Examples",
 
1
  import gradio as gr
2
  from rdkit import Chem
3
+ from rdkit.Chem.Scaffolds import MurckoScaffold
4
  from rdkit.Chem import AllChem # Add this import
5
  from rotatable_bonds import process_rotatable
6
  from dimorphite_dl import dimorphite_dl
 
38
  }
39
 
40
 
 
41
  # -----------------------------
42
  # Generic Highlighting Function
43
  # -----------------------------
 
49
  return [], not_found_msg
50
  return images, f"Found {len(images)} match(es)."
51
 
52
+
53
  # Modified process_functional_groups: removed SMILES validity check
54
  def functional_groups(smiles: str):
55
  images = highlight_by_patterns(smiles, compiled_patterns)
 
57
  return [], "No functional groups recognized."
58
  return images, f"Found {len(images)} match(es)."
59
 
60
+
61
  def interligand_moieties(smiles: str):
62
+ return process_by_patterns(
63
+ smiles,
64
+ compiled_interligand_patterns,
65
+ "No interligand moieties recognized or invalid SMILES.",
66
+ )
67
+
68
 
69
  def daylight_smarts_examples(smiles: str):
70
  patterns = load_yaml_smarts()
71
+ return process_by_patterns(
72
+ smiles, patterns, "No SMARTS examples recognized or invalid SMILES."
73
+ )
74
 
75
 
76
  # -----------------------------
 
84
  if not chiral_centers:
85
  return None
86
  highlight_atoms = [idx for idx, _ in chiral_centers]
87
+
88
  # Create labels dictionary for chiral centers
89
  atom_labels = {}
90
  for idx, chirality in chiral_centers:
91
  atom_labels[idx] = chirality # Will show R or S (or ?)
92
+
93
  legend = "Chiral Centers: " + ", ".join(
94
  f"{idx} ({ch})" for idx, ch in chiral_centers
95
  )
96
+ img = mol_to_svg(
97
+ mol,
98
+ IMAGE_SIZE,
99
+ highlightAtoms=highlight_atoms,
100
+ legend=legend,
101
+ atomLabels=atom_labels,
102
+ )
103
  return img
104
 
105
 
 
121
  potential_stereocenters = Chem.FindPotentialStereo(mol)
122
  if not potential_stereocenters:
123
  return None, "No potential stereo centers found."
124
+
125
  highlight_atoms = []
126
  atom_labels = {}
127
  for sinfo in potential_stereocenters:
128
  highlight_atoms.append(sinfo.centeredOn)
129
  atom_labels[sinfo.centeredOn] = sinfo.type.name
130
+
131
  # Create single image with all centers highlighted
132
+ svg = mol_to_svg(
133
+ mol,
134
+ IMAGE_SIZE,
135
+ highlightAtoms=highlight_atoms,
136
+ legend="Potential Stereogenic Centers",
137
+ atomLabels=atom_labels,
138
+ )
139
+
140
+ return [
141
+ (svg, "Potential Stereogenic Centers")
142
+ ], f"Found {len(potential_stereocenters)} potential stereogenic center(s)."
143
 
144
 
145
  # -----------------------------
 
161
  if bond.GetBeginAtomIdx() in match and bond.GetEndAtomIdx() in match:
162
  highlight_bonds.append(bond.GetIdx())
163
  # Modified to output SVG
164
+ img = mol_to_svg(
165
+ mol,
166
+ IMAGE_SIZE,
167
+ highlightAtoms=list(match),
168
+ highlightBonds=highlight_bonds,
169
+ legend="Murcko Scaffold",
170
+ )
171
  return [(img, "Murcko Scaffold")], "Scaffold highlighted."
172
 
173
 
 
178
  mol = Chem.MolFromSmiles(smiles)
179
  if mol is None:
180
  return [], "Invalid SMILES."
181
+
182
  # Create atom labels dictionary with hybridization states
183
  atom_labels = {}
184
  highlight_atoms = []
 
188
  if hyb != Chem.HybridizationType.UNSPECIFIED:
189
  atom_labels[atom.GetIdx()] = hyb.name
190
  highlight_atoms.append(atom.GetIdx())
191
+
192
  if not highlight_atoms:
193
  return [], "No hybridization states to display."
194
+
195
  # Generate image with hybridization labels
196
+ img = mol_to_svg(
197
+ mol,
198
+ IMAGE_SIZE,
199
+ highlightAtoms=highlight_atoms,
200
+ legend="Hybridization States",
201
+ atomLabels=atom_labels,
202
+ )
203
+
204
  return [(img, "Hybridization States")], "Hybridization states highlighted."
205
 
206
 
 
211
  mol = Chem.MolFromSmiles(smiles)
212
  if mol is None:
213
  return [], "Invalid SMILES."
214
+
215
  # Add explicit hydrogens to the molecule
216
  mol = Chem.AddHs(mol)
217
+
218
  # Compute Gasteiger charges using AllChem
219
  AllChem.ComputeGasteigerCharges(mol)
220
+
221
  # Create atom labels dictionary with charges
222
  atom_labels = {}
223
  highlight_atoms = []
224
  for atom in mol.GetAtoms():
225
+ charge = atom.GetDoubleProp("_GasteigerCharge")
226
  atom_labels[atom.GetIdx()] = f"{charge:.3f}"
227
  highlight_atoms.append(atom.GetIdx())
228
+
229
  if not highlight_atoms:
230
  return [], "Could not compute Gasteiger charges."
231
+
232
  # Generate image with charge labels, showing hydrogens
233
+ img = mol_to_svg(
234
+ mol,
235
+ IMAGE_SIZE,
236
+ highlightAtoms=highlight_atoms,
237
+ legend="Gasteiger Charges (including H)",
238
+ atomLabels=atom_labels,
239
+ )
240
+
241
+ return [
242
+ (img, "Gasteiger Charges")
243
+ ], "Gasteiger charges computed and displayed (including hydrogens)."
244
 
245
 
246
  # -----------------------------
 
253
  [Chem.MolFromSmiles(smiles)],
254
  min_ph=min_ph,
255
  max_ph=max_ph,
256
+ pka_precision=1.0,
257
  )
258
+
259
  if not protonated_mols:
260
  return [], "No protonation variants found."
261
+
262
  images = []
263
  for i, mol in enumerate(protonated_mols):
264
  # Generate SVG for each protonated variant
265
+ svg = mol_to_svg(
266
+ mol,
267
+ IMAGE_SIZE,
268
+ legend=f"Protonated variant {i + 1} at pH {min_ph}-{max_ph}",
269
+ )
270
+ images.append((svg, f"Variant {i + 1}"))
271
+
272
+ return (
273
+ images,
274
+ f"Found {len(images)} protonation variant(s) at pH {min_ph}-{max_ph}.",
275
+ )
276
  except Exception as e:
277
  print(traceback.format_exc())
278
  return [], f"Error during protonation: {str(e)}"
 
282
  # Combined Processing Function
283
  # -----------------------------
284
  # Modified process_smiles_mode: add SMILES validity check for Functional Groups
285
+ def process_smiles_main(
286
+ smiles: str, mode: str, min_ph: float = 7.0, max_ph: float = 7.0
287
+ ):
288
  if Chem.MolFromSmiles(smiles) is None:
289
  return [], "Invalid SMILES."
290
 
 
348
  "Murcko Scaffold",
349
  "Hybridization", # Add new mode
350
  "Gasteiger Charges", # Add new mode
351
+ "Protonation", # Modified mode name
352
  ],
353
  value="Functional Groups",
354
  )
 
356
  # Add pH controls in accordion
357
  with gr.Accordion("pH Settings", visible=False) as ph_accordion:
358
  with gr.Row():
359
+ min_ph = gr.Slider(
360
+ minimum=0, maximum=14, value=7.0, step=0.5, label="Minimum pH"
361
+ )
362
+ max_ph = gr.Slider(
363
+ minimum=0, maximum=14, value=7.0, step=0.5, label="Maximum pH"
364
+ )
365
 
366
  # Update visibility of pH controls based on mode
367
  def update_accordion_visibility(mode):
368
  return gr.update(visible=(mode == "Protonation"))
369
 
370
  mode_dropdown.change(
371
+ update_accordion_visibility, inputs=[mode_dropdown], outputs=[ph_accordion]
 
 
372
  )
373
 
374
  # Update gr.Examples component with new examples
 
387
  ["C1=CC=CC=C1", "Hybridization"], # Benzene ring showing SP2
388
  ["CCO", "Gasteiger Charges"], # Simple alcohol showing charge distribution
389
  ["CC(=O)O", "Gasteiger Charges"], # Acetic acid showing polar groups
390
+ [
391
+ "O=C(O)C1N2C(=O)C3C(N=CN3C)C2=O",
392
+ "Interligand Moieties",
393
+ ], # Caffeine-like structure
394
+ [
395
+ "CC(Cl)CC(F)CN",
396
+ "Potential Stereogenic Centers",
397
+ ], # Multiple potential stereocenters
398
+ [
399
+ "c1ccc2c(c1)cccc2",
400
+ "DAYLIGHT SMARTS Examples",
401
+ ], # Naphthalene for aromatic patterns
402
  ["CC1=C(C2=C(C=C1)C=CC=C2)CC(=O)O", "Murcko Scaffold"], # Naproxen scaffold
403
  ["CC(=O)O", "Protonation"], # Acetic acid
404
  ["NCc1ccccc1", "Protonation"], # Benzylamine
 
419
  "Naphthalene",
420
  "Naproxen",
421
  "Acetic acid protonation",
422
+ "Benzylamine protonation",
423
  ],
424
  inputs=[smiles_input, mode_dropdown],
425
  label="Examples",
file_helpers.py CHANGED
@@ -1,6 +1,7 @@
1
  import yaml
2
  from rdkit import Chem
3
 
 
4
  def load_interligand_moieties():
5
  moieties = {}
6
  try:
@@ -19,6 +20,7 @@ def load_interligand_moieties():
19
  print("Error loading SMARTS_InteLigand.txt:", e)
20
  return moieties
21
 
 
22
  def load_yaml_smarts():
23
  """
24
  Load and compile SMARTS from the YAML file.
 
1
  import yaml
2
  from rdkit import Chem
3
 
4
+
5
  def load_interligand_moieties():
6
  moieties = {}
7
  try:
 
20
  print("Error loading SMARTS_InteLigand.txt:", e)
21
  return moieties
22
 
23
+
24
  def load_yaml_smarts():
25
  """
26
  Load and compile SMARTS from the YAML file.
rotatable_bonds.py CHANGED
@@ -10,22 +10,23 @@ from utils import mol_to_svg, highlight_by_patterns, IMAGE_SIZE
10
  # Rotatable bond patterns
11
  rotatable_patterns = {
12
  "DAYLIGHT defn.": Chem.MolFromSmarts("[!$(*#*)&!D1]-!@[!$(*#*)&!D1]"),
13
- "RDKit defn.": Chem.MolFromSmarts('[!$(*#*)&!D1]-&!@[!$(*#*)&!D1]')
14
  }
15
 
 
16
  def get_rotatable_bond_indices(mol):
17
  """
18
  Identifies rotatable bonds in a molecule using local structural analysis.
19
-
20
  A bond is considered rotatable if it:
21
  - Is a single bond
22
  - Is not in a ring
23
  - Neither atom is a hydrogen
24
  - Both atoms have at least 2 neighbors
25
-
26
  Args:
27
  mol: RDKit molecule object
28
-
29
  Returns:
30
  list: Indices of rotatable bonds in the molecule
31
  """
@@ -44,13 +45,14 @@ def get_rotatable_bond_indices(mol):
44
  rot_bond_indices.append(bond.GetIdx())
45
  return rot_bond_indices
46
 
 
47
  def highlight_rotatable_bonds(smiles: str):
48
  """
49
  Creates an SVG visualization of a molecule with rotatable bonds highlighted.
50
-
51
  Args:
52
  smiles: SMILES string representation of the molecule
53
-
54
  Returns:
55
  str: SVG string of the molecule with rotatable bonds highlighted, or
56
  None if no rotatable bonds are found or if SMILES is invalid
@@ -61,17 +63,20 @@ def highlight_rotatable_bonds(smiles: str):
61
  rot_bonds = get_rotatable_bond_indices(mol)
62
  if not rot_bonds:
63
  return None
64
- img = mol_to_svg(mol, IMAGE_SIZE, highlightBonds=rot_bonds, legend="Rotatable Bonds")
 
 
65
  return img
66
 
 
67
  def process_rotatable(smiles: str):
68
  """
69
  Processes a molecule to identify rotatable bonds using multiple methods
70
  and generates visualizations for each method.
71
-
72
  Args:
73
  smiles: SMILES string representation of the molecule
74
-
75
  Returns:
76
  tuple: (list of (image, caption) tuples, status message string)
77
  Images show the molecule with rotatable bonds highlighted using
 
10
  # Rotatable bond patterns
11
  rotatable_patterns = {
12
  "DAYLIGHT defn.": Chem.MolFromSmarts("[!$(*#*)&!D1]-!@[!$(*#*)&!D1]"),
13
+ "RDKit defn.": Chem.MolFromSmarts("[!$(*#*)&!D1]-&!@[!$(*#*)&!D1]"),
14
  }
15
 
16
+
17
  def get_rotatable_bond_indices(mol):
18
  """
19
  Identifies rotatable bonds in a molecule using local structural analysis.
20
+
21
  A bond is considered rotatable if it:
22
  - Is a single bond
23
  - Is not in a ring
24
  - Neither atom is a hydrogen
25
  - Both atoms have at least 2 neighbors
26
+
27
  Args:
28
  mol: RDKit molecule object
29
+
30
  Returns:
31
  list: Indices of rotatable bonds in the molecule
32
  """
 
45
  rot_bond_indices.append(bond.GetIdx())
46
  return rot_bond_indices
47
 
48
+
49
  def highlight_rotatable_bonds(smiles: str):
50
  """
51
  Creates an SVG visualization of a molecule with rotatable bonds highlighted.
52
+
53
  Args:
54
  smiles: SMILES string representation of the molecule
55
+
56
  Returns:
57
  str: SVG string of the molecule with rotatable bonds highlighted, or
58
  None if no rotatable bonds are found or if SMILES is invalid
 
63
  rot_bonds = get_rotatable_bond_indices(mol)
64
  if not rot_bonds:
65
  return None
66
+ img = mol_to_svg(
67
+ mol, IMAGE_SIZE, highlightBonds=rot_bonds, legend="Rotatable Bonds"
68
+ )
69
  return img
70
 
71
+
72
  def process_rotatable(smiles: str):
73
  """
74
  Processes a molecule to identify rotatable bonds using multiple methods
75
  and generates visualizations for each method.
76
+
77
  Args:
78
  smiles: SMILES string representation of the molecule
79
+
80
  Returns:
81
  tuple: (list of (image, caption) tuples, status message string)
82
  Images show the molecule with rotatable bonds highlighted using