mofscreen-agent-api / tools /safety_rules.py
andy88836's picture
Deploy MOFScreen-Agent FastAPI backend
bc2a98e verified
Raw
History Blame Contribute Delete
3.93 kB
"""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,
}