Spaces:
Sleeping
Sleeping
File size: 3,926 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 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | """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,
}
|