// Educational browser model source snapshot. Not the native oNeura engine or a validated clinical model. // Generated from the inline script in smiles.html. No patient decisions. // ─── DRUG GALLERY ───────────────────────────────────────── const DRUGS = [ { name: 'Aspirin', smiles: 'CC(=O)Oc1ccccc1C(=O)O' }, { name: 'Ibuprofen', smiles: 'CC(C)Cc1ccc(cc1)[C@@H](C)C(=O)O' }, { name: 'Caffeine', smiles: 'Cn1cnc2c1c(=O)n(c(=O)n2C)C' }, { name: 'Warfarin', smiles: 'CC(=O)c1cccc(O)c1C1=C(Oc2ccccc2)C(=O)c2ccccc21' }, { name: 'Metoprolol', smiles: 'COCCc1ccc(OCC(O)CNC(C)C)cc1' }, { name: 'Atorvastatin', smiles: 'CC(C)C1=CC=C(C=C1)C2C(C(C(=O)O2)(C)C)C(=O)NC3CC(C=C3)O' }, { name: 'Omeprazole', smiles: 'COc1ccc2[nH]c(nc2c1)[S@@](=O)Cc3ncc(C)c(OC)c3' }, { name: 'Diazepam', smiles: 'CN1C(=O)CN=C(C2=CC=CC=C2)C1=C3C=CC=CC3Cl' }, { name: 'Morphine', smiles: 'CN1CCC23C4OC5OC(CO)C=CC5C4C(O)C2C(=CC3)C1' }, { name: 'Amoxicillin', smiles: 'CC1(C)S[C@@H]2[C@H](NC(=O)[C@H](N)c3ccc(O)cc3)C(=O)N2[C@@H]1C(=O)O' } ]; // ─── ATOMIC DATA ────────────────────────────────────────── const ATOM_MASS = { H:1.008,He:4.003,Li:6.941,Be:9.012,B:10.81,C:12.011,N:14.007, O:15.999,F:18.998,Ne:20.18,Na:22.99,Mg:24.305,Al:26.982,Si:28.086, P:30.974,S:32.065,Cl:35.453,Ar:39.948,K:39.098,Ca:40.078, Br:79.904,I:126.904,Fe:55.845,Zn:65.38 }; const VALENCE = { H:1, B:3, C:4, N:3, O:2, F:1, Si:4, P:3, S:2, Cl:1, Br:1, I:1 }; // Wildman-Crippen-style LogP atom contributions (simplified) const LOGP_CONTRIB = { C: 0.1441, N: -0.7567, O: -0.2893, S: 0.6237, F: 0.4115, Cl: 0.6895, Br: 0.8813, I: 1.0500, H: 0.1230, P: 0.2650, B: -0.2000, Si: 0.3600 }; // PSA contributions from polar atoms (simplified Ertl method) const PSA_CONTRIB = { N: 12.36, O: 13.14, S: 25.30 }; // ─── PARSE SMILES ───────────────────────────────────────── function parseSMILES(smiles) { var atoms = []; var bonds = []; var stack = []; // for branches var ringOpens = {}; // ring digit -> atom index var pos = 0; var prevAtom = -1; var nextBondType = null; // null=single/aro, '=', '#', '/' var aromaticAtoms = []; function addAtom(symbol, aromatic) { var idx = atoms.length; var atom = { symbol: symbol, aromatic: !!aromatic, charge: 0, isotope: 0, implicitH: 0, index: idx }; atoms.push(atom); if (aromatic) { aromaticAtoms.push(idx); } if (prevAtom >= 0) { var btype = nextBondType !== null ? nextBondType : (aromatic ? ':' : '-'); bonds.push({ from: prevAtom, to: idx, type: btype }); } prevAtom = idx; nextBondType = null; return idx; } function parseBracket() { // bracket atom like [C@@H], [NH3+], [13C], [nH] var i = pos; var isotope = 0; // isotope if (i < smiles.length && smiles[i] >= '0' && smiles[i] <= '9') { var isoStr = ''; while (i < smiles.length && smiles[i] >= '0' && smiles[i] <= '9') { isoStr += smiles[i]; i++; } isotope = parseInt(isoStr, 10); } // symbol var aromatic = false; var symbol = ''; if (i < smiles.length) { if (smiles[i] >= 'a' && smiles[i] <= 'z') { aromatic = true; symbol = smiles[i].toUpperCase(); i++; if (i < smiles.length && smiles[i] >= 'a' && smiles[i] <= 'z') { // two-letter lowercase aromatic like "se" → Se // not common, but handle } } else if (i < smiles.length && smiles[i] >= 'A' && smiles[i] <= 'Z') { symbol = smiles[i]; i++; if (i < smiles.length && smiles[i] >= 'a' && smiles[i] <= 'z') { symbol += smiles[i]; i++; } } } // chirality if (i < smiles.length && (smiles[i] === '@')) { i++; // skip @ if (i < smiles.length && smiles[i] === '@') i++; // @@ // skip any digits after while (i < smiles.length && smiles[i] >= '0' && smiles[i] <= '9') i++; } // H count var hCount = 0; if (i < smiles.length && smiles[i] === 'H') { i++; if (i < smiles.length && smiles[i] >= '0' && smiles[i] <= '9') { hCount = parseInt(smiles[i], 10); i++; } else { hCount = 1; } } // charge var charge = 0; if (i < smiles.length && (smiles[i] === '+' || smiles[i] === '-')) { var sign = smiles[i] === '+' ? 1 : -1; i++; if (i < smiles.length && smiles[i] >= '0' && smiles[i] <= '9') { charge = sign * parseInt(smiles[i], 10); i++; } else { charge = sign; // multiple +/- signs while (i < smiles.length && smiles[i] === (sign > 0 ? '+' : '-')) { charge += sign; i++; } } } pos = i; var idx = addAtom(symbol, aromatic); atoms[idx].charge = charge; atoms[idx].isotope = isotope; atoms[idx].explicitH = hCount; return idx; } while (pos < smiles.length) { var c = smiles[pos]; // Bond characters if (c === '-' || c === '=' || c === '#' || c === ':' || c === '/' || c === '\\') { nextBondType = c; pos++; continue; } // Branch if (c === '(') { stack.push(prevAtom); pos++; continue; } if (c === ')') { if (stack.length > 0) { prevAtom = stack.pop(); } nextBondType = null; pos++; continue; } // Ring closure if ((c >= '0' && c <= '9') || c === '%') { var ringNum; if (c === '%') { ringNum = parseInt(smiles.substring(pos + 1, pos + 3), 10); pos += 3; } else { ringNum = parseInt(c, 10); pos++; } if (ringOpens[ringNum] !== undefined) { // close ring var fromIdx = ringOpens[ringNum]; var btype = nextBondType !== null ? nextBondType : (atoms[fromIdx].aromatic && atoms[prevAtom].aromatic ? ':' : '-'); bonds.push({ from: prevAtom, to: fromIdx, type: btype }); delete ringOpens[ringNum]; } else { ringOpens[ringNum] = prevAtom; } nextBondType = null; continue; } // Bracket atom if (c === '[') { pos++; // skip '[' parseBracket(); // skip closing ']' if (pos < smiles.length && smiles[pos] === ']') pos++; continue; } // Organic subset atoms (uppercase = aliphatic) if (c === 'B') { if (pos + 1 < smiles.length && smiles[pos + 1] === 'r') { addAtom('Br', false); pos += 2; } else { addAtom('B', false); pos++; } continue; } if (c === 'C') { if (pos + 1 < smiles.length && smiles[pos + 1] === 'l') { addAtom('Cl', false); pos += 2; } else { addAtom('C', false); pos++; } continue; } if (c === 'N') { addAtom('N', false); pos++; continue; } if (c === 'O') { addAtom('O', false); pos++; continue; } if (c === 'S') { addAtom('S', false); pos++; continue; } if (c === 'P') { addAtom('P', false); pos++; continue; } if (c === 'F') { addAtom('F', false); pos++; continue; } if (c === 'I') { addAtom('I', false); pos++; continue; } // Aromatic atoms (lowercase) if (c === 'c') { addAtom('C', true); pos++; continue; } if (c === 'n') { addAtom('N', true); pos++; continue; } if (c === 'o') { addAtom('O', true); pos++; continue; } if (c === 's') { addAtom('S', true); pos++; continue; } if (c === 'p') { addAtom('P', true); pos++; continue; } // Dot (disconnected) if (c === '.') { prevAtom = -1; nextBondType = null; pos++; continue; } // Unknown char, skip pos++; } // Compute implicit H for each atom for (var i = 0; i < atoms.length; i++) { var a = atoms[i]; if (a.explicitH !== undefined) { a.implicitH = a.explicitH; } else { var bondCount = 0; var aromaticBondCount = 0; for (var j = 0; j < bonds.length; j++) { if (bonds[j].from === i || bonds[j].to === i) { if (bonds[j].type === '=') bondCount += 2; else if (bonds[j].type === '#') bondCount += 3; else if (bonds[j].type === ':') { bondCount += 1; aromaticBondCount++; } else bondCount += 1; } } var maxVal = VALENCE[a.symbol] || 4; var implicit = maxVal - bondCount; if (a.charge !== 0) { implicit -= a.charge; // positive charge reduces H } if (a.aromatic && a.symbol === 'N') { // Check if this N has a lone pair (contributes to aromaticity) // if bondCount is 2 (in ring with 2 bonds), then 1 H possible if maxVal=3 } a.implicitH = Math.max(0, implicit); } } return { atoms: atoms, bonds: bonds, aromaticAtoms: aromaticAtoms }; } // ─── COMPUTE PROPERTIES ─────────────────────────────────── function computeProps(parsed) { var atoms = parsed.atoms; var bonds = parsed.bonds; var aromaticAtoms = parsed.aromaticAtoms; // Molecular Weight var mw = 0; for (var i = 0; i < atoms.length; i++) { var sym = atoms[i].symbol; var mass = ATOM_MASS[sym] || 0; mw += mass; // add implicit H mw += atoms[i].implicitH * ATOM_MASS['H']; } // Heavy atoms (non-H) var heavyAtoms = atoms.length; // LogP (Wildman-Crippen simplified) var logp = 0; for (var i = 0; i < atoms.length; i++) { var sym = atoms[i].symbol; var contrib = LOGP_CONTRIB[sym] || 0; logp += contrib; // H contributions logp += atoms[i].implicitH * (LOGP_CONTRIB['H'] || 0); } // aromatic correction var aromaticRingCount = countAromaticRings(atoms, bonds, aromaticAtoms); logp -= aromaticRingCount * 0.32; // PSA var psa = 0; for (var i = 0; i < atoms.length; i++) { var sym = atoms[i].symbol; if (PSA_CONTRIB[sym]) { psa += PSA_CONTRIB[sym]; } } // HBD: OH and NH groups var hbd = 0; for (var i = 0; i < atoms.length; i++) { var a = atoms[i]; if (a.symbol === 'O' || a.symbol === 'N') { if (a.implicitH > 0) hbd += a.implicitH; // also check bonds for explicit H connections would go here } } // HBA: N and O atoms var hba = 0; for (var i = 0; i < atoms.length; i++) { if (atoms[i].symbol === 'N' || atoms[i].symbol === 'O') { hba++; } } // Rotatable bonds: single, non-ring, not terminal, not amide C-N var rotBonds = 0; for (var i = 0; i < bonds.length; i++) { var b = bonds[i]; if (b.type !== '-') continue; // only single bonds var a1 = atoms[b.from]; var a2 = atoms[b.to]; // skip if terminal (H or halogen) if (a1.symbol === 'H' || a2.symbol === 'H') continue; if (a1.symbol === 'F' || a2.symbol === 'F') continue; if (a1.symbol === 'Cl' || a2.symbol === 'Cl') continue; if (a1.symbol === 'Br' || a2.symbol === 'Br') continue; if (a1.symbol === 'I' || a2.symbol === 'I') continue; // skip ring bonds (simplified: check if both atoms share another bond path) if (isRingBond(bonds, b.from, b.to)) continue; rotBonds++; } return { mw: mw, logp: logp, psa: psa, hbd: hbd, hba: hba, rotBonds: rotBonds, heavyAtoms: heavyAtoms, aromaticRings: aromaticRingCount }; } function isRingBond(bonds, a, b) { // BFS from a to b without using direct bond var visited = {}; var queue = [a]; visited[a] = true; // Build adjacency (excluding the direct a-b connection we're testing) var adj = {}; for (var i = 0; i < bonds.length; i++) { var f = bonds[i].from; var t = bonds[i].to; if ((f === a && t === b) || (f === b && t === a)) continue; if (!adj[f]) adj[f] = []; if (!adj[t]) adj[t] = []; adj[f].push(t); adj[t].push(f); } while (queue.length > 0) { var node = queue.shift(); if (node === b) return true; var neighbors = adj[node] || []; for (var i = 0; i < neighbors.length; i++) { if (!visited[neighbors[i]]) { visited[neighbors[i]] = true; queue.push(neighbors[i]); } } } return false; } function countAromaticRings(atoms, bonds, aromaticAtoms) { if (aromaticAtoms.length === 0) return 0; // Count connected components of aromatic atoms that form cycles // Simplified: use aromatic bonds to find rings var aromaticBonds = []; for (var i = 0; i < bonds.length; i++) { if (bonds[i].type === ':' || (atoms[bonds[i].from].aromatic && atoms[bonds[i].to].aromatic)) { aromaticBonds.push(bonds[i]); } } // Build adjacency for aromatic subgraph var aSet = {}; for (var i = 0; i < aromaticAtoms.length; i++) aSet[aromaticAtoms[i]] = true; var adj = {}; for (var i = 0; i < aromaticBonds.length; i++) { var f = aromaticBonds[i].from; var t = aromaticBonds[i].to; if (!adj[f]) adj[f] = []; if (!adj[t]) adj[t] = []; adj[f].push(t); adj[t].push(f); } // Find rings using DFS - count distinct cycles // Simplified: estimate rings = edges - nodes + components (for planar aromatic system) var edgeCount = aromaticBonds.length; var nodeCount = aromaticAtoms.length; if (nodeCount === 0) return 0; // Count connected components var visited = {}; var components = 0; for (var i = 0; i < aromaticAtoms.length; i++) { var start = aromaticAtoms[i]; if (visited[start]) continue; components++; var stack = [start]; while (stack.length > 0) { var node = stack.pop(); if (visited[node]) continue; visited[node] = true; var neighbors = adj[node] || []; for (var j = 0; j < neighbors.length; j++) { if (!visited[neighbors[j]]) stack.push(neighbors[j]); } } } // Euler formula for planar graph: rings = edges - nodes + components var rings = edgeCount - nodeCount + components; return Math.max(0, rings); } // ─── LIPINSKI ───────────────────────────────────────────── function checkLipinski(props) { var rules = [ { name: 'MW', value: props.mw, limit: 500, label: 'MW < 500' }, { name: 'LogP', value: props.logp, limit: 5, label: 'LogP < 5' }, { name: 'HBD', value: props.hbd, limit: 5, label: 'HBD < 5' }, { name: 'HBA', value: props.hba, limit: 10, label: 'HBA < 10' } ]; var violations = 0; for (var i = 0; i < rules.length; i++) { rules[i].pass = rules[i].value < rules[i].limit; if (!rules[i].pass) violations++; } return { rules: rules, violations: violations, pass: violations <= 1 }; } // ─── DRUG CLASS DETECTION ───────────────────────────────── function detectDrugClass(smiles) { var s = smiles.toLowerCase(); // NSAID patterns if (s.match(/c\(=o\)o/) && s.match(/c1ccccc1/)) return 'NSAID / Salicylate'; if (s.match(/c\(=o\)o/) && s.match(/c1ccc/) && (s.match(/cc\[ch\]/) || s.match(/isopropyl/) || s.includes('c(cc'))) return 'NSAID / Propionic Acid Derivative'; // Beta blocker: aryloxypropanolamine if (s.match(/c1ccc.*occ/) && s.match(/oh/) && s.match(/nc/)) return 'Beta Blocker'; // Statin: HMG-CoA reductase inhibitor if (s.match(/c\(=o\)o/) && s.match(/c1ccc/) && s.length > 40) return 'Statin / Lipid Regulator'; // Benzodiazepine if (s.match(/c1=cc=cc=c1.*c=c.*c\(=o\)n/) && s.match(/cl/)) return 'Benzodiazepine / Anxiolytic'; if (s.match(/c\(=o\)n/) && s.match(/c1ccccc1/) && s.match(/n.*c=c/)) return 'Benzodiazepine / Anxiolytic'; // Opioid if (s.match(/n1ccc/) && s.match(/c.*c.*o/) && s.length > 30 && s.match(/c\(o\)/)) return 'Opioid Analgesic'; // PPI (proton pump inhibitor) if (s.match(/s.*=o/) && s.match(/c1ccc.*n/) && s.match(/c\(o[cn]/)) return 'Proton Pump Inhibitor'; // Penicillin / Beta-lactam if (s.match(/c\(=o\)n.*c.*s/) && s.match(/c\(=o\)o/)) return 'Beta-Lactam Antibiotic'; // Fluoroquinolone if (s.match(/f/) && s.match(/c\(=o\)c.*n.*c.*c\(=o\)o/)) return 'Fluoroquinolone Antibiotic'; // Xanthine stimulant if (s.match(/c\(=o\)n.*c\(=o\)n/) && s.match(/c1ncnc2/)) return 'Xanthine Derivative / Stimulant'; // General carboxylic acid drug if (s.match(/c\(=o\)o/)) return 'Carboxylic Acid Drug'; // General amine drug if (s.match(/n[c\(]/)) return 'Amino Compound'; // Phenol-containing if (s.match(/oc1ccc/) || s.match(/oc1cccc/)) return 'Phenolic Compound'; // Sulfonamide if (s.match(/s\(=o\)\(=o\)/) || s.match(/s\(=o\)\(=o\)n/)) return 'Sulfonamide'; // General heterocyclic if (s.match(/n.*c.*c.*n/) || s.match(/n1ccc/)) return 'Nitrogen Heterocycle'; // Aromatic compound if (s.match(/c1cc/) || s.match(/c1cccc/)) return 'Aromatic Compound'; return 'Unclassified Small Molecule'; } // ─── MOLECULE ENGINE ────────────────────────────────────── // Primary: OpenChemLib (handles any valid SMILES + draws the structure). // Fallback: the built-in parser above, if OCL fails to load (offline). function analyzeWithOCL(smiles) { var mol = OCL.Molecule.fromSmiles(smiles); // throws on invalid SMILES if (!mol || mol.getAllAtoms() === 0) throw new Error('empty'); mol.ensureHelperArrays(OCL.Molecule.cHelperRings); var mf = mol.getMolecularFormula(); var mp = new OCL.MoleculeProperties(mol); var heavy = 0, n = mol.getAllAtoms(); for (var i = 0; i < n; i++) { if (mol.getAtomicNo(i) !== 1) heavy++; } var aromRings = 0; try { var rs = mol.getRingSet(); for (var r = 0; r < rs.getSize(); r++) { if (rs.isAromatic(r)) aromRings++; } } catch (e) {} var svg = ''; try { svg = mol.toSVG(300, 220, undefined, { autoCrop: true, autoCropMargin: 14, suppressChiralText: true }); } catch (e) {} var canonical = smiles; try { canonical = mol.toSmiles(); } catch (e) {} return { ok: true, engine: 'OpenChemLib', props: { mw: mf.relativeWeight, logp: mp.logP, psa: mp.polarSurfaceArea, hbd: mp.donorCount, hba: mp.acceptorCount, rotBonds: mp.rotatableBondCount, heavyAtoms: heavy, aromaticRings: aromRings }, formula: mf.formula, canonical: canonical, svg: svg }; } function analyzeWithBuiltin(smiles) { var parsed = parseSMILES(smiles); if (parsed.atoms.length === 0) throw new Error('empty'); return { ok: true, engine: 'built-in', props: computeProps(parsed), formula: '', canonical: smiles, svg: '' }; } function analyzeMolecule(smiles) { if (window.OCL) { // OCL is the authority on validity: if it rejects the string, it's invalid. try { return analyzeWithOCL(smiles); } catch (e) { return { ok: false, error: 'Could not parse that SMILES string. Please check the syntax.' }; } } // OCL not loaded (offline / still downloading) → best-effort built-in parser. try { return analyzeWithBuiltin(smiles); } catch (e) { return { ok: false, error: 'Could not parse that SMILES string. Please check the syntax.' }; } } function renderStructure(res) { var box = document.getElementById('structureSvg'); if (res.svg) box.innerHTML = res.svg; else box.innerHTML = 'Structure depiction unavailable offline'; document.getElementById('propFormula').textContent = res.formula || '-'; document.getElementById('propCanonical').textContent = res.canonical || '-'; } // ─── UI UPDATE ──────────────────────────────────────────── function compute() { var smiles = document.getElementById('smilesInput').value.trim(); var errEl = document.getElementById('smilesError'); errEl.textContent = ''; if (!smiles) { errEl.textContent = 'Enter a SMILES string to analyze.'; return; } var res = analyzeMolecule(smiles); if (!res.ok) { errEl.textContent = res.error; document.getElementById('engineTag').textContent = ''; return; } updatePropsUI(res.props); updateLipinskiUI(checkLipinski(res.props), res.props); updateDrugClassUI(detectDrugClass(smiles)); renderStructure(res); document.getElementById('engineTag').textContent = 'Engine: ' + res.engine; animatePipeline(); } function updatePropsUI(props) { document.getElementById('propMW').textContent = props.mw.toFixed(2); document.getElementById('propLogP').textContent = props.logp.toFixed(2); document.getElementById('propPSA').textContent = props.psa.toFixed(1) + ' \u00C5\u00B2'; document.getElementById('propHBD').textContent = props.hbd; document.getElementById('propHBA').textContent = props.hba; document.getElementById('propRotBonds').textContent = props.rotBonds; document.getElementById('propHeavyAtoms').textContent = props.heavyAtoms; document.getElementById('propAromaticRings').textContent = props.aromaticRings; } function updateLipinskiUI(lipinski, props) { var ids = ['MW', 'LogP', 'HBD', 'HBA']; var vals = [props.mw, props.logp, props.hbd, props.hba]; var limits = [500, 5, 5, 10]; for (var i = 0; i < ids.length; i++) { var rule = lipinski.rules[i]; document.getElementById('lip' + ids[i]).textContent = vals[i].toFixed(ids[i] === 'MW' ? 2 : (ids[i] === 'LogP' ? 2 : 0)) + ' / ' + limits[i]; var badge = document.getElementById('lip' + ids[i] + 'badge'); badge.textContent = rule.pass ? 'PASS' : 'FAIL'; badge.className = 'badge ' + (rule.pass ? 'badge-pass' : 'badge-fail'); } var overall = document.getElementById('lipinskiOverall'); overall.style.display = 'block'; if (lipinski.pass) { overall.textContent = 'Lipinski Compliant (' + lipinski.violations + ' violation' + (lipinski.violations !== 1 ? 's' : '') + ')'; overall.className = 'lipinski-overall pass'; } else { overall.textContent = 'Lipinski Violations: ' + lipinski.violations + ' of 4 rules exceeded'; overall.className = 'lipinski-overall fail'; } } function updateDrugClassUI(drugClass) { document.getElementById('drugClassValue').textContent = drugClass; } function animatePipeline() { var steps = document.querySelectorAll('.pipeline-step'); for (var i = 0; i < steps.length; i++) { steps[i].classList.remove('active'); } // Animate sequentially var idx = 0; function activate() { if (idx < steps.length) { steps[idx].classList.add('active'); idx++; setTimeout(activate, 120); } } activate(); } // ─── GALLERY INIT ───────────────────────────────────────── function initGallery() { var container = document.getElementById('gallery'); for (var i = 0; i < DRUGS.length; i++) { (function(drug) { var chip = document.createElement('button'); chip.className = 'chip'; chip.innerHTML = '' + drug.name + ''; chip.onclick = function() { document.getElementById('smilesInput').value = drug.smiles; compute(); }; container.appendChild(chip); })(DRUGS[i]); } } // Allow Enter key to trigger compute document.getElementById('smilesInput').addEventListener('keydown', function(e) { if (e.key === 'Enter') compute(); }); // Wait for the OpenChemLib engine (deferred script) before the first compute, // so the page opens with a fully-rendered structure rather than the fallback. function oclReady(cb) { if (window.OCL) return cb(); var s = document.getElementById('ocl-script'); if (!s) return cb(); s.addEventListener('load', cb, { once: true }); s.addEventListener('error', cb, { once: true }); // fall back to built-in } // Init initGallery(); // Auto-run Aspirin so page never opens with blank -- document.getElementById('smilesInput').value = DRUGS[0].smiles; oclReady(compute);