| 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 |
|
|
| |
| 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: |
| |
| |
| 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()) |
|
|