Spaces:
Runtime error
Runtime error
| import plotly.graph_objects as go | |
| import numpy as np | |
| import json | |
| import re | |
| import urllib.parse | |
| import urllib.request | |
| import uuid | |
| import chemiscope | |
| from ase import Atoms | |
| from gradio_client import utils as gradio_client_utils | |
| def _patch_gradio_bool_schema(): | |
| """Work around gradio_client expecting dict schemas by handling bools.""" | |
| original_get_type = getattr(gradio_client_utils, "get_type", None) | |
| if original_get_type is None: | |
| return | |
| def safe_get_type(schema): | |
| if isinstance(schema, bool): | |
| # JSON Schema allows True/False to mean accept-all / accept-nothing. | |
| return "boolean" if schema else {} | |
| return original_get_type(schema) | |
| gradio_client_utils.get_type = safe_get_type | |
| _patch_gradio_bool_schema() | |
| def parse_cube_file(cube_file): | |
| """Parse a cube file and return grid coordinates and values.""" | |
| try: | |
| with open(cube_file, 'r') as f: | |
| lines = f.readlines() | |
| if len(lines) < 6: | |
| raise ValueError("Cube file too short") | |
| # Standard cube format: | |
| # Line 0-1: comments | |
| # Line 2: natoms, origin_x, origin_y, origin_z | |
| # Line 3: nx, voxel_x, 0, 0 | |
| # Line 4: ny, 0, voxel_y, 0 | |
| # Line 5: nz, 0, 0, voxel_z | |
| # Lines 6 to 6+natoms-1: atom data | |
| # Remaining lines: volumetric data | |
| # Parse line 2 (natoms and origin) | |
| parts = lines[2].split() | |
| natoms = abs(int(float(parts[0]))) # abs() handles negative natoms (sometimes used) | |
| origin = np.array([float(parts[1]), float(parts[2]), float(parts[3])]) | |
| # Parse line 3 (nx and voxel spacing) | |
| parts = lines[3].split() | |
| nx = abs(int(float(parts[0]))) | |
| dx = float(parts[1]) | |
| # Parse line 4 (ny and voxel spacing) | |
| parts = lines[4].split() | |
| ny = abs(int(float(parts[0]))) | |
| dy = float(parts[2]) | |
| # Parse line 5 (nz and voxel spacing) | |
| parts = lines[5].split() | |
| nz = abs(int(float(parts[0]))) | |
| dz = float(parts[3]) | |
| # Data starts after atom lines | |
| data_start = 6 + natoms | |
| data = [] | |
| for line in lines[data_start:]: | |
| data.extend([float(x) for x in line.split()]) | |
| if len(data) != nx * ny * nz: | |
| raise ValueError(f"Data size mismatch: expected {nx*ny*nz}, got {len(data)}") | |
| # Reshape data | |
| values = np.array(data).reshape((nx, ny, nz)) | |
| # Create coordinate grids | |
| x = origin[0] + np.arange(nx) * dx | |
| y = origin[1] + np.arange(ny) * dy | |
| z = origin[2] + np.arange(nz) * dz | |
| return x, y, z, values | |
| except Exception as e: | |
| raise ValueError(f"Error parsing cube file: {e}") | |
| import gradio as gr | |
| from rdkit import Chem | |
| from rdkit.Chem import Descriptors, Draw, AllChem | |
| from rdkit.Chem import rdChemReactions | |
| import cirpy | |
| import pubchempy as pcp | |
| from urllib.error import HTTPError, URLError | |
| import os | |
| import tempfile | |
| from pathlib import Path | |
| import matplotlib | |
| matplotlib.use('Agg') # Use non-interactive backend | |
| import matplotlib.pyplot as plt | |
| from mpl_toolkits.mplot3d import Axes3D | |
| from PIL import Image | |
| import io | |
| import base64 | |
| # RDKit API with multiple endpoints | |
| def _mol_from_smiles(smiles: str): | |
| mol = Chem.MolFromSmiles(smiles) | |
| if mol is None: | |
| raise gr.Error("Invalid SMILES string.") | |
| return mol | |
| def smiles_to_canonical(smiles: str) -> str: | |
| mol = _mol_from_smiles(smiles) | |
| return Chem.MolToSmiles(mol) | |
| def molecular_weight(smiles: str) -> float: | |
| mol = _mol_from_smiles(smiles) | |
| return float(Descriptors.MolWt(mol)) | |
| def logp(smiles: str) -> float: | |
| mol = _mol_from_smiles(smiles) | |
| return float(Descriptors.MolLogP(mol)) | |
| def tpsa(smiles: str) -> float: | |
| mol = _mol_from_smiles(smiles) | |
| return float(Descriptors.TPSA(mol)) | |
| def mol_image(smiles: str): | |
| mol = _mol_from_smiles(smiles) | |
| return Draw.MolToImage(mol) | |
| def name_to_smiles(name: str) -> str: | |
| """Convert chemical name to SMILES using Chemical Identifier Resolver (CIR)""" | |
| try: | |
| smiles = cirpy.resolve(name, 'smiles') | |
| if smiles is None: | |
| raise gr.Error(f"Could not find SMILES for chemical name: {name}") | |
| return smiles | |
| except (HTTPError, URLError) as e: | |
| raise gr.Error(f"Unable to connect to chemical database service. Please try again later. Error: {str(e)}") | |
| except Exception as e: | |
| raise gr.Error(f"Error converting name to SMILES: {str(e)}") | |
| def smiles_to_name(smiles: str) -> str: | |
| """Convert SMILES string to chemical name using Chemical Identifier Resolver (CIR).""" | |
| mol = _mol_from_smiles(smiles) | |
| canonical_smiles = Chem.MolToSmiles(mol) | |
| try: | |
| name = cirpy.resolve(smiles, "name") | |
| if name: | |
| return name | |
| except (HTTPError, URLError): | |
| # Ignore network failures and fall back to other resolvers. | |
| pass | |
| except Exception: | |
| # Ignore unexpected CIR errors and fall back to other resolvers. | |
| pass | |
| try: | |
| # Try PubChem as a secondary resolver in case CIR fails. | |
| compounds = pcp.get_compounds(canonical_smiles, namespace="smiles") | |
| for compound in compounds: | |
| if compound.iupac_name: | |
| return compound.iupac_name | |
| if compound.synonyms: | |
| return compound.synonyms[0] | |
| except Exception: | |
| # Ignore PubChem issues and fall back to canonical SMILES output. | |
| pass | |
| return f"No name available. Canonical SMILES: {canonical_smiles}" | |
| def reaction_smiles_to_svg(reaction_smiles: str) -> str: | |
| """Convert reaction SMILES to SVG image""" | |
| try: | |
| # Parse the reaction SMILES properly | |
| if ">>" not in reaction_smiles: | |
| raise gr.Error("Reaction SMILES must contain '>>' to separate reactants from products") | |
| # Split by '>>' to get reactants and products | |
| parts = reaction_smiles.split(">>") | |
| if len(parts) != 2: | |
| raise gr.Error("Reaction SMILES must have exactly one '>>' separator") | |
| # Handle optional reagents/conditions (e.g., "reactants>reagents>products") | |
| reactant_part = parts[0].strip() | |
| product_part = parts[1].strip() | |
| # Check if there are reagents (format: reactants>reagents>products) | |
| if ">" in reactant_part: | |
| reactant_smiles, reagent_smiles = reactant_part.split(">", 1) | |
| reactant_smiles = reactant_smiles.strip() | |
| reagent_smiles = reagent_smiles.strip() | |
| else: | |
| reactant_smiles = reactant_part | |
| reagent_smiles = "" | |
| product_smiles = product_part | |
| # Create reaction from individual molecules using MolFromSmiles (not SMARTS) | |
| reactant_mols = [] | |
| for smi in reactant_smiles.split('.'): | |
| smi = smi.strip() | |
| if smi: | |
| mol = Chem.MolFromSmiles(smi) | |
| if mol is None: | |
| raise gr.Error(f"Invalid SMILES in reactants: {smi}") | |
| reactant_mols.append(mol) | |
| # Parse reagents/catalysts if present | |
| reagent_mols = [] | |
| if reagent_smiles: | |
| for smi in reagent_smiles.split('.'): | |
| smi = smi.strip() | |
| if smi: | |
| mol = Chem.MolFromSmiles(smi) | |
| if mol is None: | |
| raise gr.Error(f"Invalid SMILES in reagents: {smi}") | |
| reagent_mols.append(mol) | |
| product_mols = [] | |
| for smi in product_smiles.split('.'): | |
| smi = smi.strip() | |
| if smi: | |
| mol = Chem.MolFromSmiles(smi) | |
| if mol is None: | |
| raise gr.Error(f"Invalid SMILES in products: {smi}") | |
| product_mols.append(mol) | |
| # Build the reaction object | |
| reaction = rdChemReactions.ChemicalReaction() | |
| for mol in reactant_mols: | |
| reaction.AddReactantTemplate(mol) | |
| for mol in reagent_mols: | |
| reaction.AddAgentTemplate(mol) | |
| for mol in product_mols: | |
| reaction.AddProductTemplate(mol) | |
| # Draw the reaction as image with proper parameters | |
| image = Draw.ReactionToImage(reaction, subImgSize=(200, 200), useSVG=False, drawOptions=None, returnPNG=False) | |
| # Convert PIL image to base64 for HTML display | |
| import io | |
| import base64 | |
| buffer = io.BytesIO() | |
| image.save(buffer, format='PNG') | |
| img_str = base64.b64encode(buffer.getvalue()).decode() | |
| # Return as HTML img tag | |
| return f'<img src="data:image/png;base64,{img_str}" alt="Reaction" style="max-width:100%; height:auto;">' | |
| # Convert PIL image to base64 for HTML display | |
| import io | |
| import base64 | |
| buffer = io.BytesIO() | |
| image.save(buffer, format='PNG') | |
| img_str = base64.b64encode(buffer.getvalue()).decode() | |
| # Return as HTML img tag | |
| return f'<img src="data:image/png;base64,{img_str}" alt="Reaction" style="max-width:100%; height:auto;">' | |
| except gr.Error: | |
| raise | |
| except Exception as e: | |
| raise gr.Error(f"Error generating reaction image: {str(e)}") | |
| CHEMISCOPE_TEMPLATE_URL = "https://chemiscope.org/chemiscope_standalone.html" | |
| CHEMISCOPE_TEMPLATE_CACHE = Path(tempfile.gettempdir()) / "chemiscope_standalone.html" | |
| CHEMISCOPE_ASSET_DIR = Path("chemiscope_artifacts") | |
| _CHEMISCOPE_TEMPLATE = None | |
| _MAX_CHEMISCOPE_MOLECULES = 12 | |
| def _load_chemiscope_template(): | |
| """Load (and cache) the standalone Chemiscope HTML shell.""" | |
| global _CHEMISCOPE_TEMPLATE | |
| if _CHEMISCOPE_TEMPLATE: | |
| return _CHEMISCOPE_TEMPLATE | |
| if CHEMISCOPE_TEMPLATE_CACHE.exists(): | |
| try: | |
| _CHEMISCOPE_TEMPLATE = CHEMISCOPE_TEMPLATE_CACHE.read_text(encoding="utf-8") | |
| return _CHEMISCOPE_TEMPLATE | |
| except OSError: | |
| # Cache is best-effort; fall back to downloading a fresh copy. | |
| pass | |
| try: | |
| with urllib.request.urlopen(CHEMISCOPE_TEMPLATE_URL, timeout=10) as response: | |
| template = response.read().decode("utf-8") | |
| except Exception as exc: | |
| raise gr.Error( | |
| "Unable to download the Chemiscope viewer. Please try again in a moment." | |
| ) from exc | |
| try: | |
| CHEMISCOPE_TEMPLATE_CACHE.write_text(template, encoding="utf-8") | |
| except OSError: | |
| # The temp directory might be read-only; ignore caching failures. | |
| pass | |
| _CHEMISCOPE_TEMPLATE = template | |
| return template | |
| def _artifact_path(name: str) -> Path: | |
| """Create (if needed) and return a path inside the chemiscope artifacts directory.""" | |
| CHEMISCOPE_ASSET_DIR.mkdir(parents=True, exist_ok=True) | |
| return CHEMISCOPE_ASSET_DIR / name | |
| def _smiles_list_from_block(smiles_block: str): | |
| """Split a block of SMILES lines/CSV text into a unique, validated list.""" | |
| tokens = re.split(r"[,\n;]+", smiles_block or "") | |
| smiles_list = [token.strip() for token in tokens if token.strip()] | |
| if not smiles_list: | |
| raise gr.Error("Provide at least one SMILES string (one per line or comma separated).") | |
| unique_smiles = [] | |
| for token in smiles_list: | |
| canonical = Chem.MolToSmiles(_mol_from_smiles(token)) | |
| if canonical not in unique_smiles: | |
| unique_smiles.append(canonical) | |
| if len(unique_smiles) > _MAX_CHEMISCOPE_MOLECULES: | |
| raise gr.Error( | |
| f"Please limit Chemiscope batches to {_MAX_CHEMISCOPE_MOLECULES} molecules to keep the viewer responsive." | |
| ) | |
| return unique_smiles | |
| def _embed_smiles_in_3d(smiles: str, seed: int): | |
| mole = Chem.AddHs(_mol_from_smiles(smiles)) | |
| params = AllChem.ETKDGv3() | |
| params.randomSeed = seed + 1 | |
| status = AllChem.EmbedMolecule(mole, params) | |
| if status == -1: | |
| params.useRandomCoords = True | |
| status = AllChem.EmbedMolecule(mole, params) | |
| if status == -1: | |
| raise gr.Error(f"Unable to generate a 3D conformer for {smiles}. Try a smaller molecule.") | |
| AllChem.UFFOptimizeMolecule(mole, maxIters=200) | |
| Chem.rdPartialCharges.ComputeGasteigerCharges(mole) | |
| return mole | |
| def _rdkit_to_ase_atoms(mol: Chem.Mol, label: str) -> Atoms: | |
| """Convert an RDKit molecule with coordinates into an ASE Atoms object.""" | |
| conf = mol.GetConformer() | |
| coords = [] | |
| for atom_idx in range(mol.GetNumAtoms()): | |
| pos = conf.GetAtomPosition(atom_idx) | |
| coords.append((float(pos.x), float(pos.y), float(pos.z))) | |
| symbols = [atom.GetSymbol() for atom in mol.GetAtoms()] | |
| ase_atoms = Atoms(symbols=symbols, positions=coords) | |
| ase_atoms.info["name"] = label | |
| return ase_atoms | |
| def _extract_gasteiger_charges(mol: Chem.Mol): | |
| charges = [] | |
| for atom in mol.GetAtoms(): | |
| if atom.HasProp("_GasteigerCharge"): | |
| try: | |
| charges.append(float(atom.GetProp("_GasteigerCharge"))) | |
| except ValueError: | |
| charges.append(0.0) | |
| else: | |
| charges.append(0.0) | |
| return charges | |
| def _infer_space_origin(): | |
| """Best-effort detection of the public Space base URL.""" | |
| for key in ("SPACE_HTTP_URL", "SPACE_URL"): | |
| candidate = os.environ.get(key) | |
| if candidate: | |
| return candidate.rstrip("/") | |
| space_id = os.environ.get("SPACE_ID") | |
| if space_id and "/" in space_id: | |
| owner, space = space_id.split("/", 1) | |
| safe_space = space.replace("_", "-") | |
| return f"https://{owner}-{safe_space}.hf.space" | |
| return "" | |
| def _build_chemiscope_embed(dataset_payload: dict, dataset_path: str | Path) -> str: | |
| """Create HTML content for Chemiscope visualization served locally from the Space.""" | |
| template_html = _load_chemiscope_template() | |
| dataset_json = json.dumps(dataset_payload, ensure_ascii=False, separators=(",", ":")) | |
| combined = template_html + dataset_json | |
| dataset_file = Path(dataset_path) | |
| viewer_name = dataset_file.name.replace(".json.gz", "_viewer.html") | |
| viewer_path = dataset_file.parent / viewer_name | |
| viewer_path.write_text(combined, encoding="utf-8") | |
| space_origin = _infer_space_origin() | |
| if space_origin: | |
| dataset_url = f"{space_origin}/file={dataset_file.as_posix()}" | |
| load_param = urllib.parse.quote(dataset_url, safe=":/?=&%") | |
| iframe_src = f"https://chemiscope.org/?load={load_param}" | |
| link = iframe_src | |
| else: | |
| encoded = base64.b64encode(combined.encode("utf-8")).decode("ascii") | |
| iframe_src = f"data:text/html;base64,{encoded}" | |
| link = iframe_src | |
| return ( | |
| "<div style='width:100%;'>" | |
| "<iframe " | |
| "title='Chemiscope explorer' " | |
| "style='width:100%;height:620px;border:none;border-radius:8px;' " | |
| f"src='{iframe_src}'></iframe>" | |
| "<p style='font-size:0.9em;margin-top:0.5rem;'>" | |
| "Open in a new tab if the viewer looks blank: " | |
| f"<a href='{link}' target='_blank' rel='noopener'>Chemiscope standalone</a>" | |
| "</p>" | |
| "</div>" | |
| ) | |
| def smiles_to_chemiscope_dataset(smiles_block: str): | |
| """Generate a Chemiscope dataset and embed it alongside a downloadable artifact.""" | |
| smiles_list = _smiles_list_from_block(smiles_block) | |
| frames = [] | |
| smiles_labels = [] | |
| atom_counts = [] | |
| mw_values = [] | |
| logp_values = [] | |
| tpsa_values = [] | |
| hbd_values = [] | |
| hba_values = [] | |
| rotatable_values = [] | |
| atomic_charges = [] | |
| for idx, smiles in enumerate(smiles_list): | |
| mol3d = _embed_smiles_in_3d(smiles, idx) | |
| canonical = Chem.MolToSmiles(Chem.RemoveHs(mol3d)) | |
| frames.append(_rdkit_to_ase_atoms(mol3d, canonical)) | |
| smiles_labels.append(canonical) | |
| atom_counts.append(int(mol3d.GetNumAtoms())) | |
| atomic_charges.extend(_extract_gasteiger_charges(mol3d)) | |
| descriptor_mol = Chem.RemoveHs(mol3d) | |
| mw_values.append(float(Descriptors.MolWt(mol3d))) | |
| logp_values.append(float(Descriptors.MolLogP(descriptor_mol))) | |
| tpsa_values.append(float(Descriptors.TPSA(descriptor_mol))) | |
| hbd_values.append(float(Descriptors.NumHDonors(descriptor_mol))) | |
| hba_values.append(float(Descriptors.NumHAcceptors(descriptor_mol))) | |
| rotatable_values.append(float(Descriptors.NumRotatableBonds(descriptor_mol))) | |
| properties = { | |
| "SMILES": {"target": "structure", "values": smiles_labels}, | |
| "Atom count": {"target": "structure", "values": atom_counts, "units": "atoms"}, | |
| "Molecular weight (g/mol)": { | |
| "target": "structure", | |
| "values": mw_values, | |
| "units": "g/mol", | |
| }, | |
| "logP": {"target": "structure", "values": logp_values}, | |
| "TPSA (Ų)": {"target": "structure", "values": tpsa_values, "units": "Ų"}, | |
| "H-bond donors": {"target": "structure", "values": hbd_values}, | |
| "H-bond acceptors": {"target": "structure", "values": hba_values}, | |
| "Rotatable bonds": {"target": "structure", "values": rotatable_values}, | |
| "Gasteiger charge (e)": { | |
| "target": "atom", | |
| "values": atomic_charges, | |
| "units": "e", | |
| }, | |
| } | |
| settings = { | |
| "map": { | |
| "x": {"property": "Molecular weight (g/mol)"}, | |
| "y": {"property": "logP"}, | |
| "color": {"property": "TPSA (Ų)"}, | |
| "size": {"property": "Atom count"}, | |
| } | |
| } | |
| meta = { | |
| "name": "RDKit Chemiscope Explorer", | |
| "description": ( | |
| "Interactive Chemiscope session generated directly inside the RDKit Hugging Face Space." | |
| ), | |
| "references": [ | |
| "https://chemiscope.org/docs/python/index.html", | |
| "https://chemiscope.org/docs/index.html", | |
| ], | |
| } | |
| dataset = chemiscope.create_input( | |
| frames=frames, | |
| properties=properties, | |
| meta=meta, | |
| settings=settings, | |
| ) | |
| dataset_path = _artifact_path(f"dataset_{uuid.uuid4().hex}.json.gz") | |
| chemiscope.write_input( | |
| str(dataset_path), | |
| frames=frames, | |
| properties=properties, | |
| meta=meta, | |
| settings=settings, | |
| ) | |
| viewer_html = _build_chemiscope_embed(dataset, dataset_path) | |
| return viewer_html, str(dataset_path) | |
| def smiles_to_molecular_orbitals(smiles_input: str, name_input: str) -> str: | |
| """Generate HOMO/LUMO isosurfaces using Psikit, when available.""" | |
| smiles = smiles_input.strip() | |
| name = name_input.strip() | |
| if not smiles and not name: | |
| raise gr.Error("Enter a SMILES string or a chemical name to compute orbitals.") | |
| if name: | |
| try: | |
| resolved = cirpy.resolve(name, "smiles") | |
| except Exception as exc: | |
| return f"<p>Could not resolve '{name}' to SMILES: {exc}</p>" | |
| if not resolved: | |
| return f"<p>No SMILES found for '{name}'. Try a different name or supply a SMILES directly.</p>" | |
| smiles = resolved | |
| if not smiles: | |
| raise gr.Error("Unable to determine SMILES for orbital calculation.") | |
| mol = _mol_from_smiles(smiles) | |
| canonical_smiles = Chem.MolToSmiles(mol) | |
| if mol.GetNumAtoms() > 30: | |
| raise gr.Error("Please provide a molecule with 30 atoms or fewer for orbital visualization.") | |
| try: | |
| import pyscf # type: ignore[import] | |
| except ImportError: | |
| return ( | |
| "<p><strong>PySCF is not available.</strong> " | |
| "Install it with <code>pip install pyscf</code> " | |
| "for molecular orbital calculations.</p>" | |
| "<p><strong>Alternative online tools:</strong></p>" | |
| "<ul>" | |
| "<li><a href='https://www.webmo.net/' target='_blank'>WebMO</a> - Web-based molecular modeling</li>" | |
| "<li><a href='https://gaussian.com/' target='_blank'>Gaussian</a> - Quantum chemistry software</li>" | |
| "<li><a href='https://www.chemcraftprog.com/' target='_blank'>ChemCraft</a> - Molecular visualization</li>" | |
| "</ul>" | |
| f"<p>You can copy this SMILES to these tools: <code>{canonical_smiles}</code></p>" | |
| ) | |
| with tempfile.TemporaryDirectory() as tmpdir: | |
| original_cwd = os.getcwd() | |
| os.chdir(tmpdir) | |
| try: | |
| # Generate 3D coordinates with RDKit | |
| mol_3d = Chem.AddHs(mol) | |
| AllChem.EmbedMolecule(mol_3d, randomSeed=42) | |
| AllChem.MMFFOptimizeMolecule(mol_3d) | |
| # Extract coordinates and atomic numbers | |
| coords = [] | |
| atoms = [] | |
| for atom in mol_3d.GetAtoms(): | |
| pos = mol_3d.GetConformer().GetAtomPosition(atom.GetIdx()) | |
| coords.append([pos.x, pos.y, pos.z]) | |
| atoms.append(atom.GetSymbol()) | |
| # Set up PySCF molecule | |
| mol_pyscf = pyscf.gto.Mole() | |
| mol_pyscf.atom = list(zip(atoms, coords)) | |
| mol_pyscf.basis = 'sto-3g' # Small basis set for speed | |
| mol_pyscf.build() | |
| # Run Hartree-Fock calculation | |
| mf = pyscf.scf.RHF(mol_pyscf) | |
| energy = mf.kernel() | |
| if not mf.converged: | |
| return "<p>Hartree-Fock calculation did not converge. Try a smaller molecule or different geometry.</p>" | |
| # Get HOMO and LUMO indices | |
| nocc = mol_pyscf.nelectron // 2 | |
| homo_idx = nocc - 1 | |
| lumo_idx = nocc | |
| # Generate cube files for HOMO and LUMO | |
| from pyscf.tools import cubegen | |
| cube_files = [] | |
| for idx, label in [(homo_idx, 'HOMO'), (lumo_idx, 'LUMO')]: | |
| cube_file = f'{label.lower()}.cube' | |
| cubegen.orbital(mol_pyscf, cube_file, mf.mo_coeff[:, idx]) | |
| cube_files.append((cube_file, label)) | |
| mol_block = Chem.MolToMolBlock(mol_3d) | |
| html_sections: list[str] = [] | |
| if name_input.strip(): | |
| html_sections.append( | |
| f"<p><strong>Resolved '{name_input.strip()}' to SMILES:</strong> {canonical_smiles}</p>" | |
| ) | |
| for cube_file, label in cube_files: | |
| if not Path(cube_file).exists(): | |
| continue | |
| # Read cube file content | |
| cube_data = Path(cube_file).read_text() | |
| # Get molecular structure | |
| mol_block = Chem.MolToMolBlock(mol_3d) | |
| # Escape the data for JavaScript | |
| cube_data_escaped = cube_data.replace('`', '\\`').replace('${', '\\${') | |
| mol_block_escaped = mol_block.replace('`', '\\`').replace('${', '\\${') | |
| # Create standalone HTML file with 3Dmol.js | |
| html_content = f""" | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <title>{label} Orbital - Interactive 3D</title> | |
| <script src="https://3dmol.csb.pitt.edu/build/3Dmol-min.js"></script> | |
| <style> | |
| body {{ | |
| margin: 0; | |
| padding: 20px; | |
| font-family: Arial, sans-serif; | |
| background: #f5f5f5; | |
| }} | |
| .container {{ | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| background: white; | |
| padding: 20px; | |
| border-radius: 10px; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.1); | |
| }} | |
| h1 {{ | |
| color: #333; | |
| margin-top: 0; | |
| }} | |
| #viewer {{ | |
| width: 100%; | |
| height: 600px; | |
| position: relative; | |
| border: 2px solid #ddd; | |
| border-radius: 5px; | |
| }} | |
| .info {{ | |
| margin-top: 20px; | |
| padding: 15px; | |
| background: #e8f4f8; | |
| border-left: 4px solid #2196F3; | |
| border-radius: 4px; | |
| }} | |
| .controls {{ | |
| margin: 20px 0; | |
| padding: 15px; | |
| background: #f9f9f9; | |
| border-radius: 4px; | |
| }} | |
| button {{ | |
| margin: 5px; | |
| padding: 10px 20px; | |
| background: #2196F3; | |
| color: white; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| }} | |
| button:hover {{ | |
| background: #1976D2; | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>{label} Orbital - Interactive 3D Visualization</h1> | |
| <div class="info"> | |
| <strong>Molecule:</strong> {canonical_smiles}<br> | |
| <strong>Blue surface:</strong> Positive phase of molecular orbital<br> | |
| <strong>Red surface:</strong> Negative phase of molecular orbital<br> | |
| <strong>Controls:</strong> Left-click drag to rotate, Right-click drag to pan, Scroll to zoom | |
| </div> | |
| <div class="controls"> | |
| <button onclick="viewer.zoomTo(); viewer.render();">Reset View</button> | |
| <button onclick="viewer.rotate(90, 'y'); viewer.render();">Rotate Y</button> | |
| <button onclick="viewer.rotate(90, 'x'); viewer.render();">Rotate X</button> | |
| <button onclick="viewer.zoom(1.2); viewer.render();">Zoom In</button> | |
| <button onclick="viewer.zoom(0.8); viewer.render();">Zoom Out</button> | |
| </div> | |
| <div id="viewer"></div> | |
| </div> | |
| <script> | |
| var viewer = $3Dmol.createViewer("viewer", {{backgroundColor: 'white'}}); | |
| // Add molecular structure | |
| var molData = `{mol_block_escaped}`; | |
| viewer.addModel(molData, "sdf"); | |
| viewer.setStyle({{}}, {{stick: {{radius: 0.15, color: 'gray'}}, sphere: {{scale: 0.25}}}}); | |
| // Add orbital isosurfaces | |
| var cubeData = `{cube_data_escaped}`; | |
| viewer.addVolumetricData(cubeData, "cube", {{isoval: 0.02, color: "blue", alpha: 0.75}}); | |
| viewer.addVolumetricData(cubeData, "cube", {{isoval: -0.02, color: "red", alpha: 0.75}}); | |
| viewer.zoomTo(); | |
| viewer.render(); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| # Save HTML file to a location accessible by Gradio | |
| html_filename = f"{label.lower()}_orbital_{hash(canonical_smiles)}.html" | |
| # Move back to original directory to save in a persistent location | |
| os.chdir(original_cwd) | |
| output_dir = Path("orbital_outputs") | |
| output_dir.mkdir(exist_ok=True) | |
| html_path = output_dir / html_filename | |
| html_path.write_text(html_content) | |
| os.chdir(tmpdir) | |
| # Create iframe to embed the HTML directly | |
| iframe_html = f""" | |
| <div style='margin: 20px 0; padding: 15px; background: white; border: 2px solid #2196F3; border-radius: 8px;'> | |
| <h3 style='color: #1976D2; margin-top: 0;'>{label} Orbital - Interactive 3D</h3> | |
| <p style='margin: 10px 0; color: #666;'><small> | |
| <strong>Blue:</strong> Positive orbital lobe | <strong>Red:</strong> Negative orbital lobe<br> | |
| <strong>Controls:</strong> Left-click drag to rotate, Scroll to zoom, Right-click drag to pan | |
| </small></p> | |
| <iframe srcdoc="{html_content.replace('"', '"')}" | |
| style="width: 100%; height: 650px; border: 1px solid #ddd; border-radius: 4px;" | |
| sandbox="allow-scripts allow-same-origin"> | |
| </iframe> | |
| </div> | |
| """ | |
| html_sections.append(iframe_html) | |
| if not html_sections: | |
| return "<p>Could not prepare HOMO/LUMO visualizations.</p>" | |
| # Wrap in a container div with proper styling | |
| result_html = "<div style='width: 100%; max-width: 1200px;'>" + "".join(html_sections) + "</div>" | |
| return result_html | |
| except Exception as exc: # pragma: no cover - runtime heavy | |
| return f"<p>Unable to compute molecular orbitals: {exc}</p>" | |
| finally: | |
| os.chdir(original_cwd) | |
| def name_to_3d_molecule(name: str) -> str: | |
| """Convert chemical name to 3D molecule visualization""" | |
| try: | |
| # Convert name to SMILES with better error handling | |
| try: | |
| smiles = cirpy.resolve(name, 'smiles') | |
| if smiles is None: | |
| raise gr.Error(f"Could not find SMILES for chemical name: {name}") | |
| except (HTTPError, URLError) as e: | |
| raise gr.Error(f"Unable to connect to chemical database service. Please try again later or use SMILES directly. Error: {str(e)}") | |
| except Exception as e: | |
| raise gr.Error(f"Error resolving chemical name '{name}': {str(e)}") | |
| # Create molecule from SMILES | |
| mol = Chem.MolFromSmiles(smiles) | |
| if mol is None: | |
| raise gr.Error(f"Could not create molecule from SMILES: {smiles}") | |
| # Add hydrogens for better 3D structure | |
| mol = Chem.AddHs(mol) | |
| # Generate 3D coordinates | |
| success = AllChem.EmbedMolecule(mol, AllChem.ETKDG()) | |
| if success == -1: | |
| raise gr.Error(f"Could not generate 3D coordinates for: {name}") | |
| # Optimize geometry | |
| AllChem.MMFFOptimizeMolecule(mol) | |
| # Convert to SDF format (contains 3D coordinates) | |
| sdf_string = Chem.SDWriter.GetText(mol) | |
| # Create HTML with embedded 3D viewer using 3Dmol.js | |
| html_content = f""" | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <script src="https://3dmol.csb.pitt.edu/build/3Dmol-min.js"></script> | |
| </head> | |
| <body> | |
| <div id="container" style="width: 400px; height: 400px; position: relative;"></div> | |
| <script> | |
| let viewer = $3Dmol.createViewer($("#container")); | |
| let sdf = `{sdf_string}`; | |
| viewer.addModel(sdf, "sdf"); | |
| viewer.setStyle({{'stick': {{}}}}); | |
| viewer.zoomTo(); | |
| viewer.render(); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| return html_content | |
| except gr.Error: | |
| # Re-raise Gradio errors as-is | |
| raise | |
| except Exception as e: | |
| raise gr.Error(f"Error creating 3D molecule: {str(e)}") | |
| def calculate_properties_batch(smiles_list: str) -> str: | |
| """Calculate physicochemical properties for multiple molecules""" | |
| from rdkit.Chem import Lipinski | |
| lines = [line.strip() for line in smiles_list.strip().split('\n') if line.strip()] | |
| if not lines: | |
| return "Please enter at least one SMILES string (one per line)" | |
| results = [] | |
| results.append("SMILES\tMW\tLogP\tTPSA\tHBD\tHBA\tRotBonds\tRings\tAromRings") | |
| for smiles in lines[:50]: # Limit to 50 molecules | |
| try: | |
| mol = Chem.MolFromSmiles(smiles) | |
| if mol is None: | |
| results.append(f"{smiles}\tInvalid SMILES") | |
| continue | |
| mw = Descriptors.MolWt(mol) | |
| logp = Descriptors.MolLogP(mol) | |
| tpsa = Descriptors.TPSA(mol) | |
| hbd = Lipinski.NumHDonors(mol) | |
| hba = Lipinski.NumHAcceptors(mol) | |
| rotbonds = Lipinski.NumRotatableBonds(mol) | |
| rings = Lipinski.RingCount(mol) | |
| arom_rings = Lipinski.NumAromaticRings(mol) | |
| results.append(f"{smiles}\t{mw:.2f}\t{logp:.2f}\t{tpsa:.2f}\t{hbd}\t{hba}\t{rotbonds}\t{rings}\t{arom_rings}") | |
| except Exception as e: | |
| results.append(f"{smiles}\tError: {str(e)}") | |
| return "\n".join(results) | |
| def cluster_molecules(smiles_list: str, n_clusters: int = 5) -> str: | |
| """Cluster molecules based on structural similarity using Morgan fingerprints""" | |
| from rdkit.Chem import AllChem | |
| from sklearn.cluster import KMeans | |
| import pandas as pd | |
| lines = [line.strip() for line in smiles_list.strip().split('\n') if line.strip()] | |
| if not lines: | |
| return "Please enter at least one SMILES string (one per line)" | |
| if len(lines) < 2: | |
| return "Please enter at least 2 SMILES strings for clustering" | |
| # Generate fingerprints | |
| mols = [] | |
| valid_smiles = [] | |
| fps = [] | |
| for smiles in lines[:100]: # Limit to 100 molecules | |
| mol = Chem.MolFromSmiles(smiles) | |
| if mol is not None: | |
| fp = AllChem.GetMorganFingerprintAsBitVect(mol, 2, nBits=1024) | |
| mols.append(mol) | |
| valid_smiles.append(smiles) | |
| fps.append(fp) | |
| if len(fps) < 2: | |
| return "Need at least 2 valid SMILES for clustering" | |
| # Convert fingerprints to numpy array | |
| fp_array = np.array([list(fp) for fp in fps]) | |
| # Perform clustering | |
| n_clusters = min(n_clusters, len(fps)) | |
| kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10) | |
| clusters = kmeans.fit_predict(fp_array) | |
| # Create results | |
| results = [] | |
| results.append(f"Clustered {len(valid_smiles)} molecules into {n_clusters} groups\n") | |
| results.append("Cluster\tSMILES\tMW\tLogP") | |
| for i, (smiles, cluster_id) in enumerate(zip(valid_smiles, clusters)): | |
| mol = mols[i] | |
| mw = Descriptors.MolWt(mol) | |
| logp = Descriptors.MolLogP(mol) | |
| results.append(f"{cluster_id + 1}\t{smiles}\t{mw:.2f}\t{logp:.2f}") | |
| return "\n".join(results) | |
| def analyze_scaffolds(smiles_list: str) -> str: | |
| """Extract and analyze molecular scaffolds (Bemis-Murcko scaffolds)""" | |
| from rdkit.Chem.Scaffolds import MurckoScaffold | |
| from collections import Counter | |
| lines = [line.strip() for line in smiles_list.strip().split('\n') if line.strip()] | |
| if not lines: | |
| return "Please enter at least one SMILES string (one per line)" | |
| scaffolds = [] | |
| mol_to_scaffold = [] | |
| for smiles in lines[:100]: # Limit to 100 molecules | |
| try: | |
| mol = Chem.MolFromSmiles(smiles) | |
| if mol is not None: | |
| scaffold = MurckoScaffold.GetScaffoldForMol(mol) | |
| scaffold_smiles = Chem.MolToSmiles(scaffold) | |
| scaffolds.append(scaffold_smiles) | |
| mol_to_scaffold.append((smiles, scaffold_smiles)) | |
| except: | |
| continue | |
| if not scaffolds: | |
| return "No valid scaffolds could be extracted" | |
| # Count scaffold frequencies | |
| scaffold_counts = Counter(scaffolds) | |
| results = [] | |
| results.append(f"Analyzed {len(mol_to_scaffold)} molecules") | |
| results.append(f"Found {len(scaffold_counts)} unique scaffolds\n") | |
| results.append("=== Most Common Scaffolds ===") | |
| for scaffold, count in scaffold_counts.most_common(10): | |
| results.append(f"\nScaffold: {scaffold}") | |
| results.append(f"Frequency: {count} molecules ({100*count/len(scaffolds):.1f}%)") | |
| # Show examples | |
| examples = [smiles for smiles, scaf in mol_to_scaffold if scaf == scaffold][:3] | |
| results.append("Examples:") | |
| for ex in examples: | |
| results.append(f" - {ex}") | |
| return "\n".join(results) | |
| def interactive_molecule_explorer(input_text: str, input_type: str): | |
| """Interactive molecule explorer - input name or SMILES, get structure and properties""" | |
| try: | |
| from rdkit.Chem import Lipinski, Crippen, Descriptors | |
| if not input_text or not input_text.strip(): | |
| return None, None, "Please enter a molecule name or SMILES", None | |
| # Parse input | |
| if input_type == "Name": | |
| try: | |
| smiles = cirpy.resolve(input_text.strip(), 'smiles') | |
| if smiles is None: | |
| return None, None, f"❌ Could not resolve '{input_text}'. Try a different name or use SMILES.", None | |
| name = input_text.strip() | |
| except Exception as e: | |
| return None, None, f"❌ Error resolving chemical name: {str(e)}", None | |
| else: # SMILES | |
| smiles = input_text.strip() | |
| # Try to get name | |
| try: | |
| name = cirpy.resolve(smiles, 'name') | |
| if name is None: | |
| name = smiles | |
| except: | |
| name = smiles | |
| # Create molecule | |
| mol = Chem.MolFromSmiles(smiles) | |
| if mol is None: | |
| return None, None, f"❌ Invalid SMILES: {smiles}", None | |
| # Generate 2D structure image | |
| try: | |
| mol_2d = Draw.MolToImage(mol, size=(400, 400)) | |
| except Exception as e: | |
| return None, None, f"❌ Error generating 2D image: {str(e)}", None | |
| # Calculate comprehensive properties | |
| properties = { | |
| "Molecular Formula": Chem.rdMolDescriptors.CalcMolFormula(mol), | |
| "Molecular Weight": f"{Descriptors.MolWt(mol):.2f} g/mol", | |
| "LogP (Lipophilicity)": f"{Descriptors.MolLogP(mol):.2f}", | |
| "TPSA (Polar Surface Area)": f"{Descriptors.TPSA(mol):.2f} Ų", | |
| "H-Bond Donors": Lipinski.NumHDonors(mol), | |
| "H-Bond Acceptors": Lipinski.NumHAcceptors(mol), | |
| "Rotatable Bonds": Lipinski.NumRotatableBonds(mol), | |
| "Aromatic Rings": Lipinski.NumAromaticRings(mol), | |
| "Fraction Csp3": f"{Lipinski.FractionCSP3(mol):.2f}", | |
| "Molar Refractivity": f"{Crippen.MolMR(mol):.2f}", | |
| "Heavy Atoms": Lipinski.HeavyAtomCount(mol), | |
| } | |
| # Create properties visualization | |
| fig = go.Figure() | |
| # Create a radar chart for key properties | |
| categories = ['MW/100', 'LogP+5', 'TPSA/20', 'HBD*10', 'HBA*5', 'RotBonds*5'] | |
| values = [ | |
| min(Descriptors.MolWt(mol) / 100, 15), | |
| min(max(Descriptors.MolLogP(mol) + 5, 0), 15), | |
| min(Descriptors.TPSA(mol) / 20, 15), | |
| min(Lipinski.NumHDonors(mol) * 2, 15), | |
| min(Lipinski.NumHAcceptors(mol) * 1.5, 15), | |
| min(Lipinski.NumRotatableBonds(mol) * 2, 15) | |
| ] | |
| fig.add_trace(go.Scatterpolar( | |
| r=values, | |
| theta=categories, | |
| fill='toself', | |
| name='Properties', | |
| line_color='rgb(30, 144, 255)', | |
| fillcolor='rgba(30, 144, 255, 0.3)' | |
| )) | |
| fig.update_layout( | |
| polar=dict( | |
| radialaxis=dict( | |
| visible=True, | |
| range=[0, 15] | |
| ) | |
| ), | |
| showlegend=False, | |
| title=f"Property Profile: {name[:50]}", | |
| height=400, | |
| margin=dict(l=80, r=80, t=100, b=80) | |
| ) | |
| # Create properties text | |
| props_text = f"## **{name}**\n\n" | |
| props_text += f"**SMILES:** `{smiles}`\n\n" | |
| props_text += "### **Molecular Properties:**\n\n" | |
| for key, value in properties.items(): | |
| props_text += f"- **{key}:** {value}\n" | |
| # Check Lipinski's Rule of 5 | |
| lipinski_violations = 0 | |
| lipinski_text = "\n### **Lipinski's Rule of 5 (Drug-Likeness):**\n\n" | |
| mw = Descriptors.MolWt(mol) | |
| logp = Descriptors.MolLogP(mol) | |
| hbd = Lipinski.NumHDonors(mol) | |
| hba = Lipinski.NumHAcceptors(mol) | |
| if mw > 500: | |
| lipinski_violations += 1 | |
| lipinski_text += "❌ Molecular Weight > 500 Da\n" | |
| else: | |
| lipinski_text += "✅ Molecular Weight ≤ 500 Da\n" | |
| if logp > 5: | |
| lipinski_violations += 1 | |
| lipinski_text += "❌ LogP > 5\n" | |
| else: | |
| lipinski_text += "✅ LogP ≤ 5\n" | |
| if hbd > 5: | |
| lipinski_violations += 1 | |
| lipinski_text += "❌ H-Bond Donors > 5\n" | |
| else: | |
| lipinski_text += "✅ H-Bond Donors ≤ 5\n" | |
| if hba > 10: | |
| lipinski_violations += 1 | |
| lipinski_text += "❌ H-Bond Acceptors > 10\n" | |
| else: | |
| lipinski_text += "✅ H-Bond Acceptors ≤ 10\n" | |
| if lipinski_violations <= 1: | |
| lipinski_text += f"\n### ✅ **DRUG-LIKE** (Violations: {lipinski_violations}/4)" | |
| else: | |
| lipinski_text += f"\n### ⚠️ **NOT DRUG-LIKE** (Violations: {lipinski_violations}/4)" | |
| props_text += lipinski_text | |
| return mol_2d, fig, props_text, smiles | |
| except Exception as e: | |
| import traceback | |
| error_msg = f"❌ **Error:** {str(e)}\n\n```\n{traceback.format_exc()}\n```" | |
| return None, None, error_msg, None | |
| def generate_3d_interactive(smiles: str): | |
| """Generate interactive 3D molecule viewer""" | |
| if not smiles or smiles == "None": | |
| return "<p>Please enter a molecule first</p>" | |
| try: | |
| mol = Chem.MolFromSmiles(smiles) | |
| if mol is None: | |
| return "<p>Invalid SMILES</p>" | |
| # Add hydrogens and generate 3D coordinates | |
| mol_3d = Chem.AddHs(mol) | |
| AllChem.EmbedMolecule(mol_3d, randomSeed=42) | |
| AllChem.MMFFOptimizeMolecule(mol_3d) | |
| # Get molecular structure | |
| mol_block = Chem.MolToMolBlock(mol_3d) | |
| # Escape for JavaScript | |
| mol_block_escaped = mol_block.replace('`', '\\`').replace('${', '\\${') | |
| # Create standalone HTML with 3Dmol.js | |
| html_content = f""" | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <script src="https://3dmol.csb.pitt.edu/build/3Dmol-min.js"></script> | |
| <style> | |
| body {{ margin: 0; padding: 0; }} | |
| #viewer {{ width: 100%; height: 500px; }} | |
| </style> | |
| </head> | |
| <body> | |
| <div id="viewer"></div> | |
| <script> | |
| var viewer = $3Dmol.createViewer("viewer", {{backgroundColor: 'white'}}); | |
| var molData = `{mol_block_escaped}`; | |
| viewer.addModel(molData, "sdf"); | |
| viewer.setStyle({{}}, {{stick: {{radius: 0.15}}, sphere: {{scale: 0.3}}}}); | |
| viewer.zoomTo(); | |
| viewer.render(); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| iframe_html = f""" | |
| <iframe srcdoc="{html_content.replace('"', '"')}" | |
| style="width: 100%; height: 520px; border: 2px solid #2196F3; border-radius: 8px;" | |
| sandbox="allow-scripts allow-same-origin"> | |
| </iframe> | |
| """ | |
| return iframe_html | |
| except Exception as e: | |
| return f"<p>Error generating 3D structure: {str(e)}</p>" | |
| # Interactive Molecule Explorer - Main Feature | |
| with gr.Blocks(theme=gr.themes.Soft()) as interactive_explorer: | |
| gr.Markdown("# 🔬 Interactive Molecule Explorer") | |
| gr.Markdown("Enter a molecule name or SMILES to explore its structure and properties") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| input_text = gr.Textbox( | |
| label="Enter Molecule", | |
| placeholder="e.g., aspirin, caffeine, or CCO", | |
| lines=1 | |
| ) | |
| input_type = gr.Radio( | |
| choices=["Name", "SMILES"], | |
| value="Name", | |
| label="Input Type" | |
| ) | |
| analyze_btn = gr.Button("🔍 Analyze Molecule", variant="primary", size="lg") | |
| gr.Markdown("### Quick Examples:") | |
| gr.Examples( | |
| examples=[ | |
| ["aspirin", "Name"], | |
| ["caffeine", "Name"], | |
| ["glucose", "Name"], | |
| ["c1ccccc1", "SMILES"], | |
| ["CCO", "SMILES"], | |
| ], | |
| inputs=[input_text, input_type], | |
| ) | |
| with gr.Column(scale=1): | |
| structure_2d = gr.Image(label="2D Structure", type="pil") | |
| with gr.Row(): | |
| properties_plot = gr.Plot(label="Property Radar Chart") | |
| with gr.Row(): | |
| properties_text = gr.Markdown(label="Detailed Properties") | |
| gr.Markdown("---") | |
| gr.Markdown("## 🧊 Interactive 3D Viewer") | |
| gr.Markdown("Click below to generate the interactive 3D molecular structure") | |
| smiles_state = gr.State(value=None) | |
| generate_3d_btn = gr.Button("🎯 Generate 3D Structure", variant="secondary", size="lg") | |
| viewer_3d = gr.HTML(label="3D Molecular Viewer") | |
| # Connect the analyze button | |
| analyze_btn.click( | |
| fn=interactive_molecule_explorer, | |
| inputs=[input_text, input_type], | |
| outputs=[structure_2d, properties_plot, properties_text, smiles_state] | |
| ) | |
| # Connect the 3D button | |
| generate_3d_btn.click( | |
| fn=generate_3d_interactive, | |
| inputs=[smiles_state], | |
| outputs=[viewer_3d] | |
| ) | |
| smiles_interface = gr.Interface( | |
| fn=smiles_to_canonical, | |
| inputs=gr.Textbox(label="SMILES"), | |
| outputs=gr.Textbox(label="Canonical SMILES"), | |
| api_name="smiles_to_mol", | |
| description="Convert an input SMILES string to its canonical form.", | |
| ) | |
| smiles_to_name_interface = gr.Interface( | |
| fn=smiles_to_name, | |
| inputs=gr.Textbox(label="SMILES", placeholder="e.g., CC(=O)Oc1ccccc1C(=O)O"), | |
| outputs=gr.Textbox(label="Chemical Name"), | |
| api_name="smiles_to_name", | |
| description="Convert a SMILES string to a chemical name.", | |
| ) | |
| orbital_interface = gr.Interface( | |
| fn=smiles_to_molecular_orbitals, | |
| inputs=[ | |
| gr.Textbox(label="SMILES", placeholder="e.g., CC(=O)O"), | |
| gr.Textbox(label="Chemical Name", placeholder="Optional, e.g., benzene"), | |
| ], | |
| outputs=gr.HTML(label="Molecular Orbitals"), | |
| api_name="smiles_to_mo", | |
| description="Generate HOMO/LUMO isosurfaces using Psikit (CPU-intensive). Provide SMILES or a name.", | |
| ) | |
| name_interface = gr.Interface( | |
| fn=name_to_smiles, | |
| inputs=gr.Textbox(label="Chemical Name", placeholder="e.g., aspirin, caffeine, benzene"), | |
| outputs=gr.Textbox(label="SMILES"), | |
| api_name="name_to_smiles", | |
| description="Convert a chemical name to SMILES notation.", | |
| examples=[["aspirin"], ["caffeine"], ["benzene"], ["ethanol"]], | |
| ) | |
| mw_interface = gr.Interface( | |
| fn=molecular_weight, | |
| inputs=gr.Textbox(label="SMILES"), | |
| outputs=gr.Number(label="Molecular Weight (g/mol)"), | |
| api_name="molecular_weight", | |
| description="Compute the molecular weight from a SMILES string.", | |
| ) | |
| logp_interface = gr.Interface( | |
| fn=logp, | |
| inputs=gr.Textbox(label="SMILES"), | |
| outputs=gr.Number(label="logP"), | |
| api_name="logp", | |
| description="Calculate the octanol/water partition coefficient (logP).", | |
| ) | |
| tpsa_interface = gr.Interface( | |
| fn=tpsa, | |
| inputs=gr.Textbox(label="SMILES"), | |
| outputs=gr.Number(label="TPSA"), | |
| api_name="tpsa", | |
| description="Calculate the topological polar surface area (TPSA).", | |
| ) | |
| molecule_3d_interface = gr.Interface( | |
| fn=name_to_3d_molecule, | |
| inputs=gr.Textbox(label="Chemical Name", placeholder="e.g., benzene, aspirin, caffeine"), | |
| outputs=gr.HTML(label="3D Molecule Viewer"), | |
| api_name="name_to_3d_molecule", | |
| description="Convert a chemical name to an interactive 3D molecule visualization.", | |
| examples=[["benzene"], ["aspirin"], ["caffeine"], ["ethanol"]], | |
| ) | |
| chemiscope_interface = gr.Interface( | |
| fn=smiles_to_chemiscope_dataset, | |
| inputs=gr.Textbox( | |
| label="SMILES batch", | |
| lines=6, | |
| placeholder="One SMILES per line or comma separated (max 12 molecules).", | |
| ), | |
| outputs=[ | |
| gr.HTML(label="Chemiscope Viewer"), | |
| gr.File(label="Chemiscope Dataset (.json.gz)"), | |
| ], | |
| api_name="chemiscope_explorer", | |
| description=( | |
| "Generate a Chemiscope dataset using RDKit + ASE + Chemiscope tooling, then explore it " | |
| "directly inside the Space or download the JSON for chemiscope.org." | |
| ), | |
| examples=[["CCO\nc1ccccc1"]], | |
| cache_examples=False, | |
| ) | |
| # Property calculation interface | |
| properties_interface = gr.Interface( | |
| fn=calculate_properties_batch, | |
| inputs=gr.Textbox( | |
| label="SMILES List (one per line)", | |
| placeholder="CCO\nc1ccccc1\nCC(=O)O\nCCN", | |
| lines=10 | |
| ), | |
| outputs=gr.Textbox(label="Properties (Tab-separated)", lines=15), | |
| title="Batch Property Calculator", | |
| description="Calculate physicochemical properties for multiple molecules. Enter one SMILES per line (max 50).", | |
| examples=[["CCO\nc1ccccc1\nCC(=O)O\nCN1C=NC2=C1C(=O)N(C(=O)N2C)C"]], | |
| ) | |
| # Clustering interface | |
| clustering_interface = gr.Interface( | |
| fn=cluster_molecules, | |
| inputs=[ | |
| gr.Textbox( | |
| label="SMILES List (one per line)", | |
| placeholder="CCO\nc1ccccc1\nCC(=O)O\nCCN", | |
| lines=10 | |
| ), | |
| gr.Slider(minimum=2, maximum=10, value=5, step=1, label="Number of Clusters") | |
| ], | |
| outputs=gr.Textbox(label="Clustering Results (Tab-separated)", lines=15), | |
| title="Molecular Clustering", | |
| description="Cluster molecules based on structural similarity using Morgan fingerprints and K-means (max 100 molecules).", | |
| examples=[["CCO\nCCCO\nCCCCO\nc1ccccc1\nc1ccc(O)cc1\nc1ccc(N)cc1\nCC(=O)O\nCCC(=O)O\nCCCC(=O)O", 3]], | |
| ) | |
| # Scaffold analysis interface | |
| scaffold_interface = gr.Interface( | |
| fn=analyze_scaffolds, | |
| inputs=gr.Textbox( | |
| label="SMILES List (one per line)", | |
| placeholder="c1ccc(CCN)cc1\nc1ccc(CCO)cc1\nc1ccc(CCC)cc1", | |
| lines=10 | |
| ), | |
| outputs=gr.Textbox(label="Scaffold Analysis", lines=15), | |
| title="Scaffold Analysis", | |
| description="Extract and analyze Bemis-Murcko scaffolds from molecules (max 100).", | |
| examples=[["c1ccc(CCN)cc1\nc1ccc(CCO)cc1\nc1ccc(CCC)cc1\nCCOc1ccc(CCN)cc1\nCCc1ccc(O)cc1"]], | |
| ) | |
| # Reaction visualization interface | |
| reaction_interface = gr.Interface( | |
| fn=reaction_smiles_to_svg, | |
| inputs=gr.Textbox( | |
| label="Reaction SMILES", | |
| placeholder="CC=O.CC=O>[OH-]>CC(O)CC=O (Aldol condensation)", | |
| info="Format: reactants>reagents>products or reactants>>products" | |
| ), | |
| outputs=gr.HTML(label="Reaction Visualization"), | |
| title="Reaction Visualizer", | |
| description="Visualize chemical reactions from SMILES notation. Use 'reactants>reagents>products' or 'reactants>>products' format.", | |
| api_name="reaction_visualizer", | |
| examples=[ | |
| ["CC=O.CC=O>[OH-]>CC(O)CC=O"], | |
| ["CC(=O)O.CO>>CC(=O)OC.O"], | |
| ["c1ccccc1.ClCl>>c1ccccc1Cl.Cl"] | |
| ], | |
| ) | |
| demo = gr.TabbedInterface( | |
| [ | |
| interactive_explorer, | |
| orbital_interface, | |
| properties_interface, | |
| clustering_interface, | |
| scaffold_interface, | |
| reaction_interface, | |
| name_interface, | |
| molecule_3d_interface, | |
| chemiscope_interface, | |
| smiles_interface, | |
| smiles_to_name_interface, | |
| mw_interface, | |
| logp_interface, | |
| tpsa_interface, | |
| ], | |
| [ | |
| "🔬 Interactive Explorer", | |
| "Molecular Orbitals", | |
| "Property Calculator", | |
| "Molecular Clustering", | |
| "Scaffold Analysis", | |
| "Reaction Visualizer", | |
| "Name to SMILES", | |
| "3D Molecule Viewer", | |
| "Chemiscope Explorer", | |
| "SMILES to Canonical", | |
| "SMILES to Name", | |
| "Molecular Weight", | |
| "LogP", | |
| "TPSA", | |
| ], | |
| title="RDKit API - Interactive Molecular Analysis", | |
| css=".gradio-container {max-width: 1200px; margin: auto;}", | |
| ) | |
| if __name__ == "__main__": | |
| demo.queue().launch(server_name="0.0.0.0", server_port=7860, show_api=False) | |