| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import math |
| from rdkit import Chem, DataStructs |
| from rdkit.Chem import AllChem |
| from model import DATASET, COMPOUND_DB |
|
|
| |
|
|
| def _canonical(smiles): |
| mol = Chem.MolFromSmiles(smiles) |
| return Chem.MolToSmiles(mol) if mol else None |
|
|
| def _morgan_fp(smiles, r=2, n=2048): |
| mol = Chem.MolFromSmiles(smiles) |
| return AllChem.GetMorganFingerprintAsBitVect(mol, r, nBits=n) if mol else None |
|
|
| |
| _COMPOUND_INDEX = [] |
| for _at, _sub, _pos, _smi in COMPOUND_DB: |
| _can = _canonical(_smi) |
| _fp = _morgan_fp(_can) if _can else None |
| if _can and _fp: |
| _COMPOUND_INDEX.append((_at, _sub, _pos, _can, _fp)) |
|
|
| |
| _DATASET_MAP = {} |
| for _row in DATASET: |
| _key = (_row[0], _row[1], _row[2]) |
| _DATASET_MAP.setdefault(_key, []).append(_row) |
|
|
| |
| _LOCAL_SOURCES = { |
| "paper_1": { |
| "name": "Long et al., SynOpen 2023", |
| "type": "Internal dataset β peer-reviewed journal", |
| "doi": "10.1055/a-XXXX-XXXX", |
| "url": "https://www.thieme-connect.com/products/ejournals/journal/10.1055/s-00000121", |
| }, |
| "paper_2": { |
| "name": "Long et al., Bioorg. Med. Chem. 2024", |
| "type": "Internal dataset β peer-reviewed journal", |
| "doi": None, |
| "url": None, |
| }, |
| } |
|
|
| |
| _PAPER2_SUBS = { |
| "pyrrolidine", "azetidine", "piperidine", "azepane", "thiazolidine", |
| "morpholine", "dimethylmorpholine", "thiomorpholine", "thiomorpholine_dioxide", |
| "boc_piperazine", "boc_dimethylpiperazine", "bromo_tetrahydroquinoline", "piperazine", |
| } |
|
|
|
|
| def _paper_for_row(row) -> str: |
| return "paper_2" if row[1] in _PAPER2_SUBS else "paper_1" |
|
|
|
|
| |
| _EWG = {"F", "Cl", "Br", "CF3", "NO2", "CN", "fluoro", "chloro", "bromo", |
| "trifluoromethyl", "nitro", "cyano", "F3C", "fluoromethyl"} |
| _EDG = {"OMe", "Me", "methyl", "methoxy", "NMe2", "OH", "tBu", |
| "dimethylamino", "hydroxy", "tert-butyl", "ethyl", "OEt"} |
|
|
|
|
| def _generate_rationale(at: str, sub: str, pos: str, |
| temp_i: int, cat_i: int, yld: float, |
| match_tier: str, sim: float) -> str: |
| parts = [] |
|
|
| |
| sub_norm = sub.lower() if sub else "none" |
| pos_str = f"{pos}-substituted " if pos and pos != "none" else "" |
| sub_str = f"{sub} " if sub and sub != "none" else "" |
|
|
| if yld == 0: |
| outcome = "did not react (0% yield, confirmed hard fail)" |
| elif yld >= 70: |
| outcome = f"gave excellent yield ({yld:.0f}%)" |
| elif yld >= 50: |
| outcome = f"gave good yield ({yld:.0f}%)" |
| elif yld >= 30: |
| outcome = f"gave moderate yield ({yld:.0f}%)" |
| else: |
| outcome = f"gave low yield ({yld:.0f}%)" |
|
|
| temp_str = "at reflux (EtOH, ~78 Β°C)" if temp_i else "at room temperature" |
| cat_str = "with Zn(OTf)β (10β13 mol%)" if cat_i else "without Lewis acid catalyst" |
|
|
| parts.append( |
| f"The {pos_str}{sub_str}{at} {outcome} {temp_str} {cat_str} in EtOH." |
| ) |
|
|
| |
| if at == "aniline": |
| if cat_i: |
| parts.append( |
| "Anilines are poor nucleophiles due to lone-pair delocalisation into the aromatic ring; " |
| "Zn(OTf)β activates the squarate electrophile to enable productive mono-addition." |
| ) |
| else: |
| parts.append( |
| "Aniline nitrogen is deactivated by resonance with the arene; " |
| "unusually, this substrate reacted without Lewis acid activation." |
| ) |
| elif at == "benzylamine": |
| parts.append( |
| "Benzylic amines are significantly more nucleophilic than anilines " |
| "because the lone pair is not conjugated into the ring, " |
| "allowing reaction without Lewis acid catalyst." |
| ) |
| elif at == "heterocyclic": |
| parts.append( |
| "Cyclic secondary amines react via NβH mono-addition to one ester of " |
| "diethyl squarate; ring strain and basicity of the nitrogen influence rate." |
| ) |
|
|
| |
| is_ewg = sub_norm in {s.lower() for s in _EWG} |
| is_edg = sub_norm in {s.lower() for s in _EDG} |
|
|
| if yld == 0 and is_ewg: |
| parts.append( |
| f"The strongly electron-withdrawing {sub} group deactivates the amine sufficiently " |
| "to prevent reaction entirely β a confirmed hard fail in the Wren dataset." |
| ) |
| elif is_ewg: |
| parts.append( |
| f"The electron-withdrawing {sub} group reduces nucleophilicity at nitrogen " |
| "but does not fully suppress reactivity; yield is lower than electron-neutral analogues." |
| ) |
| elif is_edg: |
| parts.append( |
| f"The electron-donating {sub} group increases electron density at nitrogen, " |
| "enhancing nucleophilicity and contributing to the higher yield." |
| ) |
|
|
| |
| if pos == "para": |
| parts.append( |
| "Para substitution avoids steric interaction with the reacting amine and " |
| "transmits electronic effects most efficiently through resonance β " |
| "typically the highest-yielding regioisomer in this series." |
| ) |
| elif pos == "meta": |
| parts.append( |
| "Meta substitution acts primarily through inductive pathways; " |
| "yields are generally intermediate between para and ortho analogues." |
| ) |
| elif pos == "ortho": |
| if yld == 0: |
| parts.append( |
| "Ortho substitution places the group adjacent to the amine, causing " |
| "severe steric clash that completely prevents nucleophilic addition to squarate." |
| ) |
| else: |
| parts.append( |
| "Ortho substitution creates steric congestion near the amine, " |
| "reducing but not eliminating reactivity." |
| ) |
|
|
| |
| if match_tier == "exact": |
| parts.append( |
| "This is an experimentally confirmed result for the exact same compound " |
| "β yield is directly reported in the cited paper." |
| ) |
| elif match_tier == "high": |
| parts.append( |
| f"Structural similarity to your query is high (Tanimoto {sim:.2f}); " |
| "this reference is strongly predictive of expected outcome." |
| ) |
| elif match_tier == "medium": |
| parts.append( |
| f"Moderate structural similarity (Tanimoto {sim:.2f}); " |
| "electronic and steric trends should transfer, but yield may differ by Β±15β20%." |
| ) |
| else: |
| parts.append( |
| f"Lower structural similarity (Tanimoto {sim:.2f}); " |
| "treat as directional guidance β conditions and general reactivity pattern apply." |
| ) |
|
|
| return " ".join(parts) |
|
|
|
|
| def _row_to_ref(row, sim: float, match_tier: str) -> dict: |
| """Convert a DATASET row to a structured reference dict.""" |
| at, sub, pos, temp_i, cat_i, yld = row |
| paper_key = _paper_for_row(row) |
| src = _LOCAL_SOURCES[paper_key] |
| return { |
| "amine_type": at, |
| "substituent": sub, |
| "position": pos, |
| "temperature": "reflux" if temp_i else "r.t.", |
| "catalyst": "Zn(OTf)β (10β13 mol%)" if cat_i else "none", |
| "yield_percent": yld, |
| "solvent": "EtOH", |
| "match_tier": match_tier, |
| "similarity": round(sim, 3), |
| "rationale": _generate_rationale(at, sub, pos, temp_i, cat_i, yld, match_tier, sim), |
| "source": { |
| "label": src["name"], |
| "type": src["type"], |
| "doi": src["doi"], |
| "url": src.get("url"), |
| }, |
| } |
|
|
|
|
| |
|
|
| def search_local(amine_smiles: str, top_k: int = 5) -> dict: |
| """ |
| Search the local training dataset for reactions involving amines |
| similar to the query. |
| |
| Returns: |
| exact list[dict] β same canonical SMILES in training set |
| similar list[dict] β Tanimoto-sorted similar amines (>0.30) |
| sources list[str] β source labels used |
| """ |
| can = _canonical(amine_smiles) |
| q_fp = _morgan_fp(can) if can else None |
|
|
| exact_rows = [] |
| similar_hits = [] |
|
|
| for at, sub, pos, db_can, db_fp in _COMPOUND_INDEX: |
| |
| if can and can == db_can: |
| key = (at, sub, pos) |
| for row in _DATASET_MAP.get(key, []): |
| exact_rows.append(_row_to_ref(row, 1.0, "exact")) |
| continue |
|
|
| |
| if q_fp is not None and db_fp is not None: |
| sim = DataStructs.TanimotoSimilarity(q_fp, db_fp) |
| if sim >= 0.30: |
| key = (at, sub, pos) |
| for row in _DATASET_MAP.get(key, []): |
| similar_hits.append((sim, row)) |
|
|
| |
| similar_hits.sort(key=lambda x: -x[0]) |
| seen = set() |
| similar_refs = [] |
| for sim, row in similar_hits: |
| k = (row[0], row[1], row[2], row[3], row[4]) |
| if k not in seen: |
| seen.add(k) |
| tier = "high" if sim >= 0.70 else "medium" if sim >= 0.50 else "low" |
| similar_refs.append(_row_to_ref(row, sim, tier)) |
| if len(similar_refs) >= top_k: |
| break |
|
|
| sources = [] |
| if exact_rows or similar_refs: |
| sources = [ |
| "Long et al., SynOpen 2023 (internal dataset)", |
| "Long et al., Bioorg. Med. Chem. 2024 (internal dataset)", |
| ] |
|
|
| return { |
| "exact": exact_rows, |
| "similar": similar_refs, |
| "sources": sources, |
| } |
|
|
|
|
| def compute_yield_range(refs: list[dict]) -> dict | None: |
| """Compute min / max / mean yield from a list of reference dicts.""" |
| yields = [r["yield_percent"] for r in refs if r["yield_percent"] > 0] |
| if not yields: |
| return None |
| return { |
| "min": min(yields), |
| "max": max(yields), |
| "mean": round(sum(yields) / len(yields), 1), |
| "n": len(yields), |
| } |
|
|
|
|
| |
| |
| |
| |
|
|
| def search_pubchem_reactions(amine_cid: int | None) -> dict: |
| """ |
| Placeholder β PubChem BioAssay / Patent cross-reference. |
| Returns empty results with a clear status note. |
| """ |
| return { |
| "status": "not_implemented", |
| "message": ( |
| "PubChem does not provide a general reaction search API. " |
| "Compound-level validation (CAS, SMILES, IUPAC) is performed via PubChem; " |
| "reaction references come from the internal dataset." |
| ), |
| "results": [], |
| "source": { |
| "label": "PubChem", |
| "type": "compound_database_only", |
| "url": "https://pubchem.ncbi.nlm.nih.gov", |
| }, |
| } |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| def search_ord(amine_smiles: str) -> dict: |
| """ |
| Placeholder β Open Reaction Database search. |
| Requires the ORD dataset to be downloaded and indexed locally. |
| """ |
| return { |
| "status": "not_connected", |
| "message": ( |
| "ORD search is not yet connected. " |
| "To enable: download the ORD dataset from https://github.com/open-reaction-database/ord-data, " |
| "filter for squaric acid reactions, and load the JSON index here." |
| ), |
| "results": [], |
| "source": { |
| "label": "Open Reaction Database (ORD)", |
| "type": "open_reaction_database", |
| "url": "https://open-reaction-database.org", |
| }, |
| } |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| def search_uspto(amine_smiles: str) -> dict: |
| """ |
| Placeholder β USPTO patent reaction dataset search. |
| Requires the dataset to be downloaded and indexed locally. |
| """ |
| return { |
| "status": "not_connected", |
| "message": ( |
| "USPTO patent reaction search is not yet connected. " |
| "To enable: download the Lowe USPTO reaction dataset from Figshare " |
| "(https://figshare.com/articles/dataset/Chemical_reactions_from_US_patents/5104873) " |
| "and index the squaric acid subset." |
| ), |
| "results": [], |
| "source": { |
| "label": "USPTO Patent Reaction Dataset (Lowe 2012/2017)", |
| "type": "patent_database", |
| "url": "https://figshare.com/articles/dataset/Chemical_reactions_from_US_patents/5104873", |
| }, |
| } |
|
|
|
|
| |
|
|
| def search_all_references(amine_smiles: str) -> dict: |
| """ |
| Run all search tiers and return a unified reference report. |
| |
| Keys: |
| local dict β Tier 1 results |
| pubchem dict β Tier 2 (compound-level only) |
| ord dict β Tier 3 (stub) |
| uspto dict β Tier 4 (stub) |
| reported_yield dict | None β statistics over exact + high-similarity matches |
| best_source str β highest-quality source that returned data |
| """ |
| local = search_local(amine_smiles) |
| pc = search_pubchem_reactions(None) |
| ord_res = search_ord(amine_smiles) |
| uspto = search_uspto(amine_smiles) |
|
|
| |
| primary = local["exact"] or local["similar"][:3] |
| yield_stats = compute_yield_range(primary) |
|
|
| |
| if local["exact"]: |
| best_source = "Internal dataset β exact match" |
| elif local["similar"]: |
| best_source = "Internal dataset β similar reactions" |
| else: |
| best_source = "No reference reactions found" |
|
|
| return { |
| "local": local, |
| "pubchem": pc, |
| "ord": ord_res, |
| "uspto": uspto, |
| "reported_yield": yield_stats, |
| "best_source": best_source, |
| } |
|
|