Spaces:
Sleeping
Sleeping
| """Safety rule checks: metal blacklist + PMT pre-filter.""" | |
| from pathlib import Path | |
| import yaml | |
| from rdkit import Chem | |
| from rdkit.Chem import Descriptors | |
| def _load_metals_config() -> dict: | |
| config_path = Path(__file__).resolve().parent.parent / "configs" / "metals.yaml" | |
| with open(config_path, "r", encoding="utf-8") as f: | |
| return yaml.safe_load(f) | |
| def _load_pmt_config() -> dict: | |
| config_path = Path(__file__).resolve().parent.parent / "configs" / "thresholds.yaml" | |
| with open(config_path, "r", encoding="utf-8") as f: | |
| return yaml.safe_load(f)["pmt_criteria"] | |
| def check_metal_safety(metals: list[str]) -> dict: | |
| """Check metal nodes against black/grey/white list. | |
| Args: | |
| metals: list of element symbols (e.g. ["Zn", "Cu"]). | |
| Returns: | |
| { | |
| "tier": "white" | "grey" | "black" | "unknown", | |
| "flagged_metals": list[str], | |
| "details": str, | |
| } | |
| """ | |
| cfg = _load_metals_config() | |
| black = set(cfg.get("black_list", [])) | |
| grey = set(cfg.get("grey_list", [])) | |
| white = set(cfg.get("white_list", [])) | |
| flagged = [m for m in metals if m in black] | |
| if flagged: | |
| return { | |
| "tier": "black", | |
| "flagged_metals": flagged, | |
| "details": f"Blacklisted metal(s): {', '.join(flagged)}. Hard reject.", | |
| } | |
| grey_found = [m for m in metals if m in grey] | |
| if grey_found: | |
| return { | |
| "tier": "grey", | |
| "flagged_metals": grey_found, | |
| "details": f"Grey-list metal(s): {', '.join(grey_found)}. Use with caution.", | |
| } | |
| white_found = [m for m in metals if m in white] | |
| unknown = [m for m in metals if m not in white] | |
| if unknown: | |
| return { | |
| "tier": "unknown", | |
| "flagged_metals": unknown, | |
| "details": f"Unknown metal(s): {', '.join(unknown)}. Not in any list.", | |
| } | |
| return { | |
| "tier": "white", | |
| "flagged_metals": [], | |
| "details": f"All metals ({', '.join(metals)}) are on the white list.", | |
| } | |
| def check_pmt_pre_filter(linker_smiles: str | None) -> dict: | |
| """Screen linker SMILES against ECHA PMT criteria. | |
| Args: | |
| linker_smiles: SMILES string or None. | |
| Returns: | |
| { | |
| "pmt_pass": bool, | |
| "flags": list[str], | |
| "descriptors": dict | None, | |
| } | |
| """ | |
| if linker_smiles is None: | |
| return { | |
| "pmt_pass": True, | |
| "flags": ["No linker SMILES available; PMT check skipped."], | |
| "descriptors": None, | |
| } | |
| mol = Chem.MolFromSmiles(linker_smiles) | |
| if mol is None: | |
| return { | |
| "pmt_pass": False, | |
| "flags": [f"Invalid SMILES: {linker_smiles}"], | |
| "descriptors": None, | |
| } | |
| pmt = _load_pmt_config() | |
| logp = Descriptors.MolLogP(mol) | |
| mw = Descriptors.MolWt(mol) | |
| hbd = Descriptors.NumHDonors(mol) | |
| hba = Descriptors.NumHAcceptors(mol) | |
| descriptors = { | |
| "logP": round(logp, 2), | |
| "MW": round(mw, 2), | |
| "HBD": hbd, | |
| "HBA": hba, | |
| } | |
| flags = [] | |
| if logp > pmt["logP_max"]: | |
| flags.append(f"logP={logp:.2f} > {pmt['logP_max']} (bioaccumulation risk)") | |
| if mw > pmt["mw_max"]: | |
| flags.append(f"MW={mw:.1f} > {pmt['mw_max']} (persistence concern)") | |
| if hbd > pmt["hbd_max"]: | |
| flags.append(f"HBD={hbd} > {pmt['hbd_max']}") | |
| if hba > pmt["hba_max"]: | |
| flags.append(f"HBA={hba} > {pmt['hba_max']}") | |
| # PFAS check: presence of C-F bonds | |
| has_cf = any( | |
| bond.GetBeginAtom().GetSymbol() == "F" or bond.GetEndAtom().GetSymbol() == "F" | |
| for bond in mol.GetBonds() | |
| ) | |
| if has_cf: | |
| flags.append("Contains C-F bonds (potential PFAS concern)") | |
| return { | |
| "pmt_pass": len(flags) == 0, | |
| "flags": flags if flags else ["All PMT criteria passed."], | |
| "descriptors": descriptors, | |
| } | |