import html import gradio as gr import spaces from rdkit import Chem from rdkit.Chem.Draw import rdMolDraw2D from inference import VQAtomTokenizer TOKENIZER = VQAtomTokenizer("data/model_epoch_3.pt", device="cpu") def draw_svg(smiles, tokens): mol = Chem.MolFromSmiles(smiles) if mol is None: return "" for atom, token in zip(mol.GetAtoms(), tokens): atom.SetProp("atomNote", f"VQ{token}") drawer = rdMolDraw2D.MolDraw2DSVG(760, 330) opts = drawer.drawOptions() opts.addAtomIndices = True drawer.DrawMolecule(mol) drawer.FinishDrawing() return drawer.GetDrawingText().replace("svg:", "") @spaces.GPU(duration=1) def zerogpu_marker(): """Required by ZeroGPU hosting; live inference is intentionally CPU-side.""" return True def encode_molecule(smiles): smiles = (smiles or "").strip() if not smiles: raise gr.Error("Enter a SMILES string.") try: result = TOKENIZER.encode(smiles) except Exception as exc: raise gr.Error(str(exc)) mol = Chem.MolFromSmiles(smiles) atoms = [a.GetSymbol() for a in mol.GetAtoms()] tokens = result["tokens"] chips = "".join( f'
{i}{html.escape(atom)}VQ{token}
' for i, (atom, token) in enumerate(zip(atoms, tokens)) ) return f'''
VQATOM ENCODING{html.escape(smiles)}
{draw_svg(smiles, tokens)}
PER-ATOM TOKEN SEQUENCE
{chips}
''' CSS = """ @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap'); :root{--ink:#17231d;--paper:#f7f6f1;--line:#d9ddd7;--green:#bfff3f;--muted:#56625c} .gradio-container{max-width:1080px!important;margin:0 auto!important;padding:0 24px!important;background:var(--paper)!important;color:var(--ink)!important;font-family:'DM Sans',sans-serif!important} .contain{max-width:none!important;padding:0!important}footer{display:none!important}.main{padding:0!important} .shell{width:100%;margin:auto}.topbar{height:70px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line)} .brand{font-size:20px;font-weight:700}.brand i{font-style:normal;background:var(--ink);color:var(--green);border-radius:50%;font-size:10px;padding:9px 6px;margin-right:8px} .paper-link{color:var(--ink)!important;text-decoration:none;border:1px solid var(--line);border-radius:7px;padding:10px 14px;font-size:15px;font-weight:600} .hero{padding:54px 0 32px;max-width:850px}.hero h1{color:var(--ink)!important;font-size:clamp(46px,6vw,68px);line-height:1.05;letter-spacing:-2.5px;margin:0 0 20px}.hero p{font-size:19px;line-height:1.65;color:var(--muted)!important;max-width:760px;margin:0} .demo-intro{background:white;border:1px solid var(--line);border-radius:12px 12px 0 0;padding:24px 26px 20px}.demo-intro h2{color:var(--ink)!important;font-size:25px;margin:0 0 7px}.demo-intro p{color:var(--muted)!important;font-size:16px;margin:0}.example{display:inline-block;margin-top:13px!important;padding:9px 12px;background:#f0f4ed;border-radius:7px;color:var(--ink)!important;font-family:monospace;font-size:15px!important} .input-card{background:white!important;border:1px solid var(--line)!important;border-top:0!important;border-radius:0 0 12px 12px!important;padding:20px 24px 24px!important;margin:0!important;gap:12px!important}.input-card label span{color:var(--ink)!important;font-size:14px!important;letter-spacing:.5px;font-weight:700!important}.input-card textarea{font-family:monospace!important;font-size:17px!important;background:#fbfcfa!important;color:var(--ink)!important;border-color:var(--line)!important} .encode-btn{background:var(--ink)!important;color:white!important;border:0!important;font-size:16px!important;font-weight:700!important;min-width:190px!important} .output-html{background:transparent!important;border:0!important;padding:0!important;margin:16px 0 0!important} .result-shell{border:1px solid var(--line);border-radius:9px;overflow:hidden;background:white}.result-head{display:flex;justify-content:space-between;padding:14px 16px;border-bottom:1px solid var(--line)} .result-head span,.sequence>span{font-size:9px;letter-spacing:1.4px;color:var(--muted)}.result-head small{font:10px monospace;color:var(--muted)} .mol-svg{padding:12px;background:white;text-align:center}.mol-svg svg{max-width:760px;width:100%;height:auto}.sequence{border-top:1px solid var(--line);padding:16px} .sequence>div{display:flex;gap:7px;flex-wrap:wrap;margin-top:10px}.atom-token{display:grid;grid-template-columns:auto auto;gap:2px 7px;align-items:center;border:1px solid var(--line);background:#f7f9f6;padding:7px 9px;border-radius:6px} .atom-token span{font-size:8px;color:#7d8983}.atom-token b{font-size:12px}.atom-token code{grid-column:1/3;color:#356600;font-size:10px} .method{padding:62px 0 72px}.method h2{color:var(--ink)!important;font-size:34px;margin:0 0 26px}.steps{display:flex;flex-direction:column;gap:0}.steps article{display:grid;grid-template-columns:120px 1fr;gap:28px;align-items:center;border-top:1px solid var(--line);padding:28px 4px;background:transparent}.steps article:last-child{border-bottom:1px solid var(--line)}.step-visual{width:110px;height:82px;color:var(--ink)}.step-copy b{color:#5f8700;font-size:13px}.step-copy h3{color:var(--ink)!important;font-size:23px;margin:7px 0}.step-copy p{font-size:17px;line-height:1.65;color:var(--muted)!important;margin:0;max-width:720px}.citation{font-size:16px;color:var(--muted)!important;margin-top:26px;line-height:1.6}.citation a{color:var(--ink)!important;font-weight:700} @media(max-width:700px){.gradio-container{padding:0 16px!important}.hero{padding:38px 0 25px}.hero h1{font-size:43px}.hero p{font-size:17px}.demo-intro{padding:20px}.input-card{display:block!important;padding:18px!important}.encode-btn{margin-top:10px!important;width:100%!important}.steps article{grid-template-columns:82px 1fr;gap:16px}.step-visual{width:76px}.step-copy p{font-size:15px}.result-head{display:block}.result-head small{display:block;margin-top:6px;overflow-wrap:anywhere}} """ with gr.Blocks(css=CSS, title="VQAtom Demo", fill_width=True) as demo: gr.HTML('''
VQAtom
Read the paper ↗

Turn molecular environments into discrete tokens.

Enter a SMILES string to assign a learned, context-dependent VQAtom ID to each heavy atom.

Try VQAtom

Use the default example or replace it with any valid SMILES string.

Acetanilide · CC(=O)NC1=CC=CC=C1

''') with gr.Row(elem_classes="input-card"): smiles = gr.Textbox(value="CC(=O)NC1=CC=CC=C1", label="SMILES", lines=1, scale=5) button = gr.Button("Generate IDs", elem_classes="encode-btn", scale=1) output = gr.HTML(elem_classes="output-html") gr.HTML('''

How the IDs are created

01 · GRAPH CONTEXT

Aggregate the neighborhood

A graph neural network collects the structural environment surrounding each atom over three hops.

02 · VECTOR QUANTIZATION

Find the nearest code

The continuous representation is matched to the nearest entry in an element-partitioned codebook.

03 · DISCRETE TOKEN

Return a global VQAtom ID

The selected codebook entry becomes a reusable discrete token for that atom.

Method and evaluation: VQ-Atom: Semantic Discretization of Local Atomic Environments for Molecular Representation Learning ↗

''') button.click(encode_molecule, smiles, output) smiles.submit(encode_molecule, smiles, output) if __name__ == "__main__": demo.launch(ssr_mode=False)