Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions news/cluster-mining.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
**Added:**

* Add tools to generate bounded libraries of finite metallic clusters
using ASE and screen them against experimental PDFs through the MCP
server, reusing the existing ``RefinementSession`` infrastructure
for individual refinements.
1 change: 1 addition & 0 deletions requirements/conda.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ diffpy.srreal
diffpy.structure
networkx
mcp[cli]
ase
1 change: 1 addition & 0 deletions requirements/pip.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ase
111 changes: 110 additions & 1 deletion src/diffpy/apps/parametric_refinement/refinement_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import json
import logging
from functools import wraps
from typing import Annotated, Literal
from typing import Annotated, Any, Literal

from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
Expand Down Expand Up @@ -848,5 +848,114 @@ async def list_running_jobs() -> list[dict]:
return session.list_running_jobs()


# Cluster screening tools use an in-memory library specification.
# No structures or cross-call session state are persisted.
@mcp.tool()
@tool_errors
async def generate_cluster_library(
element: str,
min_atoms: int,
max_atoms: int,
max_clusters: int = 20,
candidate_specs: list[dict] | None = None,
motif_ranges: dict | None = None,
lattice_constant: float | None = None,
) -> dict[str, Any]:
"""Preview a small ASE cluster library without writing XYZ files.

Pass exactly one of candidate_specs (individual builder
combinations) or motif_ranges (small Cartesian-product grids). The
maximum number of requested combinations is enforced before
generation.
"""
from diffpy.apps.screening.cluster import (
generate_cluster_library as build_library,
)

library = build_library(
element,
min_atoms=min_atoms,
max_atoms=max_atoms,
max_clusters=max_clusters,
candidate_specs=candidate_specs,
motif_ranges=motif_ranges,
lattice_constant=lattice_constant,
)
# The MCP response is JSON only. ASE Atoms stay local to Python.
return {
"library_spec": {
"element": library["element"],
"min_atoms": library["min_atoms"],
"max_atoms": library["max_atoms"],
"max_clusters": library["max_clusters"],
"lattice_constant": library["lattice_constant_angstrom"],
"candidate_specs": [
{"motif": item["motif"], **item["parameters"]}
for item in library["requested_specs"]
],
},
"ase_version": library["ase_version"],
"models": [
{key: value for key, value in item.items() if key != "atoms"}
for item in library["models"]
],
"skipped": library["skipped"],
}


@mcp.tool()
@tool_errors
async def screen_cluster_library(
preview: dict,
pdf_path: str,
refinement: dict,
) -> dict[str, Any]:
"""Refine candidates from a validated library preview, then rank by
Rw.

The candidates are reconstructed in memory from the preview's exact
ASE builder parameters. No XYZ files or persistent library state are
required. The same single-stage refinement is used for every model.
"""
from diffpy.apps.screening.cluster import (
generate_cluster_library as build_library,
)
from diffpy.apps.screening.cluster_screen import (
screen_cluster_library as run_screen,
)

if not isinstance(preview, dict):
raise ValueError("preview must be a library preview dictionary.")
spec = preview.get("library_spec")
if not isinstance(spec, dict):
raise ValueError(
"The preview has no library_spec. "
"Call generate_cluster_library first."
)
library = build_library(**spec)
actual_models = [
{key: value for key, value in item.items() if key != "atoms"}
for item in library["models"]
]
if (
preview.get("ase_version") != library["ase_version"]
or preview.get("models") != actual_models
or preview.get("skipped") != library["skipped"]
):
raise ValueError(
"The preview no longer matches the generated structures. "
"Generate a new library preview before screening."
)
result = run_screen(
library,
pdf_path,
refinement,
max_clusters=spec["max_clusters"],
)
result["library_spec"] = spec
result["ase_version"] = library["ase_version"]
return result


if __name__ == "__main__":
mcp.run()
1 change: 1 addition & 0 deletions src/diffpy/apps/screening/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Reusable structure-screening operations for DiffPy applications."""
Loading
Loading