from rdkit import Chem from rdkit.Chem import Descriptors, rdMolDescriptors import re from typing import Dict, Optional, List # ======================================================================== # COMPREHENSIVE MOLECULE DATABASE # ======================================================================== # Common molecule names mapped to SMILES COMMON_NAMES = { # Simple molecules "water": "O", "h2o": "O", "hydrogen": "[H][H]", "h2": "[H][H]", "oxygen": "O=O", "o2": "O=O", "nitrogen": "N#N", "n2": "N#N", "ammonia": "N", "nh3": "N", "methane": "C", "ch4": "C", "ethane": "CC", "c2h6": "CC", "propane": "CCC", "c3h8": "CCC", "butane": "CCCC", "c4h10": "CCCC", "ethylene": "C=C", "ethene": "C=C", "c2h4": "C=C", "acetylene": "C#C", "c2h2": "C#C", "benzene": "c1ccccc1", "c6h6": "c1ccccc1", "methanol": "CO", "ch3oh": "CO", "ethanol": "CCO", "c2h5oh": "CCO", "formaldehyde": "C=O", "ch2o": "C=O", "formic acid": "C(=O)O", "hcooh": "C(=O)O", "acetic acid": "CC(=O)O", "ch3cooh": "CC(=O)O", # Inorganic molecules "carbon dioxide": "C(=O)=O", "co2": "C(=O)=O", "carbon monoxide": "[C-]#[O+]", "co": "[C-]#[O+]", "nitric oxide": "[N]=O", "no": "[N]=O", "nitrogen dioxide": "N(=O)[O]", "no2": "N(=O)[O]", "sulfur dioxide": "O=S=O", "so2": "O=S=O", "hydrogen sulfide": "S", "h2s": "S", "hydrochloric acid": "Cl", "hcl": "Cl", "hydrogen peroxide": "OO", "h2o2": "OO", # Metal catalysts (single atoms) "iron": "[Fe]", "fe": "[Fe]", "platinum": "[Pt]", "pt": "[Pt]", "palladium": "[Pd]", "pd": "[Pd]", "nickel": "[Ni]", "ni": "[Ni]", "copper": "[Cu]", "cu": "[Cu]", "gold": "[Au]", "au": "[Au]", "silver": "[Ag]", "ag": "[Ag]", "rhodium": "[Rh]", "rh": "[Rh]", "ruthenium": "[Ru]", "ru": "[Ru]", "cobalt": "[Co]", "co_metal": "[Co]", "titanium": "[Ti]", "ti": "[Ti]", "zinc": "[Zn]", "zn": "[Zn]", "chromium": "[Cr]", "cr": "[Cr]", "manganese": "[Mn]", "mn": "[Mn]", "vanadium": "[V]", "v": "[V]", "molybdenum": "[Mo]", "mo": "[Mo]", "tungsten": "[W]", "w": "[W]", # Metal oxides (simplified representations) "iron oxide": "[Fe]=O", "feo": "[Fe]=O", "nickel oxide": "[Ni]=O", "nio": "[Ni]=O", "copper oxide": "[Cu]=O", "cuo": "[Cu]=O", "zinc oxide": "[Zn]=O", "zno": "[Zn]=O", "titanium dioxide": "O=[Ti]=O", "tio2": "O=[Ti]=O", } # Formula patterns for automatic parsing PERIODIC_TABLE = { 'H': 1, 'He': 2, 'Li': 3, 'Be': 4, 'B': 5, 'C': 6, 'N': 7, 'O': 8, 'F': 9, 'Ne': 10, 'Na': 11, 'Mg': 12, 'Al': 13, 'Si': 14, 'P': 15, 'S': 16, 'Cl': 17, 'Ar': 18, 'K': 19, 'Ca': 20, 'Sc': 21, 'Ti': 22, 'V': 23, 'Cr': 24, 'Mn': 25, 'Fe': 26, 'Co': 27, 'Ni': 28, 'Cu': 29, 'Zn': 30, 'Ga': 31, 'Ge': 32, 'As': 33, 'Se': 34, 'Br': 35, 'Kr': 36, 'Rb': 37, 'Sr': 38, 'Y': 39, 'Zr': 40, 'Nb': 41, 'Mo': 42, 'Tc': 43, 'Ru': 44, 'Rh': 45, 'Pd': 46, 'Ag': 47, 'Cd': 48, 'In': 49, 'Sn': 50, 'Sb': 51, 'Te': 52, 'I': 53, 'Xe': 54, 'Pt': 78, 'Au': 79, 'W': 74 } # ======================================================================== # FORMULA PARSER # ======================================================================== def parse_molecular_formula(formula: str) -> Optional[str]: """ Parse molecular formula like H2O, CO2, CH4, Fe2O3 and convert to SMILES. Uses RDKit's built-in formula parser when possible. Args: formula: Molecular formula string (e.g., "H2O", "CO2", "CH4") Returns: SMILES string or None if parsing fails """ try: # Try simple molecules first simple_formulas = { "H2": "[H][H]", "O2": "O=O", "N2": "N#N", "Cl2": "ClCl", "H2O": "O", "H2O2": "OO", "CO": "[C-]#[O+]", "CO2": "O=C=O", "NO": "[N]=O", "NO2": "N(=O)[O]", "SO2": "O=S=O", "NH3": "N", "CH4": "C", "C2H6": "CC", "C2H4": "C=C", "C2H2": "C#C", "H2S": "S", "HCl": "Cl", "HF": "F", "HBr": "Br", "HI": "I", } if formula in simple_formulas: return simple_formulas[formula] # Check if it's a single element (catalyst) if formula in PERIODIC_TABLE: return f"[{formula}]" # Try to use RDKit's MolFromFormula (experimental) # This works for simple organic molecules try: mol = Chem.rdMolDescriptors.CalcMolFormula(Chem.MolFromSmiles("C")) # Test availability # For now, return None as RDKit's formula parser is limited return None except: return None except Exception: return None # ======================================================================== # MAIN CONVERSION FUNCTION # ======================================================================== def molecule_to_smiles(molecule_input: str) -> Optional[str]: """ Intelligent converter: handles SMILES, common names, formulas, and elements. 4-Stage Parsing Pipeline: 1. Check if input is already valid SMILES 2. Check common names database 3. Try parsing as molecular formula 4. Return None if all fail Args: molecule_input: User input (any format) Returns: SMILES string or None Examples: >>> molecule_to_smiles("water") "O" >>> molecule_to_smiles("H2O") "O" >>> molecule_to_smiles("Fe") "[Fe]" >>> molecule_to_smiles("O") # Direct SMILES "O" """ if not molecule_input: return None # Normalize input molecule_input = molecule_input.strip() # Stage 1: Try direct SMILES parsing first try: mol = Chem.MolFromSmiles(molecule_input) if mol is not None: # Sanitize to ensure it's chemically valid Chem.SanitizeMol(mol) return Chem.MolToSmiles(mol) # Return canonical SMILES except: pass # Stage 2: Check common names database (case-insensitive) lookup_key = molecule_input.lower() if lookup_key in COMMON_NAMES: return COMMON_NAMES[lookup_key] # Stage 3: Try parsing as molecular formula formula_smiles = parse_molecular_formula(molecule_input) if formula_smiles: return formula_smiles # Stage 4: Failed all parsing attempts return None # ======================================================================== # VALIDATION & METADATA # ======================================================================== def validate_molecule(smiles: str, max_atoms: int = 6) -> Dict: """ Comprehensive validation with chemistry rules and detailed metadata. Validation Steps: 1. RDKit can parse the SMILES 2. Molecule is chemically valid (valence rules, sanitization) 3. Atom count ≤ max_atoms (including hydrogens) 4. Return detailed metadata for downstream use Args: smiles: SMILES string to validate max_atoms: Maximum allowed atom count (default 6) Returns: Dictionary with validation results and metadata: { "valid": bool, "smiles": str (canonical), "formula": str, "mol_weight": float, "atom_count": int, "heavy_atom_count": int, "elements": List[str], "error": str (if invalid) } """ result = { "valid": False, "smiles": smiles, "formula": "", "mol_weight": 0.0, "atom_count": 0, "heavy_atom_count": 0, "elements": [], "error": "" } try: # Step 1: Parse SMILES mol = Chem.MolFromSmiles(smiles) if mol is None: result["error"] = "Invalid SMILES string - cannot parse molecule" return result # Step 2: Chemical validation (sanitization) try: Chem.SanitizeMol(mol) except Exception as e: result["error"] = f"Chemically invalid molecule: {str(e)}" return result # Step 3: Add explicit hydrogens for accurate atom count mol_with_hs = Chem.AddHs(mol) atom_count = mol_with_hs.GetNumAtoms() heavy_atom_count = mol.GetNumHeavyAtoms() # Step 4: Check atom count constraint if atom_count > max_atoms: result["error"] = f"Molecule has {atom_count} atoms (max allowed: {max_atoms})" return result # Step 5: Extract metadata formula = Chem.rdMolDescriptors.CalcMolFormula(mol) mol_weight = Descriptors.MolWt(mol) elements = list(set([atom.GetSymbol() for atom in mol.GetAtoms()])) canonical_smiles = Chem.MolToSmiles(mol) # Success! result.update({ "valid": True, "smiles": canonical_smiles, "formula": formula, "mol_weight": round(mol_weight, 2), "atom_count": atom_count, "heavy_atom_count": heavy_atom_count, "elements": sorted(elements), "error": "" }) return result except Exception as e: result["error"] = f"Validation error: {str(e)}" return result # ======================================================================== # USER-FRIENDLY WRAPPER # ======================================================================== def process_molecule_input(user_input: str, max_atoms: int = 6) -> Dict: """ Complete pipeline: Convert input → Validate → Return metadata. This is the main function to use in the Streamlit app. Args: user_input: Any format (name, formula, SMILES) max_atoms: Maximum atom count allowed Returns: Validation result dictionary with all metadata Example: >>> result = process_molecule_input("water") >>> print(result) { "valid": True, "smiles": "O", "formula": "H2O", "atom_count": 3, ... } """ # Step 1: Convert to SMILES smiles = molecule_to_smiles(user_input) if smiles is None: return { "valid": False, "error": f"Cannot recognize '{user_input}'. Try: common names (water, methane), formulas (H2O, CO2), or SMILES strings.", "smiles": "", "formula": "", "mol_weight": 0.0, "atom_count": 0, "heavy_atom_count": 0, "elements": [] } # Step 2: Validate and get metadata return validate_molecule(smiles, max_atoms) # ======================================================================== # HELPER: Get suggestions for failed inputs # ======================================================================== def get_similar_molecules(user_input: str, limit: int = 5) -> List[str]: """ Suggest similar molecule names when input fails. Uses simple string matching. """ user_lower = user_input.lower() matches = [] for name in COMMON_NAMES.keys(): if user_lower in name or name in user_lower: matches.append(name) if len(matches) >= limit: break return matches