pino-source-code / scripts /audit_unifac_groups.py
mattbitzesty's picture
feat(scripts): add UNIFAC subgroup audit tool for registry compounds
212527a unverified
Raw
History Blame Contribute Delete
4.96 kB
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Any
from rdkit import Chem
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from pino.thermo.unifac import DOUFSG_AVAILABLE_GROUPS, get_dortmund_subgroups
def audit_registry_thermodynamics(registry_path: str | Path) -> dict[str, Any]:
"""Scan a JSON-Lines registry and flag compounds that cannot be represented in DOUFSG."""
print("=== Beginning Comprehensive Dortmund UNIFAC (DOUFSG) Group Audit ===")
with Path(registry_path).open("r", encoding="utf-8") as f:
compounds = [json.loads(line) for line in f]
unifac_failures = []
total = len(compounds)
inspected = 0
for c in compounds:
cas = c.get("cas", "")
smiles = c.get("smiles", "")
name = c.get("name", "Unknown")
if not smiles:
unifac_failures.append({
"cas": cas,
"name": name,
"smiles": smiles,
"reason": "Missing SMILES string",
"missing_subgroup_ids": [],
})
continue
try:
mol = Chem.MolFromSmiles(smiles)
if mol is None:
raise ValueError("Invalid SMILES structure")
molecule_subgroups = get_dortmund_subgroups(smiles)
inspected += 1
# Check if all discovered structural fragments exist in the DOUFSG parameter set
missing_in_doufsg = [
int(gid) for gid in molecule_subgroups.keys()
if int(gid) not in DOUFSG_AVAILABLE_GROUPS
]
if missing_in_doufsg:
unifac_failures.append({
"cas": cas,
"name": name,
"smiles": smiles,
"reason": f"Subgroup IDs {missing_in_doufsg} not present in active DOUFSG definitions",
"missing_subgroup_ids": missing_in_doufsg,
})
if not molecule_subgroups:
# Either fragmentation failed or every subgroup was missing.
# Re-check by trying a raw ugropy call to report the missing subgroup names.
reason = "UNIFAC fragmentation returned no usable DOUFSG subgroups"
try:
import ugropy
raw_groups = ugropy.Groups(smiles, identifier_type="smiles").dortmund.subgroups
unknown = [
name for name in raw_groups.keys()
if int(getattr(name, "id", 0)) not in DOUFSG_AVAILABLE_GROUPS
]
if unknown:
reason = f"ugropy groups {unknown} have no matching DOUFSG parameter"
except Exception:
pass
unifac_failures.append({
"cas": cas,
"name": name,
"smiles": smiles,
"reason": reason,
"missing_subgroup_ids": [],
})
except Exception as e:
unifac_failures.append({
"cas": cas,
"name": name,
"smiles": smiles,
"reason": f"UNIFAC fragmentation error: {str(e)}",
"missing_subgroup_ids": [],
})
print(f"\nAudit complete. Inspected {total} materials.")
print(f"Successfully derived DOUFSG subgroups for {inspected} materials.")
if unifac_failures:
print(f"⚠️ Found {len(unifac_failures)} thermodynamic integration blockers:")
for fail in unifac_failures[:10]:
print(f" - CAS [{fail['cas']}] ({fail['name']}): {fail['reason']}")
if len(unifac_failures) > 10:
print(f" ... and {len(unifac_failures) - 10} more")
else:
print("✅ SUCCESS: 100% of registry compounds have valid, un-fragmented DOUFSG definitions.")
return {
"total": total,
"inspected": inspected,
"failures": unifac_failures,
}
def main() -> int:
parser = argparse.ArgumentParser(description="Audit registry compounds against DOUFSG")
parser.add_argument(
"--registry",
default="data/aroma_chemicals.jsonl",
help="Path to the JSON-Lines registry file",
)
parser.add_argument(
"--output",
default="data/unifac_missing_groups.json",
help="Path to write the failure report",
)
args = parser.parse_args()
result = audit_registry_thermodynamics(args.registry)
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps(result, indent=2, ensure_ascii=False), encoding="utf-8")
print(f"\nBlockers exported to {output_path}.")
return 0 if not result["failures"] else 1
if __name__ == "__main__":
raise SystemExit(main())