From f245498bc6296c606fdf075574876b859389cd74 Mon Sep 17 00:00:00 2001 From: ahxbcn <2092233074@qq.com> Date: Thu, 12 Feb 2026 12:11:51 +0800 Subject: [PATCH 01/10] feat: add materials project download functionality to submodules --- src/abacusagent/modules/submodules/abacus.py | 138 ++++++++++++++----- 1 file changed, 103 insertions(+), 35 deletions(-) diff --git a/src/abacusagent/modules/submodules/abacus.py b/src/abacusagent/modules/submodules/abacus.py index 851843f..0823698 100644 --- a/src/abacusagent/modules/submodules/abacus.py +++ b/src/abacusagent/modules/submodules/abacus.py @@ -12,6 +12,9 @@ from abacusagent.init_mcp import mcp from abacusagent.modules.util.comm import generate_work_path, run_abacus, collect_metrics +import tempfile +from pymatgen.ext.matproj import MPRester +from pymatgen.core import Structure def abacus_prepare( stru_file: Path, @@ -573,44 +576,109 @@ def read_abacus_input_kpt( except Exception as e: return {'message': f"Read ABACUS INPUT file failed: {e}"} -def read_abacus_stru(abacus_input_dir: Path): +@mcp.tool() +def materials_project_download( + material_id: str, + destination_path: Optional[str] = None, + format: Literal["cif", "poscar", "stru"] = "cif" +) -> Dict[str, Any]: """ - Read ABACUS STRU file. + Download structure from Materials Project database by material ID. + Args: - abacus_input_dir (str): Path to the directory containing the ABACUS input files. + material_id (str): The Materials Project material ID (e.g., 'mp-12345') + destination_path (str, optional): The path to save the downloaded structure file. + If not provided, a temporary file will be created. + format (Literal["cif", "poscar", "stru"] = "cif"): The format to save the structure file. + Can be 'cif', 'poscar', or 'stru'. + Returns: - A dictionary containing information from the STRU file. Containing the following keys: - cell: the cell of the structure - atom_kinds: a dict, keys are atom labels, values are dicts containing the following keys: - pp: the pseudopotential file name - orb: the orbital file name - element: the element name - number: the number of atoms with this label - atommag: the magnetic moment of each atom with this label - coord: the coordinates of each atom - move: the movable flags of each atom - Raises: - FileNotFoundError: If path of given STRU file does not exist + A dictionary containing: + - 'structure_file': Path to the downloaded structure file. + - 'material_id': The material ID used for download. + - 'format': The format of the downloaded structure. """ try: - input_params = ReadInput(os.path.join(abacus_input_dir, "INPUT")) - stru_file = os.path.join(abacus_input_dir, input_params.get('stru_file', "STRU")) - if not os.path.isfile(stru_file): - raise FileNotFoundError(f"STRU file {stru_file} does not exist.") - - stru = AbacusStru.ReadStru(stru_file) - atom_kinds = {} - for idx, label in enumerate(stru.get_label(total=False)): - atom_kinds[label] = { - 'pp': stru.get_pp()[idx], - 'orb': stru.get_orb()[idx] if stru.get_orb() is not None else None, - 'element': stru.get_element(number=False,total=False)[idx], - 'number': stru.get_label().count(label), - 'atommag': stru.get_atommag()[idx], - } - return {'cell': stru.get_cell(), - 'atom_kinds': atom_kinds, - 'coord': stru.get_coord(), - 'move': stru.get_move()} + # Get the Materials Project API key from environment variables + api_key = os.environ.get("MP_API_KEY") + if not api_key: + raise ValueError("Materials Project API key not found. Please set MP_API_KEY environment variable.") + + # Connect to Materials Project database + with MPRester(api_key) as mpr: + # Retrieve the structure + structure = mpr.get_structure_by_material_id(material_id) + + # Determine destination path + if destination_path is None: + # Create a temporary file + temp_file = tempfile.NamedTemporaryFile(suffix=f".{format}", delete=False) + destination_path = temp_file.name + temp_file.close() + + # Save structure in specified format + if format == "cif": + structure.to(filename=destination_path, fmt="cif") + elif format == "poscar": + structure.to(filename=destination_path, fmt="poscar") + elif format == "stru": + # For stru format, we need to convert to ABACUS format + stru_content = _structure_to_abacus_stru(structure) + with open(destination_path, 'w') as f: + f.write(stru_content) + else: + raise ValueError(f"Unsupported format: {format}") + + return { + "structure_file": destination_path, + "material_id": material_id, + "format": format + } + except Exception as e: - return {'message': f"Read ABACUS STRU file failed: {e}"} + return {"message": f"Failed to download structure from Materials Project: {e}"} + +def _structure_to_abacus_stru(structure: Structure) -> str: + """ + Convert a pymatgen Structure to ABACUS STRU format. + + Args: + structure (Structure): The pymatgen Structure object + + Returns: + str: The ABACUS STRU formatted string + """ + # Start building the STRU content + stru_lines = [] + + # Title + stru_lines.append("ATOMIC_STRUCTURE") + + # Cell parameters + lattice = structure.lattice + stru_lines.append(f"{lattice.a:.10f} {lattice.b:.10f} {lattice.c:.10f}") + + # Lattice vectors (in fractional coordinates) + stru_lines.append("0.0 0.0 0.0") + stru_lines.append("0.0 0.0 0.0") + stru_lines.append("0.0 0.0 0.0") + + # Atom kinds and coordinates + # Group atoms by element + element_counts = {} + for site in structure: + element = site.specie.symbol + element_counts[element] = element_counts.get(element, 0) + 1 + + # Write atom kinds + for element, count in element_counts.items(): + stru_lines.append(f"{element} {count}") + + # Write coordinates in cartesian format + stru_lines.append("CART") + for site in structure: + element = site.specie.symbol + coord = site.coords + stru_lines.append(f"{element} {coord[0]:.10f} {coord[1]:.10f} {coord[2]:.10f} 1 1 1") + + return "\n".join(stru_lines) From 761921a481e5cb4f7496af71e86d0b56b7459996 Mon Sep 17 00:00:00 2001 From: ahxbcn <2092233074@qq.com> Date: Thu, 12 Feb 2026 12:13:46 +0800 Subject: [PATCH 02/10] feat: add materials project download functionality to structure_generator module --- .../modules/structure_generator.py | 125 +++++++++++++++++- 1 file changed, 118 insertions(+), 7 deletions(-) diff --git a/src/abacusagent/modules/structure_generator.py b/src/abacusagent/modules/structure_generator.py index cd5eb6c..2d77148 100644 --- a/src/abacusagent/modules/structure_generator.py +++ b/src/abacusagent/modules/structure_generator.py @@ -6,16 +6,127 @@ from abacusagent.modules.submodules.structure_generator import generate_molecule_structure as _generate_molecule_structure from abacusagent.modules.submodules.structure_generator import generate_bulk_structure_from_wyckoff_position as _generate_bulk_structure_from_wyckoff_position from abacusagent.modules.submodules.structure_generator import get_ieee_standard_structure as _get_ieee_standard_structure +import tempfile +import os +from pymatgen.ext.matproj import MPRester +from pymatgen.core import Structure + +@mcp.tool() +def materials_project_download( + material_id: str, + destination_path: Optional[str] = None, + format: Literal["cif", "poscar", "stru"] = "cif" +) -> Dict[str, Any]: + """ + Download structure from Materials Project database by material ID. + + Args: + material_id (str): The Materials Project material ID (e.g., 'mp-12345') + destination_path (str, optional): The path to save the downloaded structure file. + If not provided, a temporary file will be created. + format (Literal["cif", "poscar", "stru"] = "cif"): The format to save the structure file. + Can be 'cif', 'poscar', or 'stru'. + + Returns: + A dictionary containing: + - 'structure_file': Path to the downloaded structure file. + - 'material_id': The material ID used for download. + - 'format': The format of the downloaded structure. + """ + try: + # Get the Materials Project API key from environment variables + api_key = os.environ.get("MP_API_KEY") + if not api_key: + raise ValueError("Materials Project API key not found. Please set MP_API_KEY environment variable.") + + # Connect to Materials Project database + with MPRester(api_key) as mpr: + # Retrieve the structure + structure = mpr.get_structure_by_material_id(material_id) + + # Determine destination path + if destination_path is None: + # Create a temporary file + temp_file = tempfile.NamedTemporaryFile(suffix=f".{format}", delete=False) + destination_path = temp_file.name + temp_file.close() + + # Save structure in specified format + if format == "cif": + structure.to(filename=destination_path, fmt="cif") + elif format == "poscar": + structure.to(filename=destination_path, fmt="poscar") + elif format == "stru": + # For stru format, we need to convert to ABACUS format + stru_content = _structure_to_abacus_stru(structure) + with open(destination_path, 'w') as f: + f.write(stru_content) + else: + raise ValueError(f"Unsupported format: {format}") + + return { + "structure_file": destination_path, + "material_id": material_id, + "format": format + } + + except Exception as e: + return {"message": f"Failed to download structure from Materials Project: {e}"} + +def _structure_to_abacus_stru(structure: Structure) -> str: + """ + Convert a pymatgen Structure to ABACUS STRU format. + + Args: + structure (Structure): The pymatgen Structure object + + Returns: + str: The ABACUS STRU formatted string + """ + # Start building the STRU content + stru_lines = [] + + # Title + stru_lines.append("ATOMIC_STRUCTURE") + + # Cell parameters + lattice = structure.lattice + stru_lines.append(f"{lattice.a:.10f} {lattice.b:.10f} {lattice.c:.10f}") + + # Lattice vectors (in fractional coordinates) + stru_lines.append("0.0 0.0 0.0") + stru_lines.append("0.0 0.0 0.0") + stru_lines.append("0.0 0.0 0.0") + + # Atom kinds and coordinates + # Group atoms by element + element_counts = {} + for site in structure: + element = site.specie.symbol + element_counts[element] = element_counts.get(element, 0) + 1 + + # Write atom kinds + for element, count in element_counts.items(): + stru_lines.append(f"{element} {count}") + + # Write coordinates in cartesian format + stru_lines.append("CART") + for site in structure: + element = site.specie.symbol + coord = site.coords + stru_lines.append(f"{element} {coord[0]:.10f} {coord[1]:.10f} {coord[2]:.10f} 1 1 1") + + return "\n".join(stru_lines) @mcp.tool() def generate_bulk_structure(element: str, - crystal_structure:Literal["sc", "fcc", "bcc","hcp","diamond", "zincblende", "rocksalt"]='fcc', - a:float =None, - c: float =None, - cubic: bool =False, - orthorhombic: bool =False, - file_format: Literal["cif", "poscar"] = "cif", - ) -> Dict[str, Any]: + crystal_structure:Literal["sc", "fcc", "bcc","hcp","diamond", "zincblende", "rocksalt"]='fcc', + a:float =None, + c: float =None, + cubic: bool =False, + orthorhombic: bool =False, + file_format: Literal["cif", "poscar"] = "cif", + ) -> Dict[str, Any]: """ Generate a bulk crystal structure using ASE's `bulk` function. From 7a4da377c8a3e8b672e5c3094e89c163e24b00bf Mon Sep 17 00:00:00 2001 From: ahxbcn <2092233074@qq.com> Date: Thu, 12 Feb 2026 12:25:21 +0800 Subject: [PATCH 03/10] feat: add materials project download functionality to submodules --- .../modules/submodules/structure_generator.py | 67 ++++++++++++++++--- 1 file changed, 59 insertions(+), 8 deletions(-) diff --git a/src/abacusagent/modules/submodules/structure_generator.py b/src/abacusagent/modules/submodules/structure_generator.py index 6c1a066..99a655e 100644 --- a/src/abacusagent/modules/submodules/structure_generator.py +++ b/src/abacusagent/modules/submodules/structure_generator.py @@ -9,6 +9,9 @@ from typing import Literal, Optional, Dict, Any, List, Tuple, Union from abacusagent.modules.util.comm import generate_work_path +import tempfile +import os +from pymatgen.ext.matproj import MPRester # From Introduction to Solid State Physics, 8th edition, by Charles Kittel @@ -77,15 +80,63 @@ "Pb": {"crystal": "fcc", "a": 4.95}, } -#@mcp.tool() + +def materials_project_download( + material_id: str, + destination_path: Optional[str] = None +) -> Dict[str, Any]: + """ + Download structure from Materials Project database by material ID. + + Args: + material_id (str): The Materials Project material ID (e.g., 'mp-12345') + destination_path (str, optional): The path to save the downloaded structure file. + If not provided, a temporary file will be created. + + Returns: + A dictionary containing: + - 'structure_file': Path to the downloaded structure file. + - 'material_id': The material ID used for download. + """ + try: + # Get the Materials Project API key from environment variables + api_key = os.environ.get("MP_API_KEY") + if not api_key: + raise ValueError("Materials Project API key not found. Please set MP_API_KEY environment variable.") + + # Connect to Materials Project database + with MPRester(api_key) as mpr: + # Retrieve the structure + structure = mpr.get_structure_by_material_id(material_id) + + # Determine destination path + if destination_path is None: + # Create a temporary file + temp_file = tempfile.NamedTemporaryFile(suffix=".cif", delete=False) + destination_path = temp_file.name + temp_file.close() + + # Save structure in CIF format (default) + structure.to(filename=destination_path, fmt="cif") + + return { + "structure_file": destination_path, + "material_id": material_id + } + + except Exception as e: + return {"message": f"Failed to download structure from Materials Project: {e}"} + + +@mcp.tool() def generate_bulk_structure(element: str, - crystal_structure:Literal["sc", "fcc", "bcc","hcp","diamond", "zincblende", "rocksalt"]='fcc', - a:float =None, - c: float =None, - cubic: bool =False, - orthorhombic: bool =False, - file_format: Literal["cif", "poscar"] = "cif", - ) -> Dict[str, Any]: + crystal_structure:Literal["sc", "fcc", "bcc","hcp","diamond", "zincblende", "rocksalt"]='fcc', + a:float =None, + c: float =None, + cubic: bool =False, + orthorhombic: bool =False, + file_format: Literal["cif", "poscar"] = "cif", + ) -> Dict[str, Any]: """ Generate a bulk crystal structure using ASE's `bulk` function. From 29fed81c1dfde5352a18074ab0f932996b3dc538 Mon Sep 17 00:00:00 2001 From: ahxbcn <2092233074@qq.com> Date: Thu, 12 Feb 2026 13:28:41 +0800 Subject: [PATCH 04/10] refactor: format data --- .../modules/structure_generator.py | 131 +++--------------- .../modules/submodules/structure_generator.py | 95 ++----------- 2 files changed, 32 insertions(+), 194 deletions(-) diff --git a/src/abacusagent/modules/structure_generator.py b/src/abacusagent/modules/structure_generator.py index 2d77148..e6b3ee4 100644 --- a/src/abacusagent/modules/structure_generator.py +++ b/src/abacusagent/modules/structure_generator.py @@ -6,117 +6,8 @@ from abacusagent.modules.submodules.structure_generator import generate_molecule_structure as _generate_molecule_structure from abacusagent.modules.submodules.structure_generator import generate_bulk_structure_from_wyckoff_position as _generate_bulk_structure_from_wyckoff_position from abacusagent.modules.submodules.structure_generator import get_ieee_standard_structure as _get_ieee_standard_structure -import tempfile -import os -from pymatgen.ext.matproj import MPRester -from pymatgen.core import Structure +from abacusagent.modules.submodules.structure_generator import materials_project_download as _materials_project_download -@mcp.tool() -def materials_project_download( - material_id: str, - destination_path: Optional[str] = None, - format: Literal["cif", "poscar", "stru"] = "cif" -) -> Dict[str, Any]: - """ - Download structure from Materials Project database by material ID. - - Args: - material_id (str): The Materials Project material ID (e.g., 'mp-12345') - destination_path (str, optional): The path to save the downloaded structure file. - If not provided, a temporary file will be created. - format (Literal["cif", "poscar", "stru"] = "cif"): The format to save the structure file. - Can be 'cif', 'poscar', or 'stru'. - - Returns: - A dictionary containing: - - 'structure_file': Path to the downloaded structure file. - - 'material_id': The material ID used for download. - - 'format': The format of the downloaded structure. - """ - try: - # Get the Materials Project API key from environment variables - api_key = os.environ.get("MP_API_KEY") - if not api_key: - raise ValueError("Materials Project API key not found. Please set MP_API_KEY environment variable.") - - # Connect to Materials Project database - with MPRester(api_key) as mpr: - # Retrieve the structure - structure = mpr.get_structure_by_material_id(material_id) - - # Determine destination path - if destination_path is None: - # Create a temporary file - temp_file = tempfile.NamedTemporaryFile(suffix=f".{format}", delete=False) - destination_path = temp_file.name - temp_file.close() - - # Save structure in specified format - if format == "cif": - structure.to(filename=destination_path, fmt="cif") - elif format == "poscar": - structure.to(filename=destination_path, fmt="poscar") - elif format == "stru": - # For stru format, we need to convert to ABACUS format - stru_content = _structure_to_abacus_stru(structure) - with open(destination_path, 'w') as f: - f.write(stru_content) - else: - raise ValueError(f"Unsupported format: {format}") - - return { - "structure_file": destination_path, - "material_id": material_id, - "format": format - } - - except Exception as e: - return {"message": f"Failed to download structure from Materials Project: {e}"} - -def _structure_to_abacus_stru(structure: Structure) -> str: - """ - Convert a pymatgen Structure to ABACUS STRU format. - - Args: - structure (Structure): The pymatgen Structure object - - Returns: - str: The ABACUS STRU formatted string - """ - # Start building the STRU content - stru_lines = [] - - # Title - stru_lines.append("ATOMIC_STRUCTURE") - - # Cell parameters - lattice = structure.lattice - stru_lines.append(f"{lattice.a:.10f} {lattice.b:.10f} {lattice.c:.10f}") - - # Lattice vectors (in fractional coordinates) - stru_lines.append("0.0 0.0 0.0") - stru_lines.append("0.0 0.0 0.0") - stru_lines.append("0.0 0.0 0.0") - - # Atom kinds and coordinates - # Group atoms by element - element_counts = {} - for site in structure: - element = site.specie.symbol - element_counts[element] = element_counts.get(element, 0) + 1 - - # Write atom kinds - for element, count in element_counts.items(): - stru_lines.append(f"{element} {count}") - - # Write coordinates in cartesian format - stru_lines.append("CART") - for site in structure: - element = site.specie.symbol - coord = site.coords - stru_lines.append(f"{element} {coord[0]:.10f} {coord[1]:.10f} {coord[2]:.10f} 1 1 1") - - return "\n".join(stru_lines) @mcp.tool() def generate_bulk_structure(element: str, @@ -266,3 +157,23 @@ def get_ieee_standard_structure( - standard_stru_file: The absolute path to the rotated crystal structure file. """ return _get_ieee_standard_structure(stru_file, stru_type) + +@mcp.tool() +def materials_project_download( + material_id: str, + destination_path: Optional[str] = None, +) -> Dict[str, Any]: + """ + Download structure from Materials Project database by material ID. + + Args: + material_id (str): The Materials Project material ID (e.g., 'mp-12345') + destination_path (str, optional): The path to save the downloaded structure file. + If not provided, a temporary file will be created. + + Returns: + A dictionary containing: + - 'structure_file': Path to the downloaded structure file. + - 'material_id': The material ID used for download. + """ + return _materials_project_download(material_id, destination_path) diff --git a/src/abacusagent/modules/submodules/structure_generator.py b/src/abacusagent/modules/submodules/structure_generator.py index 99a655e..8b8d6dc 100644 --- a/src/abacusagent/modules/submodules/structure_generator.py +++ b/src/abacusagent/modules/submodules/structure_generator.py @@ -1,89 +1,21 @@ +import os +from pathlib import Path +from typing import Literal, Optional, Dict, Any, List, Tuple, Union + from ase import Atoms from ase.io import read, write from ase.build import molecule from ase.data import chemical_symbols from ase.collections import g2 from pymatgen.core import Structure, Lattice - -from pathlib import Path -from typing import Literal, Optional, Dict, Any, List, Tuple, Union - -from abacusagent.modules.util.comm import generate_work_path -import tempfile -import os from pymatgen.ext.matproj import MPRester - -# From Introduction to Solid State Physics, 8th edition, by Charles Kittel -ELEMENT_CRYSTAL_STRUCTURES = { - "Li": {"crystal": "bcc", "a": 3.51}, - "Be": {"crystal": "hcp", "a": 2.27, "c": 3.59}, - "Na": {"crystal": "bcc", "a": 4.23}, - "Mg": {"crystal": "hcp", "a": 3.21, "c": 5.21}, - "Al": {"crystal": "fcc", "a": 4.05}, - "Si": {"crystal": "diamond", "a": 5.43}, - "K": {"crystal": "bcc", "a": 5.23}, - "Ca": {"crystal": "fcc", "a": 5.58}, - "Sc": {"crystal": "hcp", "a": 3.31, "c": 5.27}, - "Ti": {"crystal": "hcp", "a": 2.95, "c": 4.68}, - "V": {"crystal": "bcc", "a": 3.03}, - "Cr": {"crystal": "bcc", "a": 2.88}, - "Mn": {"crystal": "bcc", "a": 2.91}, # To be checked - "Fe": {"crystal": "bcc", "a": 2.87}, - "Co": {"crystal": "hcp", "a": 2.51, "c": 4.07}, - "Ni": {"crystal": "fcc", "a": 3.52}, - "Cu": {"crystal": "fcc", "a": 3.61}, - "Zn": {"crystal": "hcp", "a": 2.66, "c": 4.95}, - "Ga": {"crystal": "fcc", "a": 4.50}, # To be checked - "Ge": {"crystal": "diamond", "a": 5.69}, - "Rb": {"crystal": "bcc", "a": 5.58}, - "Sr": {"crystal": "fcc", "a": 6.08}, - "Y": {"crystal": "hcp", "a": 3.65, "c": 5.73}, - "Zr": {"crystal": "hcp", "a": 3.23, "c": 5.15}, - "Nb": {"crystal": "bcc", "a": 3.30}, - "Mo": {"crystal": "bcc", "a": 3.15}, - "Tc": {"crystal": "hcp", "a": 2.74, "c": 4.44}, - "Ru": {"crystal": "hcp", "a": 2.71, "c": 4.28}, - "Rh": {"crystal": "fcc", "a": 3.80}, - "Pd": {"crystal": "fcc", "a": 3.89}, - "Ag": {"crystal": "fcc", "a": 4.09}, - "Cd": {"crystal": "hcp", "a": 2.98, "c": 5.62}, - "In": {"crystal": "bcc", "a": 3.25}, # To be checked - "Sn": {"crystal": "diamond", "a": 6.49}, - "Cs": {"crystal": "bcc", "a": 6.05}, - "Ba": {"crystal": "bcc", "a": 5.02}, - "La": {"crystal": "hcp", "a": 3.75, "c": 5.75}, # To be checked - "Ce": {"crystal": "fcc", "a": 5.16}, - "Pr": {"crystal": "hcp", "a": 3.65, "c": 5.75}, # To be checked - "Nd": {"crystal": "hcp", "a": 3.65, "c": 5.73}, # To be checked - "Pm": {"crystal": "hcp", "a": 3.65, "c": 5.73}, # To be checked - "Sm": {"crystal": "hcp", "a": 3.65, "c": 5.73}, # To be checked - "Eu": {"crystal": "bcc", "a": 4.58}, - "Gd": {"crystal": "hcp", "a": 3.63, "c": 5.78}, - "Tb": {"crystal": "hcp", "a": 3.60, "c": 5.70}, - "Dy": {"crystal": "hcp", "a": 3.59, "c": 5.65}, - "Ho": {"crystal": "hcp", "a": 3.58, "c": 5.62}, - "Er": {"crystal": "hcp", "a": 3.56, "c": 5.59}, - "Tm": {"crystal": "hcp", "a": 3.54, "c": 5.56}, - "Yb": {"crystal": "fcc", "a": 5.48}, - "Lu": {"crystal": "hcp", "a": 3.50, "c": 5.55}, - "Hf": {"crystal": "hcp", "a": 3.19, "c": 5.05}, - "Ta": {"crystal": "bcc", "a": 3.30}, - "W": {"crystal": "bcc", "a": 3.16}, - "Re": {"crystal": "hcp", "a": 2.76, "c": 4.46}, - "Os": {"crystal": "hcp", "a": 2.74, "c": 4.32}, - "Ir": {"crystal": "fcc", "a": 3.84}, - "Pt": {"crystal": "fcc", "a": 3.92}, - "Au": {"crystal": "fcc", "a": 4.08}, - "Hg": {"crystal": "hcp", "a": 2.99, "c": 5.01}, # To be checked - "Tl": {"crystal": "hcp", "a": 3.46, "c": 5.52}, - "Pb": {"crystal": "fcc", "a": 4.95}, -} +from abacusagent.modules.util.comm import generate_work_path def materials_project_download( material_id: str, - destination_path: Optional[str] = None + destination_path: Optional[str] = None, ) -> Dict[str, Any]: """ Download structure from Materials Project database by material ID. @@ -111,24 +43,21 @@ def materials_project_download( # Determine destination path if destination_path is None: - # Create a temporary file - temp_file = tempfile.NamedTemporaryFile(suffix=".cif", delete=False) - destination_path = temp_file.name - temp_file.close() + destination_path = f"{material_id}.cif" - # Save structure in CIF format (default) + # Save structure structure.to(filename=destination_path, fmt="cif") return { "structure_file": destination_path, - "material_id": material_id + "material_id": material_id, } except Exception as e: + import traceback + traceback.print_exc() return {"message": f"Failed to download structure from Materials Project: {e}"} - -@mcp.tool() def generate_bulk_structure(element: str, crystal_structure:Literal["sc", "fcc", "bcc","hcp","diamond", "zincblende", "rocksalt"]='fcc', a:float =None, @@ -214,7 +143,6 @@ def generate_bulk_structure(element: str, except Exception as e: return {"message": f"Generating bulk structure failed: {e}"} -#@mcp.tool() def generate_bulk_structure_from_wyckoff_position( a: float, b: float, @@ -264,7 +192,6 @@ def generate_bulk_structure_from_wyckoff_position( except Exception as e: return {"message": f"Generating bulk structure from Wyckoff position failed: {e}"} -#@mcp.tool() def generate_molecule_structure( molecule_name: Literal['PH3', 'P2', 'CH3CHO', 'H2COH', 'CS', 'OCHCHO', 'C3H9C', 'CH3COF', 'CH3CH2OCH3', 'HCOOH', 'HCCl3', 'HOCl', 'H2', 'SH2', 'C2H2', 'C4H4NH', From 02c4ef393f72608cac40c0ff6ed672374571ee98 Mon Sep 17 00:00:00 2001 From: ahxbcn <2092233074@qq.com> Date: Thu, 12 Feb 2026 13:32:41 +0800 Subject: [PATCH 05/10] fix: restore missing read_abacus_stru --- src/abacusagent/modules/submodules/abacus.py | 43 +++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/abacusagent/modules/submodules/abacus.py b/src/abacusagent/modules/submodules/abacus.py index 0823698..91372dd 100644 --- a/src/abacusagent/modules/submodules/abacus.py +++ b/src/abacusagent/modules/submodules/abacus.py @@ -576,7 +576,48 @@ def read_abacus_input_kpt( except Exception as e: return {'message': f"Read ABACUS INPUT file failed: {e}"} -@mcp.tool() +def read_abacus_stru(abacus_input_dir: Path): + """ + Read ABACUS STRU file. + Args: + abacus_input_dir (str): Path to the directory containing the ABACUS input files. + Returns: + A dictionary containing information from the STRU file. Containing the following keys: + cell: the cell of the structure + atom_kinds: a dict, keys are atom labels, values are dicts containing the following keys: + pp: the pseudopotential file name + orb: the orbital file name + element: the element name + number: the number of atoms with this label + atommag: the magnetic moment of each atom with this label + coord: the coordinates of each atom + move: the movable flags of each atom + Raises: + FileNotFoundError: If path of given STRU file does not exist + """ + try: + input_params = ReadInput(os.path.join(abacus_input_dir, "INPUT")) + stru_file = os.path.join(abacus_input_dir, input_params.get('stru_file', "STRU")) + if not os.path.isfile(stru_file): + raise FileNotFoundError(f"STRU file {stru_file} does not exist.") + + stru = AbacusStru.ReadStru(stru_file) + atom_kinds = {} + for idx, label in enumerate(stru.get_label(total=False)): + atom_kinds[label] = { + 'pp': stru.get_pp()[idx], + 'orb': stru.get_orb()[idx] if stru.get_orb() is not None else None, + 'element': stru.get_element(number=False,total=False)[idx], + 'number': stru.get_label().count(label), + 'atommag': stru.get_atommag()[idx], + } + return {'cell': stru.get_cell(), + 'atom_kinds': atom_kinds, + 'coord': stru.get_coord(), + 'move': stru.get_move()} + except Exception as e: + return {'message': f"Read ABACUS STRU file failed: {e}"} + def materials_project_download( material_id: str, destination_path: Optional[str] = None, From b817fad1feb8c9b9d185bc6d918a5eace3e0ca33 Mon Sep 17 00:00:00 2001 From: ahxbcn <2092233074@qq.com> Date: Thu, 12 Feb 2026 13:44:20 +0800 Subject: [PATCH 06/10] test: add integrate test for materials project download function --- .../integrate_test/test_materials_project.py | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 tests/integrate_test/test_materials_project.py diff --git a/tests/integrate_test/test_materials_project.py b/tests/integrate_test/test_materials_project.py new file mode 100644 index 0000000..d72b52e --- /dev/null +++ b/tests/integrate_test/test_materials_project.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +""" +集成测试:测试Materials Project下载功能 +""" + +import os +import tempfile +from abacusagent.modules.submodules.structure_generator import materials_project_download + +def test_materials_project_download(): + """测试materials_project_download函数的基本功能""" + + # 设置测试用的API密钥 + os.environ['MP_API_KEY'] = 'n9WVEKNI1A8yP1MfO3KBK8eKeFwESFBu' + + print("测试materials_project_download功能...") + + # 测试1: 基本功能测试 + try: + # 使用一个已知存在的材料ID进行测试 + result = materials_project_download(material_id="mp-1234") + print("✓ 基本功能测试通过") + print(f" 结构文件: {result.get('structure_file')}") + print(f" 材料ID: {result.get('material_id')}") + + # 检查返回值 + assert 'structure_file' in result, "应返回structure_file字段" + assert 'material_id' in result, "应返回material_id字段" + assert result['material_id'] == "mp-1234", "材料ID应匹配" + + # 检查文件是否存在 + if 'structure_file' in result: + file_path = result['structure_file'] + assert os.path.exists(file_path), "下载的文件应该存在" + print(f"✓ 文件存在: {file_path}") + + except Exception as e: + print(f"✗ 基本功能测试失败: {e}") + return False + + # 测试2: 指定输出路径 + try: + with tempfile.NamedTemporaryFile(suffix='.cif', delete=False) as tmp: + temp_path = tmp.name + + result = materials_project_download( + material_id="mp-1234", + destination_path=temp_path + ) + + assert result['structure_file'] == temp_path, "应返回指定的输出路径" + assert os.path.exists(temp_path), "指定路径的文件应该存在" + print("✓ 指定路径测试通过") + + # 清理临时文件 + os.unlink(temp_path) + + except Exception as e: + print(f"✗ 指定路径测试失败: {e}") + return False + + # 测试3: 错误处理测试 - 无效的材料ID + try: + result = materials_project_download(material_id="mp-999999999") + # 如果没有抛出异常,检查是否返回了错误消息 + if 'message' in result: + print("✓ 错误处理测试通过") + else: + print("✓ 错误处理测试通过(未找到材料)") + except Exception as e: + print(f"✓ 错误处理测试通过(捕获异常): {e}") + + print("\n所有测试完成!") + return True + +if __name__ == "__main__": + test_materials_project_download() \ No newline at end of file From c83ffbfa78b01f9d29b167d281a164f705fcc38d Mon Sep 17 00:00:00 2001 From: ahxbcn <2092233074@qq.com> Date: Thu, 12 Feb 2026 13:48:02 +0800 Subject: [PATCH 07/10] test: rewrite integrate test for materials project download function in standard format --- .../integrate_test/test_materials_project.py | 104 +++++++----------- 1 file changed, 41 insertions(+), 63 deletions(-) diff --git a/tests/integrate_test/test_materials_project.py b/tests/integrate_test/test_materials_project.py index d72b52e..07fd128 100644 --- a/tests/integrate_test/test_materials_project.py +++ b/tests/integrate_test/test_materials_project.py @@ -1,77 +1,55 @@ -#!/usr/bin/env python3 -""" -集成测试:测试Materials Project下载功能 -""" - import os import tempfile +import unittest +from pathlib import Path from abacusagent.modules.submodules.structure_generator import materials_project_download -def test_materials_project_download(): - """测试materials_project_download函数的基本功能""" - - # 设置测试用的API密钥 - os.environ['MP_API_KEY'] = 'n9WVEKNI1A8yP1MfO3KBK8eKeFwESFBu' - - print("测试materials_project_download功能...") +class TestMaterialsProjectDownload(unittest.TestCase): - # 测试1: 基本功能测试 - try: - # 使用一个已知存在的材料ID进行测试 + def setUp(self): + """设置测试环境""" + # 设置测试用的API密钥 + self.api_key = 'n9WVEKNI1A8yP1MfO3KBK8eKeFwESFBu' + os.environ['MP_API_KEY'] = self.api_key + + def test_materials_project_download_basic(self): + """测试materials_project_download基本功能""" + # 测试基本功能 - 使用一个合理的材料ID + # 注意:由于API限制,我们主要测试函数接口和逻辑正确性 result = materials_project_download(material_id="mp-1234") - print("✓ 基本功能测试通过") - print(f" 结构文件: {result.get('structure_file')}") - print(f" 材料ID: {result.get('material_id')}") - # 检查返回值 - assert 'structure_file' in result, "应返回structure_file字段" - assert 'material_id' in result, "应返回material_id字段" - assert result['material_id'] == "mp-1234", "材料ID应匹配" + # 检查返回值结构 + self.assertIn('structure_file', result) + self.assertIn('material_id', result) + self.assertEqual(result['material_id'], "mp-1234") - # 检查文件是否存在 - if 'structure_file' in result: - file_path = result['structure_file'] - assert os.path.exists(file_path), "下载的文件应该存在" - print(f"✓ 文件存在: {file_path}") - - except Exception as e: - print(f"✗ 基本功能测试失败: {e}") - return False - - # 测试2: 指定输出路径 - try: + def test_materials_project_download_with_custom_path(self): + """测试指定输出路径的功能""" with tempfile.NamedTemporaryFile(suffix='.cif', delete=False) as tmp: temp_path = tmp.name - result = materials_project_download( - material_id="mp-1234", - destination_path=temp_path - ) - - assert result['structure_file'] == temp_path, "应返回指定的输出路径" - assert os.path.exists(temp_path), "指定路径的文件应该存在" - print("✓ 指定路径测试通过") - - # 清理临时文件 - os.unlink(temp_path) - - except Exception as e: - print(f"✗ 指定路径测试失败: {e}") - return False + try: + result = materials_project_download( + material_id="mp-1234", + destination_path=temp_path + ) + + # 检查是否返回了指定的路径 + self.assertEqual(result['structure_file'], temp_path) + # 检查文件是否存在 + self.assertTrue(os.path.exists(temp_path)) + finally: + # 清理临时文件 + if os.path.exists(temp_path): + os.unlink(temp_path) - # 测试3: 错误处理测试 - 无效的材料ID - try: + def test_materials_project_download_error_handling(self): + """测试错误处理功能""" + # 测试无效材料ID的情况 result = materials_project_download(material_id="mp-999999999") - # 如果没有抛出异常,检查是否返回了错误消息 - if 'message' in result: - print("✓ 错误处理测试通过") - else: - print("✓ 错误处理测试通过(未找到材料)") - except Exception as e: - print(f"✓ 错误处理测试通过(捕获异常): {e}") - - print("\n所有测试完成!") - return True + + # 应该返回错误信息而不是抛出异常 + self.assertIn('message', result) -if __name__ == "__main__": - test_materials_project_download() \ No newline at end of file +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 5ce354e3adef1eb6930d5fe3c9ab8a77fb0d28b4 Mon Sep 17 00:00:00 2001 From: ahxbcn <2092233074@qq.com> Date: Thu, 12 Feb 2026 13:50:12 +0800 Subject: [PATCH 08/10] test: use environment variable for API key in materials project test --- tests/integrate_test/test_materials_project.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integrate_test/test_materials_project.py b/tests/integrate_test/test_materials_project.py index 07fd128..cb45c25 100644 --- a/tests/integrate_test/test_materials_project.py +++ b/tests/integrate_test/test_materials_project.py @@ -8,8 +8,8 @@ class TestMaterialsProjectDownload(unittest.TestCase): def setUp(self): """设置测试环境""" - # 设置测试用的API密钥 - self.api_key = 'n9WVEKNI1A8yP1MfO3KBK8eKeFwESFBu' + # 从环境变量获取API密钥,如果没有则使用默认值用于测试 + self.api_key = os.environ.get('MP_API_KEY', 'n9WVEKNI1A8yP1MfO3KBK8eKeFwESFBu') os.environ['MP_API_KEY'] = self.api_key def test_materials_project_download_basic(self): From 4f86b647b96c6b0f38b27e3b0e3fdeca1cf6cdfa Mon Sep 17 00:00:00 2001 From: ahxbcn <2092233074@qq.com> Date: Thu, 12 Feb 2026 13:57:04 +0800 Subject: [PATCH 09/10] test: add file cleanup after download in materials project test --- .../integrate_test/test_materials_project.py | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/tests/integrate_test/test_materials_project.py b/tests/integrate_test/test_materials_project.py index cb45c25..d83fbac 100644 --- a/tests/integrate_test/test_materials_project.py +++ b/tests/integrate_test/test_materials_project.py @@ -7,22 +7,20 @@ class TestMaterialsProjectDownload(unittest.TestCase): def setUp(self): - """设置测试环境""" - # 从环境变量获取API密钥,如果没有则使用默认值用于测试 - self.api_key = os.environ.get('MP_API_KEY', 'n9WVEKNI1A8yP1MfO3KBK8eKeFwESFBu') + self.api_key = os.environ.get('MP_API_KEY', 'test_key') os.environ['MP_API_KEY'] = self.api_key def test_materials_project_download_basic(self): - """测试materials_project_download基本功能""" - # 测试基本功能 - 使用一个合理的材料ID - # 注意:由于API限制,我们主要测试函数接口和逻辑正确性 result = materials_project_download(material_id="mp-1234") - # 检查返回值结构 self.assertIn('structure_file', result) self.assertIn('material_id', result) self.assertEqual(result['material_id'], "mp-1234") + # 清理下载的文件 + if 'structure_file' in result and os.path.exists(result['structure_file']): + os.unlink(result['structure_file']) + def test_materials_project_download_with_custom_path(self): """测试指定输出路径的功能""" with tempfile.NamedTemporaryFile(suffix='.cif', delete=False) as tmp: @@ -34,22 +32,20 @@ def test_materials_project_download_with_custom_path(self): destination_path=temp_path ) - # 检查是否返回了指定的路径 self.assertEqual(result['structure_file'], temp_path) - # 检查文件是否存在 self.assertTrue(os.path.exists(temp_path)) finally: - # 清理临时文件 if os.path.exists(temp_path): os.unlink(temp_path) def test_materials_project_download_error_handling(self): - """测试错误处理功能""" - # 测试无效材料ID的情况 result = materials_project_download(material_id="mp-999999999") - # 应该返回错误信息而不是抛出异常 self.assertIn('message', result) + + # 如果下载成功(虽然不应该),清理文件 + if 'structure_file' in result and os.path.exists(result.get('structure_file', '')): + os.unlink(result['structure_file']) if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() From 1996d1b8760881d515cdedadcee7a1084706efb6 Mon Sep 17 00:00:00 2001 From: ahxbcn <2092233074@qq.com> Date: Thu, 12 Feb 2026 13:58:28 +0800 Subject: [PATCH 10/10] fix: remove redundant code --- src/abacusagent/modules/submodules/abacus.py | 105 ------------------- 1 file changed, 105 deletions(-) diff --git a/src/abacusagent/modules/submodules/abacus.py b/src/abacusagent/modules/submodules/abacus.py index 91372dd..0448aad 100644 --- a/src/abacusagent/modules/submodules/abacus.py +++ b/src/abacusagent/modules/submodules/abacus.py @@ -618,108 +618,3 @@ def read_abacus_stru(abacus_input_dir: Path): except Exception as e: return {'message': f"Read ABACUS STRU file failed: {e}"} -def materials_project_download( - material_id: str, - destination_path: Optional[str] = None, - format: Literal["cif", "poscar", "stru"] = "cif" -) -> Dict[str, Any]: - """ - Download structure from Materials Project database by material ID. - - Args: - material_id (str): The Materials Project material ID (e.g., 'mp-12345') - destination_path (str, optional): The path to save the downloaded structure file. - If not provided, a temporary file will be created. - format (Literal["cif", "poscar", "stru"] = "cif"): The format to save the structure file. - Can be 'cif', 'poscar', or 'stru'. - - Returns: - A dictionary containing: - - 'structure_file': Path to the downloaded structure file. - - 'material_id': The material ID used for download. - - 'format': The format of the downloaded structure. - """ - try: - # Get the Materials Project API key from environment variables - api_key = os.environ.get("MP_API_KEY") - if not api_key: - raise ValueError("Materials Project API key not found. Please set MP_API_KEY environment variable.") - - # Connect to Materials Project database - with MPRester(api_key) as mpr: - # Retrieve the structure - structure = mpr.get_structure_by_material_id(material_id) - - # Determine destination path - if destination_path is None: - # Create a temporary file - temp_file = tempfile.NamedTemporaryFile(suffix=f".{format}", delete=False) - destination_path = temp_file.name - temp_file.close() - - # Save structure in specified format - if format == "cif": - structure.to(filename=destination_path, fmt="cif") - elif format == "poscar": - structure.to(filename=destination_path, fmt="poscar") - elif format == "stru": - # For stru format, we need to convert to ABACUS format - stru_content = _structure_to_abacus_stru(structure) - with open(destination_path, 'w') as f: - f.write(stru_content) - else: - raise ValueError(f"Unsupported format: {format}") - - return { - "structure_file": destination_path, - "material_id": material_id, - "format": format - } - - except Exception as e: - return {"message": f"Failed to download structure from Materials Project: {e}"} - -def _structure_to_abacus_stru(structure: Structure) -> str: - """ - Convert a pymatgen Structure to ABACUS STRU format. - - Args: - structure (Structure): The pymatgen Structure object - - Returns: - str: The ABACUS STRU formatted string - """ - # Start building the STRU content - stru_lines = [] - - # Title - stru_lines.append("ATOMIC_STRUCTURE") - - # Cell parameters - lattice = structure.lattice - stru_lines.append(f"{lattice.a:.10f} {lattice.b:.10f} {lattice.c:.10f}") - - # Lattice vectors (in fractional coordinates) - stru_lines.append("0.0 0.0 0.0") - stru_lines.append("0.0 0.0 0.0") - stru_lines.append("0.0 0.0 0.0") - - # Atom kinds and coordinates - # Group atoms by element - element_counts = {} - for site in structure: - element = site.specie.symbol - element_counts[element] = element_counts.get(element, 0) + 1 - - # Write atom kinds - for element, count in element_counts.items(): - stru_lines.append(f"{element} {count}") - - # Write coordinates in cartesian format - stru_lines.append("CART") - for site in structure: - element = site.specie.symbol - coord = site.coords - stru_lines.append(f"{element} {coord[0]:.10f} {coord[1]:.10f} {coord[2]:.10f} 1 1 1") - - return "\n".join(stru_lines)