Spaces:
Sleeping
Sleeping
File size: 3,325 Bytes
bc2a98e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | """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
|