Spaces:
Sleeping
Sleeping
| """Aquatic toxicity prediction using trained RF + Morgan fingerprint models.""" | |
| from pathlib import Path | |
| import joblib | |
| import numpy as np | |
| import yaml | |
| from rdkit import Chem | |
| from rdkit.Chem import AllChem, Descriptors | |
| def _load_config() -> dict: | |
| config_path = Path(__file__).resolve().parent.parent / "configs" / "paths.yaml" | |
| with open(config_path, "r", encoding="utf-8") as f: | |
| return yaml.safe_load(f) | |
| def _smiles_to_features(smiles: str, radius: int = 2, n_bits: int = 2048) -> np.ndarray: | |
| mol = Chem.MolFromSmiles(smiles) | |
| if mol is None: | |
| raise ValueError(f"Invalid SMILES: {smiles}") | |
| fp = AllChem.GetMorganFingerprintAsBitVect(mol, radius=radius, nBits=n_bits) | |
| fp_arr = np.array(fp, dtype=np.float32) | |
| desc = [ | |
| Descriptors.MolLogP(mol), Descriptors.MolWt(mol), Descriptors.TPSA(mol), | |
| Descriptors.NumHDonors(mol), Descriptors.NumHAcceptors(mol), | |
| Descriptors.NumRotatableBonds(mol), Descriptors.HeavyAtomCount(mol), | |
| ] | |
| return np.concatenate([fp_arr, np.array(desc, dtype=np.float32)]) | |
| _ENDPOINT_MAP = { | |
| "LC50": "rf_lc50.pkl", | |
| "LC50DM": "rf_lc50dm.pkl", | |
| "IGC50": "rf_igc50.pkl", | |
| "IBC50": "rf_ibc50.pkl", | |
| } | |
| _UNIT = "-log10(mol/L)" | |
| _CONFIDENCE = { | |
| "LC50": "high", | |
| "LC50DM": "medium", | |
| "IGC50": "high", | |
| "IBC50": "low", | |
| } | |
| def predict_toxicity(linker_smiles: str | None) -> dict: | |
| """Predict aquatic toxicity for 4 endpoints from a linker SMILES. | |
| Args: | |
| linker_smiles: SMILES string or None (Level 3 linker extraction). | |
| Returns: | |
| dict with keys: LC50_Pimephales, LC50_Daphnia, IGC50_Tetrahymena, | |
| IBC50_Vibrio (float|None), plus _unit and _confidence for each, | |
| and applicability_domain_flag (bool). | |
| Values are -log10(mol/L); higher = more toxic. | |
| """ | |
| result = { | |
| "LC50_Pimephales": None, "LC50_Pimephales_unit": _UNIT, "LC50_Pimephales_confidence": None, | |
| "LC50_Daphnia": None, "LC50_Daphnia_unit": _UNIT, "LC50_Daphnia_confidence": None, | |
| "IGC50_Tetrahymena": None, "IGC50_Tetrahymena_unit": _UNIT, "IGC50_Tetrahymena_confidence": None, | |
| "IBC50_Vibrio": None, "IBC50_Vibrio_unit": _UNIT, "IBC50_Vibrio_confidence": None, | |
| "applicability_domain_flag": False, | |
| } | |
| if linker_smiles is None: | |
| return result | |
| cfg = _load_config() | |
| model_dir = Path(cfg["toxicity_models"]["model_dir"]) | |
| fp_cfg = cfg["toxicity_models"]["fingerprint"] | |
| try: | |
| features = _smiles_to_features( | |
| linker_smiles, radius=fp_cfg["radius"], n_bits=fp_cfg["n_bits"] | |
| ) | |
| except ValueError: | |
| return result | |
| X = features.reshape(1, -1) | |
| key_map = { | |
| "LC50": "LC50_Pimephales", | |
| "LC50DM": "LC50_Daphnia", | |
| "IGC50": "IGC50_Tetrahymena", | |
| "IBC50": "IBC50_Vibrio", | |
| } | |
| all_ok = True | |
| for endpoint, filename in _ENDPOINT_MAP.items(): | |
| model_path = model_dir / filename | |
| if not model_path.exists(): | |
| all_ok = False | |
| continue | |
| model = joblib.load(str(model_path)) | |
| pred = float(model.predict(X)[0]) | |
| out_key = key_map[endpoint] | |
| result[out_key] = round(pred, 4) | |
| result[f"{out_key}_confidence"] = _CONFIDENCE[endpoint] | |
| result["applicability_domain_flag"] = all_ok | |
| return result | |