chemgraph-loop / cg_extract.py
rockyaaos's picture
clean calculator label for live runs (parse type/method from repr)
1dd0b17 verified
Raw
History Blame Contribute Delete
8.01 kB
"""Shared extraction: turn a ChemGraph agent `state` into the rich payload the
site renders. Used by both app.py (live) and precompute.py (cached heavy runs)
so the shape never diverges."""
from __future__ import annotations
import os
import re
from typing import Optional
# ---- element data for the client (kept here so formula/labels are consistent) ----
def parse_xyz(path: Optional[str]) -> dict:
"""Read an .xyz file → {atoms:[{el,x,y,z}], natoms}. Empty dict on failure."""
if not path or not os.path.exists(path):
return {}
try:
with open(path, "r", encoding="utf-8") as f:
lines = f.read().splitlines()
n = int(lines[0].strip())
atoms = []
for ln in lines[2 : 2 + n]:
parts = ln.split()
if len(parts) < 4:
continue
el = parts[0]
x, y, z = (float(parts[1]), float(parts[2]), float(parts[3]))
atoms.append({"el": el, "x": round(x, 4), "y": round(y, 4), "z": round(z, 4)})
return {"atoms": atoms, "natoms": len(atoms)}
except Exception:
return {}
def _num(x):
try:
return float(x)
except Exception:
return None
def _clean_freqs(raw) -> list:
"""vib frequencies come as strings like '1592.3' or '-42.1i'. Keep real,
drop translation/rotation near-zeros, round."""
out = []
for v in raw or []:
s = str(v)
imag = s.endswith("i")
val = _num(s[:-1] if imag else s)
if val is None:
continue
if imag:
out.append({"cm1": round(-abs(val), 1), "imaginary": True})
elif abs(val) > 1.0: # drop ~0 modes (translations/rotations)
out.append({"cm1": round(val, 1), "imaginary": False})
return out
def _summarize(tool: str, out: dict, task: str) -> str:
if not isinstance(out, dict):
return str(out)[:140]
if tool == "molecule_name_to_smiles":
return f"{out.get('resolved_name', out.get('name',''))} → SMILES {out.get('smiles','?')} · {out.get('molecular_formula','')} (PubChem)"
if tool == "smiles_to_coordinate_file":
return f"built 3D structure · {out.get('natoms','?')} atoms"
if tool == "run_ase":
e = out.get("single_point_energy")
if isinstance(e, (int, float)):
return f"simulation success · energy {e:.4f} eV"
if out.get("dipole_moment"):
return "simulation success · dipole computed"
if isinstance(out.get("result"), dict):
r = out["result"]
if "ir_spectrum" in r:
return "vibrational + IR analysis success"
if "vibrational_frequencies" in r:
return "vibrational analysis success"
if "thermochemistry" in r:
return "thermochemistry computed"
return str(out.get("status", "done"))
return str(out.get("status", ""))[:140]
def _detail(tool: str, args: dict, out: dict) -> dict:
"""Richer, foldable per-step detail (SMILES, formula, args, coords count)."""
d = {}
if tool == "molecule_name_to_smiles" and isinstance(out, dict):
d = {"name": out.get("name"), "resolved": out.get("resolved_name"),
"smiles": out.get("smiles"), "formula": out.get("molecular_formula"),
"cid": out.get("cid"), "source": out.get("source")}
elif tool == "smiles_to_coordinate_file" and isinstance(out, dict):
d = {"smiles": out.get("smiles"), "natoms": out.get("natoms")}
elif tool == "run_ase":
params = (args or {}).get("params", {}) if isinstance(args, dict) else {}
calc = params.get("calculator")
calc_s = method = None
if isinstance(calc, dict):
calc_s = calc.get("calculator_type")
method = calc.get("method")
elif isinstance(calc, str):
# live runs pass the full repr, e.g. "calculator_type='TBLite' method='GFN2-xTB' ...";
# keep just the type + method, not the whole settings dump.
m = re.search(r"calculator_type=['\"]?([\w-]+)", calc)
calc_s = m.group(1) if m else calc[:24]
mm = re.search(r"method=['\"]?([\w-]+)", calc)
method = mm.group(1) if mm else None
d = {"driver": params.get("driver"), "calculator": calc_s, "method": method}
return {k: v for k, v in d.items() if v is not None}
def extract(state: dict, query: str, molecule: str, calc_label: str, task: str,
cached: bool = False) -> dict:
intent = state.get("intent", {}) or {}
trace = state.get("tool_trace", []) or []
smiles = formula = resolved = None
xyz_path = None
steps = []
run_out = {}
for s in trace:
tool = s.get("tool")
args = s.get("args", {}) or {}
out = s.get("output", {}) or {}
steps.append({
"tool": tool,
"status": s.get("status"),
"summary": _summarize(tool, out, task),
"detail": _detail(tool, args, out),
})
if tool == "molecule_name_to_smiles" and isinstance(out, dict):
smiles = out.get("smiles") or smiles
formula = out.get("molecular_formula") or formula
resolved = out.get("resolved_name") or resolved
if tool == "smiles_to_coordinate_file" and isinstance(out, dict):
xyz_path = out.get("path") or xyz_path
if tool == "run_ase":
run_out = out if isinstance(out, dict) else {}
p = (args.get("params", {}) or {})
xyz_path = xyz_path or p.get("input_structure_file")
structure = parse_xyz(xyz_path)
# ---- task-specific result ----
result = {"kind": task}
e = run_out.get("single_point_energy")
if isinstance(e, (int, float)):
result["energy"] = {"value": e, "unit": run_out.get("unit", "eV")}
if run_out.get("dipole_moment"):
vec = [round(_num(x) or 0.0, 4) for x in run_out["dipole_moment"]]
mag = round(sum(v * v for v in vec) ** 0.5, 4)
result["dipole"] = {"vector": vec, "magnitude": mag,
"unit": run_out.get("dipole_unit", "e·Å")}
res = run_out.get("result")
if isinstance(res, dict):
vf = res.get("vibrational_frequencies")
if isinstance(vf, dict):
result["frequencies"] = _clean_freqs(vf.get("frequencies"))
irs = res.get("ir_spectrum")
if isinstance(irs, dict):
freqs = [(_num(x) or 0.0) for x in irs.get("frequency_cm1", [])]
ints = [(_num(x) or 0.0) for x in irs.get("intensity", [])]
modes = [{"cm1": round(f, 1), "intensity": round(i, 4)}
for f, i in zip(freqs, ints) if f > 1.0]
result["ir"] = {"modes": modes}
th = res.get("thermochemistry")
if isinstance(th, dict):
result["thermo"] = {
"enthalpy": _num(th.get("enthalpy")),
"entropy": _num(th.get("entropy")),
"gibbs": _num(th.get("gibbs_free_energy")),
"unit": th.get("unit", "eV"),
}
validation = state.get("validation", {}) or {}
v_ok = validation.get("passed", validation.get("ok")) if isinstance(validation, dict) else None
has_result = any(k in result for k in ("energy", "dipole", "frequencies", "ir", "thermo"))
return {
"ok": True,
"cached": cached,
"query": query,
"molecule": molecule,
"formula": formula,
"smiles": smiles,
"resolved": resolved,
"calculator": calc_label,
"task": task,
"intent": {
"task_type": intent.get("task_type"),
"required_output": intent.get("required_output"),
"calculator": intent.get("calculator"),
"target_molecules": intent.get("target_molecules"),
},
"structure": structure,
"steps": steps,
"result": result,
"verified": bool(v_ok) if v_ok is not None else has_result,
}