| |
| |
| |
| |
| |
| |
| |
|
|
| import math |
| import warnings |
| warnings.filterwarnings("ignore", category=DeprecationWarning, module="rdkit") |
|
|
| from rdkit import Chem, DataStructs |
| from rdkit.Chem import AllChem |
|
|
|
|
| |
| |
| |
| |
|
|
| DATASET = [ |
| |
| ("benzylamine", "none", "none", 0, 0, 33), |
| ("aniline", "none", "none", 0, 1, 37), |
| ("aniline", "F", "para", 0, 1, 75), |
| ("aniline", "Cl", "meta", 0, 1, 26), |
| ("aniline", "Cl", "para", 0, 1, 60), |
| ("aniline", "Br", "meta", 0, 1, 64), |
| ("aniline", "Br", "para", 0, 1, 66), |
| ("aniline", "OH", "para", 0, 0, 44), |
| ("aniline", "OH", "meta", 0, 0, 54), |
| ("aniline", "OH", "para", 0, 0, 45), |
| ("aniline", "OMe", "ortho", 0, 1, 18), |
| ("aniline", "OMe", "para", 0, 1, 94), |
| ("aniline", "OMe", "meta", 0, 1, 49), |
| ("aniline", "tolyl", "para", 0, 0, 29), |
| ("aniline", "tolyl", "meta", 0, 0, 89), |
| ("aniline", "tolyl", "ortho", 0, 0, 75), |
| ("aniline", "CF3", "para", 0, 0, 54), |
| ("aniline", "CF3", "meta", 0, 1, 48), |
| ("aniline", "Cl", "ortho", 0, 1, 38), |
| ("benzylamine", "Cl", "ortho", 0, 0, 25), |
| ("benzylamine", "Cl", "para", 1, 0, 99), |
| ("benzylamine", "Br", "ortho", 0, 0, 34), |
| ("benzylamine", "Br", "para", 0, 0, 57), |
| ("benzylamine", "Br", "meta", 0, 0, 41), |
| ("benzylamine", "Br", "para", 0, 0, 91), |
| ("aniline", "OH", "ortho", 1, 1, 31), |
| ("benzylamine", "fused_ring", "none", 1, 0, 48), |
| |
| ("aniline", "Cl", "ortho", 1, 1, 0), |
| ("aniline", "Br", "ortho", 1, 1, 0), |
|
|
| |
| ("heterocyclic", "pyrrolidine", "none", 0, 0, 10), |
| ("heterocyclic", "azetidine", "none", 0, 0, 40), |
| ("heterocyclic", "piperidine", "none", 0, 1, 54), |
| ("heterocyclic", "azepane", "none", 1, 0, 21), |
| ("heterocyclic", "thiazolidine", "none", 0, 0, 21), |
| ("heterocyclic", "morpholine", "none", 0, 0, 72), |
| ("heterocyclic", "dimethylmorpholine", "none", 0, 0, 76), |
| ("heterocyclic", "thiomorpholine", "none", 1, 0, 95), |
| ("heterocyclic", "thiomorpholine_dioxide", "none", 1, 0, 27), |
| ("heterocyclic", "boc_piperazine", "none", 0, 0, 73), |
| ("heterocyclic", "boc_dimethylpiperazine", "none", 0, 0, 59), |
| ("heterocyclic", "bromo_tetrahydroquinoline","none", 0, 0, 21), |
| |
| ("heterocyclic", "piperazine", "none", 0, 0, 0), |
| ] |
|
|
|
|
| |
| |
| |
|
|
| COMPOUND_DB = [ |
| |
| |
| ("benzylamine", "none", "none", "NCC1=CC=CC=C1"), |
| ("aniline", "none", "none", "Nc1ccccc1"), |
| ("aniline", "F", "para", "Nc1ccc(F)cc1"), |
| ("aniline", "Cl", "meta", "Nc1cccc(Cl)c1"), |
| ("aniline", "Cl", "para", "Nc1ccc(Cl)cc1"), |
| ("aniline", "Cl", "ortho", "Nc1ccccc1Cl"), |
| ("aniline", "Br", "meta", "Nc1cccc(Br)c1"), |
| ("aniline", "Br", "para", "Nc1ccc(Br)cc1"), |
| ("aniline", "Br", "ortho", "Nc1ccccc1Br"), |
| ("aniline", "OH", "para", "Nc1ccc(O)cc1"), |
| ("aniline", "OH", "meta", "Nc1cccc(O)c1"), |
| ("aniline", "OH", "ortho", "Nc1ccccc1O"), |
| ("aniline", "OMe", "ortho", "Nc1ccccc1OC"), |
| ("aniline", "OMe", "para", "Nc1ccc(OC)cc1"), |
| ("aniline", "OMe", "meta", "Nc1cccc(OC)c1"), |
| ("aniline", "tolyl", "para", "Nc1ccc(C)cc1"), |
| ("aniline", "tolyl", "meta", "Nc1cccc(C)c1"), |
| ("aniline", "tolyl", "ortho", "Nc1ccccc1C"), |
| ("aniline", "CF3", "para", "Nc1ccc(C(F)(F)F)cc1"), |
| ("aniline", "CF3", "meta", "Nc1cccc(C(F)(F)F)c1"), |
| ("benzylamine", "Cl", "ortho", "NCC1=CC=CC=C1Cl"), |
| ("benzylamine", "Cl", "para", "NCC1=CC=C(Cl)C=C1"), |
| ("benzylamine", "Br", "ortho", "NCC1=CC=CC=C1Br"), |
| ("benzylamine", "Br", "para", "NCC1=CC=C(Br)C=C1"), |
| ("benzylamine", "Br", "meta", "NCC1=CC=CC(Br)=C1"), |
| |
| ("heterocyclic", "pyrrolidine", "none", "C1CCNC1"), |
| ("heterocyclic", "azetidine", "none", "C1CNC1"), |
| ("heterocyclic", "piperidine", "none", "C1CCNCC1"), |
| ("heterocyclic", "azepane", "none", "C1CCNCCC1"), |
| ("heterocyclic", "thiazolidine", "none", "C1CNCS1"), |
| ("heterocyclic", "morpholine", "none", "C1CNCCO1"), |
| ("heterocyclic", "dimethylmorpholine", "none", "CC1CNCC(C)O1"), |
| ("heterocyclic", "thiomorpholine", "none", "C1CNCCS1"), |
| ("heterocyclic", "thiomorpholine_dioxide", "none", "O=S1(=O)CCNCC1"), |
| ("heterocyclic", "boc_piperazine", "none", "CC(C)(C)OC(=O)N1CCNCC1"), |
| ("heterocyclic", "boc_dimethylpiperazine", "none", "CC(C)(C)OC(=O)N1CC(C)NCC1"), |
| ("heterocyclic", "bromo_tetrahydroquinoline","none", "Brc1ccc2c(c1)CCNC2"), |
| ("heterocyclic", "piperazine", "none", "C1CNCCN1"), |
| ] |
|
|
|
|
| |
|
|
| AMINE_ENC = {"aniline": 0, "benzylamine": 1, "heterocyclic": 2} |
| POSITION_ENC = {"ortho": 0, "meta": 1, "para": 2, "none": 2} |
| SUBSTITUENT_ENC = { |
| "none": 0, "F": 1, "Cl": 2, "Br": 3, "OH": 4, |
| "OMe": 5, "CF3": 6, "tolyl": 7, "fused_ring": 8, |
| "pyrrolidine": 9, "azetidine": 10, "piperidine": 11, "azepane": 12, |
| "thiazolidine": 13, "morpholine": 14, "dimethylmorpholine": 15, |
| "thiomorpholine": 16, "thiomorpholine_dioxide": 17, |
| "boc_piperazine": 18, "boc_dimethylpiperazine": 19, |
| "bromo_tetrahydroquinoline": 20, "piperazine": 21, |
| } |
|
|
|
|
| def _encode_categorical(amine_type, substituent, position, temperature, catalyst): |
| return [ |
| AMINE_ENC.get(amine_type, 0), |
| SUBSTITUENT_ENC.get(substituent, 0), |
| POSITION_ENC.get(position, 2), |
| int(temperature == "reflux"), |
| int(bool(catalyst)), |
| ] |
|
|
|
|
| |
|
|
| def _canonical(smiles): |
| mol = Chem.MolFromSmiles(smiles) |
| return Chem.MolToSmiles(mol) if mol else None |
|
|
|
|
| def _morgan_fp(smiles, radius=2, n_bits=2048): |
| mol = Chem.MolFromSmiles(smiles) |
| if mol is None: |
| return None |
| return AllChem.GetMorganFingerprintAsBitVect(mol, radius=radius, nBits=n_bits) |
|
|
|
|
| |
| _DB_INDEX = [] |
| for _at, _sub, _pos, _smi in COMPOUND_DB: |
| _can = _canonical(_smi) |
| _fp = _morgan_fp(_can) if _can else None |
| if _can and _fp: |
| _enc = _encode_categorical(_at, _sub, _pos, "r.t.", False) |
| _DB_INDEX.append((_at, _sub, _pos, _can, _fp, _enc)) |
|
|
|
|
| |
|
|
| _HARD_FAIL_PATTERNS = [ |
| ( |
| Chem.MolFromSmarts("Nc1ccccc1Cl"), |
| "Ortho-Cl aniline fails due to steric crowding + poor nucleophilicity. " |
| "No reaction even at reflux + 48 h + Zn(OTf)β catalyst.", |
| "Switch to meta or para position for significantly higher yield.", |
| ), |
| ( |
| Chem.MolFromSmarts("Nc1ccccc1Br"), |
| "Ortho-Br aniline fails due to steric crowding + poor nucleophilicity. " |
| "No reaction even at reflux + 48 h + Zn(OTf)β catalyst.", |
| "Switch to meta or para position for significantly higher yield.", |
| ), |
| ( |
| Chem.MolFromSmarts("[NH]1CC[NH]CC1"), |
| "Unprotected piperazine fails β both N atoms compete, " |
| "giving unwanted double substitution rather than the monoamide.", |
| "Use Boc-protected piperazine instead (N-Boc-piperazine gives 73% yield).", |
| ), |
| ] |
|
|
|
|
| def _check_hard_fail(amine_smiles: str): |
| """Returns (is_fail: bool, message: str, recommendation: str).""" |
| mol = Chem.MolFromSmiles(amine_smiles) |
| if mol is None: |
| return False, "", "" |
| for pat, msg, rec in _HARD_FAIL_PATTERNS: |
| if pat and mol.HasSubstructMatch(pat): |
| return True, msg, rec |
| return False, "", "" |
|
|
|
|
| |
|
|
| _PAT_ANILINE = Chem.MolFromSmarts("Nc1ccccc1") |
| _PAT_BENZYLAMINE = Chem.MolFromSmarts("NCc1ccccc1") |
| _PAT_NH2 = Chem.MolFromSmarts("[NH2]") |
| _PAT_NH = Chem.MolFromSmarts("[NH1;R]") |
|
|
|
|
| def _infer_amine_class(smiles: str): |
| """ |
| Rough SMARTS classification for compounds not in COMPOUND_DB. |
| Returns (amine_type, substituent, position). |
| """ |
| mol = Chem.MolFromSmiles(smiles) |
| if mol is None: |
| return "aniline", "none", "none" |
| if mol.HasSubstructMatch(_PAT_BENZYLAMINE): |
| return "benzylamine", "none", "none" |
| if mol.HasSubstructMatch(_PAT_ANILINE): |
| return "aniline", "none", "none" |
| if mol.HasSubstructMatch(_PAT_NH): |
| return "heterocyclic", "morpholine", "none" |
| return "aniline", "none", "none" |
|
|
|
|
| |
|
|
| def _match_to_training(amine_smiles: str): |
| """ |
| Find the closest training compound by Morgan fingerprint Tanimoto similarity. |
| Returns (amine_type, substituent, position, match_type, similarity, matched_name). |
| match_type: 'exact' | 'similar' | 'inferred' |
| """ |
| can = _canonical(amine_smiles) |
| if can is None: |
| return None, None, None, "invalid", 0.0, "β" |
|
|
| |
| for at, sub, pos, db_can, db_fp, _ in _DB_INDEX: |
| if can == db_can: |
| label = f"{at} / {sub}" + (f" / {pos}" if pos != "none" else "") |
| return at, sub, pos, "exact", 1.0, label |
|
|
| |
| q_fp = _morgan_fp(can) |
| if q_fp is None: |
| at, sub, pos = _infer_amine_class(can) |
| return at, sub, pos, "inferred", 0.0, f"{at} (inferred)" |
|
|
| best_sim, best_at, best_sub, best_pos, best_label = -1.0, None, None, None, "β" |
| for at, sub, pos, db_can, db_fp, _ in _DB_INDEX: |
| sim = DataStructs.TanimotoSimilarity(q_fp, db_fp) |
| if sim > best_sim: |
| best_sim = sim |
| best_at, best_sub, best_pos = at, sub, pos |
| best_label = f"{at} / {sub}" + (f" / {pos}" if pos != "none" else "") |
|
|
| if best_sim >= 0.30: |
| return best_at, best_sub, best_pos, "similar", round(best_sim, 3), best_label |
|
|
| |
| at, sub, pos = _infer_amine_class(can) |
| return at, sub, pos, "inferred", round(best_sim, 3), f"{at} (inferred)" |
|
|
|
|
| |
|
|
| def _euclidean(a, b): |
| return math.sqrt(sum((x - y) ** 2 for x, y in zip(a, b))) |
|
|
|
|
| def _knn_predict(query_features, k=3): |
| """ |
| Inverse-distance weighted k-NN regressor over DATASET. |
| Returns (predicted_yield: float, neighbours: list). |
| """ |
| encoded = [] |
| for row in DATASET: |
| at, sub, pos, temp_i, cat_i, yld = row |
| feats = _encode_categorical( |
| at, sub, pos, |
| "reflux" if temp_i else "r.t.", |
| bool(cat_i), |
| ) |
| encoded.append((feats, yld, row)) |
|
|
| distances = [( |
| _euclidean(query_features, feats), yld, row |
| ) for feats, yld, row in encoded] |
| distances.sort(key=lambda x: x[0]) |
| neighbours = distances[:k] |
|
|
| total_w, weighted_sum = 0.0, 0.0 |
| for d, yld, _ in neighbours: |
| w = 1.0 / (d + 0.001) |
| weighted_sum += w * yld |
| total_w += w |
|
|
| return round(weighted_sum / total_w, 1), neighbours |
|
|
|
|
| |
|
|
| |
| |
| |
| _RXN_PRIMARY = AllChem.ReactionFromSmarts( |
| "[NH2:1].[c:2]1([O:6]CC)[c:3](OCC)[c:4](=O)[c:5]1=O" |
| ">>" |
| "[NH1:1][c:2]1[c:3](OCC)[c:4](=O)[c:5]1=O" |
| ) |
| _RXN_SECONDARY = AllChem.ReactionFromSmarts( |
| "[NH1:1].[c:2]1([O:6]CC)[c:3](OCC)[c:4](=O)[c:5]1=O" |
| ">>" |
| "[N:1][c:2]1[c:3](OCC)[c:4](=O)[c:5]1=O" |
| ) |
|
|
|
|
| def _compute_product_smiles(amine_smiles: str, squarate_smiles: str) -> str: |
| """ |
| Run a 2-component reaction SMARTS to produce the squaric acid monoamide. |
| Returns canonical product SMILES, or '' on failure. |
| """ |
| try: |
| amine_mol = Chem.MolFromSmiles(amine_smiles) |
| sq_mol = Chem.MolFromSmiles(squarate_smiles) |
| if amine_mol is None or sq_mol is None: |
| return "" |
|
|
| is_primary = amine_mol.HasSubstructMatch(Chem.MolFromSmarts("[NH2]")) |
| is_secondary = amine_mol.HasSubstructMatch(Chem.MolFromSmarts("[NH1]")) |
|
|
| rxn = _RXN_PRIMARY if is_primary else (_RXN_SECONDARY if is_secondary else None) |
| if rxn is None: |
| return "" |
|
|
| products = rxn.RunReactants((amine_mol, sq_mol)) |
| if not products: |
| return "" |
|
|
| prod_mol = products[0][0] |
| Chem.SanitizeMol(prod_mol) |
| return Chem.MolToSmiles(prod_mol) |
| except Exception: |
| return "" |
|
|
|
|
| |
|
|
| def _soft_warnings(amine_type, position, catalyst): |
| warnings = [] |
| if amine_type == "aniline" and not catalyst: |
| warnings.append( |
| "β οΈ Anilines typically require Zn(OTf)β catalyst " |
| "(10β13 mol%) for good yield." |
| ) |
| if position == "ortho": |
| warnings.append( |
| "β οΈ Ortho-substituted amines often give lower yields " |
| "due to steric hindrance." |
| ) |
| return " ".join(warnings) if warnings else None |
|
|
|
|
| |
|
|
| def _build_recommendation(amine_type, position, temperature, catalyst, predicted_yield): |
| tips = [] |
| if predicted_yield < 40: |
| if amine_type == "aniline" and not catalyst: |
| tips.append("Add Zn(OTf)β (10β13 mol%) to initiate reaction.") |
| if temperature != "reflux": |
| tips.append("Try heating to reflux to drive the reaction forward.") |
| if position == "ortho": |
| tips.append("Consider switching to para position for higher yield.") |
| elif predicted_yield >= 80: |
| tips.append("Conditions look optimal based on similar reactions in the training set.") |
| return " ".join(tips) if tips else "Conditions look reasonable." |
|
|
|
|
| |
|
|
| def predict_from_smiles( |
| amine_smiles: str, |
| squarate_smiles: str = "CCOC1=C(OCC)C(=O)C1=O", |
| temperature: str = "r.t.", |
| catalyst: bool = False, |
| ) -> dict: |
| """ |
| Predict reaction outcome given SMILES inputs. |
| |
| Args: |
| amine_smiles : SMILES of the amine coupling partner |
| squarate_smiles : SMILES of the squarate ester (default = diethyl squarate) |
| temperature : 'r.t.' or 'reflux' |
| catalyst : True if Zn(OTf)2 is added |
| |
| Returns dict with keys: |
| success, yield_percent, confidence, match_type, similarity, |
| amine_matched, product_smiles, message, warning, recommendation, |
| similar_reactions, error (optional) |
| """ |
|
|
| |
| amine_can = _canonical(amine_smiles) |
| if amine_can is None: |
| return { |
| "success": False, "yield_percent": 0.0, "confidence": "none", |
| "match_type": "invalid", "similarity": 0.0, "amine_matched": "β", |
| "product_smiles": "", "message": "β Invalid amine SMILES.", |
| "warning": None, "recommendation": "Check SMILES for typos.", |
| "similar_reactions": [], "error": "invalid_smiles", |
| } |
|
|
| |
| is_fail, fail_msg, fail_rec = _check_hard_fail(amine_can) |
| if is_fail: |
| return { |
| "success": False, "yield_percent": 0.0, "confidence": "high", |
| "match_type": "hard_fail", "similarity": 1.0, "amine_matched": amine_can, |
| "product_smiles": "", |
| "message": f"β {fail_msg}", |
| "warning": None, "recommendation": fail_rec, |
| "similar_reactions": [], |
| } |
|
|
| |
| at, sub, pos, match_type, similarity, matched_label = _match_to_training(amine_can) |
|
|
| |
| query_feats = _encode_categorical(at, sub, pos, temperature, catalyst) |
| predicted_yield, neighbours = _knn_predict(query_feats, k=3) |
|
|
| success = predicted_yield >= 20.0 |
|
|
| |
| nearest_dist = neighbours[0][0] |
| if match_type == "exact" and nearest_dist < 0.5: |
| confidence = "high" |
| elif nearest_dist < 1.5 and match_type in ("exact", "similar"): |
| confidence = "medium" |
| else: |
| confidence = "low (limited data for this combination)" |
|
|
| |
| product_smiles = "" |
| if success: |
| product_smiles = _compute_product_smiles(amine_can, squarate_smiles) |
|
|
| |
| warning = _soft_warnings(at, pos, catalyst) |
| recommendation = _build_recommendation(at, pos, temperature, catalyst, predicted_yield) |
|
|
| icon = "β
" if success else "β" |
| message = ( |
| f"{icon} Predicted yield: {predicted_yield}% " |
| f"(confidence: {confidence})" |
| ) |
|
|
| similar = [ |
| { |
| "amine": r[0], |
| "substituent": r[1], |
| "position": r[2], |
| "temperature": "reflux" if r[3] else "r.t.", |
| "catalyst": bool(r[4]), |
| "yield": r[5], |
| } |
| for _, _, r in neighbours |
| ] |
|
|
| return { |
| "success": success, |
| "yield_percent": predicted_yield, |
| "confidence": confidence, |
| "match_type": match_type, |
| "similarity": similarity, |
| "amine_matched": matched_label, |
| "product_smiles": product_smiles, |
| "message": message, |
| "warning": warning, |
| "recommendation": recommendation, |
| "similar_reactions": similar, |
| } |
|
|
|
|
| |
|
|
| _LEGACY_AMINE_MAP = {"aniline": 0, "benzylamine": 1} |
| _LEGACY_POS_MAP = {"ortho": 0, "meta": 1, "para": 2, "none": 2} |
| _LEGACY_SUB_MAP = { |
| "none": 0, "F": 1, "Cl": 2, "Br": 3, "OH": 4, |
| "OMe": 5, "CF3": 6, "tolyl": 7, "fused_ring": 8, |
| "pyrrolidine": 9, "azetidine": 10, "piperidine": 11, "azepane": 12, |
| "thiazolidine": 13, "morpholine": 14, "dimethylmorpholine": 15, |
| "thiomorpholine": 16, "thiomorpholine_dioxide": 17, |
| "boc_piperazine": 18, "boc_dimethylpiperazine": 19, |
| "bromo_tetrahydroquinoline": 20, "piperazine": 21, |
| } |
|
|
| _LEGACY_HARD_FAILS = { |
| ("aniline", "Cl", "ortho"): ( |
| "Ortho-Cl aniline fails due to steric crowding. No reaction even at reflux.", |
| "Change position to meta or para.", |
| ), |
| ("aniline", "Br", "ortho"): ( |
| "Ortho-Br aniline fails due to steric crowding. No reaction even at reflux.", |
| "Change position to meta or para.", |
| ), |
| ("heterocyclic", "piperazine", "none"): ( |
| "Unprotected piperazine fails β both N atoms compete.", |
| "Use Boc-protected piperazine instead.", |
| ), |
| } |
|
|
|
|
| def predict(amine_type: str, substituent: str, position: str, |
| temperature: str, catalyst: bool) -> dict: |
| """ |
| Legacy predict() β accepts dropdown-encoded inputs. |
| Kept for backward compatibility with the React Native mobile app. |
| """ |
| key = (amine_type, substituent, position) |
| if key in _LEGACY_HARD_FAILS: |
| msg, rec = _LEGACY_HARD_FAILS[key] |
| return { |
| "success": False, "yield_percent": 0.0, "confidence": "high", |
| "message": f"β {msg}", "warning": None, |
| "recommendation": rec, "similar_reactions": [], |
| } |
|
|
| warning = None |
| if amine_type == "aniline" and not catalyst: |
| warning = "β οΈ Anilines typically require Zn(OTf)β catalyst." |
|
|
| query = [ |
| _LEGACY_AMINE_MAP.get(amine_type, 0), |
| _LEGACY_SUB_MAP.get(substituent, 0), |
| _LEGACY_POS_MAP.get(position, 2), |
| int(temperature == "reflux"), |
| int(bool(catalyst)), |
| ] |
| predicted_yield, neighbours = _knn_predict(query, k=3) |
| success = predicted_yield >= 20.0 |
|
|
| nearest_dist = neighbours[0][0] |
| if nearest_dist < 0.5: |
| confidence = "high" |
| elif nearest_dist < 1.5: |
| confidence = "medium" |
| else: |
| confidence = "low (limited data for this combination)" |
|
|
| icon = "β
" if success else "β" |
| similar = [ |
| {"amine": r[0], "substituent": r[1], "position": r[2], "yield": r[5]} |
| for _, _, r in neighbours |
| ] |
|
|
| return { |
| "success": success, |
| "yield_percent": predicted_yield, |
| "confidence": confidence, |
| "message": f"{icon} Predicted yield: {predicted_yield}% (confidence: {confidence})", |
| "warning": warning, |
| "recommendation": _build_recommendation(amine_type, position, temperature, catalyst, predicted_yield), |
| "similar_reactions": similar, |
| } |
|
|