""" MatGL Tools Module This module provides tools for material structure relaxation and property prediction using MatGL (Materials Graph Library) models. """ from __future__ import annotations from ...core.config import material_config import warnings import json from typing import Dict, List, Union, Optional, Any import torch from pymatgen.core import Lattice, Structure from pymatgen.ext.matproj import MPRester from pymatgen.io.ase import AseAtomsAdaptor import matgl from matgl.ext.ase import Relaxer, MolecularDynamics, PESCalculator from ase.md.velocitydistribution import MaxwellBoltzmannDistribution from ...core.llm_tools import llm_tool import os from ..support.utils import read_structure_from_file_name_or_content_string # To suppress warnings for clearer output warnings.simplefilter("ignore") @llm_tool(name="relax_crystal_structure_M3GNet", description="Optimize crystal structure geometry using M3GNet universal potential from a structure file or content string") async def relax_crystal_structure_M3GNet( structure_source: str, fmax: float = 0.01 ) -> str: """ Optimize crystal structure geometry to find its equilibrium configuration. Uses M3GNet universal potential for fast and accurate structure relaxation without DFT. Accepts a structure file or content string. Args: structure_source: The name of the structure file (e.g., POSCAR, CIF) or the content string. fmax: Maximum force tolerance for convergence in eV/Å (default: 0.01). Returns: A Markdown formatted string with the relaxation results or an error message. """ try: # 使用read_structure_from_file_name_or_content_string函数读取结构 structure_content, content_format = read_structure_from_file_name_or_content_string(structure_source) # 使用pymatgen读取结构 structure = Structure.from_str(structure_content, fmt=content_format) if structure is None: return "Error: Failed to obtain a valid structure" # Load the M3GNet universal potential model pot = matgl.load_model("M3GNet-MP-2021.2.8-PES") # Create a relaxer and relax the structure relaxer = Relaxer(potential=pot) relax_results = relaxer.relax(structure, fmax=fmax) # Get the relaxed structure relaxed_structure = relax_results["final_structure"] reduced_formula = relaxed_structure.composition.reduced_formula # 添加结构信息 lattice_info = relaxed_structure.lattice volume = relaxed_structure.volume density = relaxed_structure.density symmetry = relaxed_structure.get_space_group_info() # 构建原子位置表格 sites_table = " # SP a b c\n" sites_table += "--- ---- -------- -------- --------\n" for i, site in enumerate(relaxed_structure): frac_coords = site.frac_coords sites_table += f"{i:3d} {site.species_string:4s} {frac_coords[0]:8.6f} {frac_coords[1]:8.6f} {frac_coords[2]:8.6f}\n" return (f"## Structure Relaxation\n\n" f"- **Structure**: `{reduced_formula}`\n" f"- **Force Tolerance**: `{fmax} eV/Å`\n" f"- **Status**: `Successfully relaxed`\n\n" f"### Relaxed Structure Information\n\n" f"- **Space Group**: `{symmetry[0]} (#{symmetry[1]})`\n" f"- **Volume**: `{volume:.2f} ų`\n" f"- **Density**: `{density:.2f} g/cm³`\n" f"- **Lattice Parameters**:\n" f" - a = `{lattice_info.a:.6f} Å`, b = `{lattice_info.b:.6f} Å`, c = `{lattice_info.c:.6f} Å`\n" f" - α = `{lattice_info.alpha:.6f}°`, β = `{lattice_info.beta:.6f}°`, γ = `{lattice_info.gamma:.6f}°`\n\n" f"### Atomic Positions (Fractional Coordinates)\n\n" f"```\n" f"abc : {lattice_info.a:.6f} {lattice_info.b:.6f} {lattice_info.c:.6f}\n" f"angles: {lattice_info.alpha:.6f} {lattice_info.beta:.6f} {lattice_info.gamma:.6f}\n" f"pbc : {relaxed_structure.lattice.pbc[0]!s:5s} {relaxed_structure.lattice.pbc[1]!s:5s} {relaxed_structure.lattice.pbc[2]!s:5s}\n" f"Sites ({len(relaxed_structure)})\n" f"{sites_table}```\n") except Exception as e: return f"Error during structure relaxation: {str(e)}" # 内部函数,用于结构优化,返回结构对象而不是格式化字符串 async def _relax_crystal_structure_M3GNet_internal( structure_source: str, fmax: float = 0.01 ) -> Union[Structure, str]: """ 内部使用的结构优化函数,返回结构对象而不是格式化字符串。 Args: structure_source: 结构文件名或内容字符串 fmax: 力收敛阈值 (eV/Å) Returns: 优化后的结构对象或错误信息 """ try: # 使用read_structure_from_file_name_or_content_string函数读取结构 structure_content, content_format = read_structure_from_file_name_or_content_string(structure_source) # 使用pymatgen读取结构 structure = Structure.from_str(structure_content, fmt=content_format) if structure is None: return "Error: Failed to obtain a valid structure" # Load the M3GNet universal potential model pot = matgl.load_model("M3GNet-MP-2021.2.8-PES") # Create a relaxer and relax the structure relaxer = Relaxer(potential=pot) relax_results = relaxer.relax(structure, fmax=fmax) # Get the relaxed structure relaxed_structure = relax_results["final_structure"] return relaxed_structure except Exception as e: return f"Error during structure relaxation: {str(e)}" @llm_tool(name="predict_formation_energy_M3GNet", description="Predict the formation energy of a crystal structure using the M3GNet formation energy model from a structure file or content string, with optional structure optimization") async def predict_formation_energy_M3GNet( structure_source: str, optimize_structure: bool = True, fmax: float = 0.01 ) -> str: """ Predict the formation energy of a crystal structure using the M3GNet formation energy model. Args: structure_source: The name of the structure file (e.g., POSCAR, CIF) or the content string. optimize_structure: Whether to optimize the structure before prediction (default: True). fmax: Maximum force tolerance for structure relaxation in eV/Å (default: 0.01). Returns: A Markdown formatted string containing the predicted formation energy in eV/atom or an error message. """ try: # 获取结构(优化或不优化) if optimize_structure: # 使用内部函数优化结构 structure = await _relax_crystal_structure_M3GNet_internal( structure_source=structure_source, fmax=fmax ) # 检查优化是否成功 if isinstance(structure, str) and structure.startswith("Error"): return structure else: # 直接读取结构,不进行优化 structure_content, content_format = read_structure_from_file_name_or_content_string(structure_source) structure = Structure.from_str(structure_content, fmt=content_format) if structure is None: return "Error: Failed to obtain a valid structure" # 加载预训练模型 model = matgl.load_model("M3GNet-MP-2018.6.1-Eform") # 预测形成能 eform = model.predict_structure(structure) reduced_formula = structure.composition.reduced_formula # 构建结果字符串 optimization_status = "optimized" if optimize_structure else "non-optimized" # 添加结构信息 lattice_info = structure.lattice volume = structure.volume density = structure.density symmetry = structure.get_space_group_info() # 构建原子位置表格 sites_table = " # SP a b c\n" sites_table += "--- ---- -------- -------- --------\n" for i, site in enumerate(structure): frac_coords = site.frac_coords sites_table += f"{i:3d} {site.species_string:4s} {frac_coords[0]:8.6f} {frac_coords[1]:8.6f} {frac_coords[2]:8.6f}\n" return (f"## Formation Energy Prediction\n\n" f"- **Structure**: `{reduced_formula}`\n" f"- **Structure Status**: `{optimization_status}`\n" f"- **Formation Energy**: `{float(eform):.3f} eV/atom`\n\n" f"### Structure Information\n\n" f"- **Space Group**: `{symmetry[0]} (#{symmetry[1]})`\n" f"- **Volume**: `{volume:.2f} ų`\n" f"- **Density**: `{density:.2f} g/cm³`\n" f"- **Lattice Parameters**:\n" f" - a = `{lattice_info.a:.6f} Å`, b = `{lattice_info.b:.6f} Å`, c = `{lattice_info.c:.6f} Å`\n" f" - α = `{lattice_info.alpha:.6f}°`, β = `{lattice_info.beta:.6f}°`, γ = `{lattice_info.gamma:.6f}°`\n\n" f"### Atomic Positions (Fractional Coordinates)\n\n" f"```\n" f"abc : {lattice_info.a:.6f} {lattice_info.b:.6f} {lattice_info.c:.6f}\n" f"angles: {lattice_info.alpha:.6f} {lattice_info.beta:.6f} {lattice_info.gamma:.6f}\n" f"pbc : {structure.lattice.pbc[0]!s:5s} {structure.lattice.pbc[1]!s:5s} {structure.lattice.pbc[2]!s:5s}\n" f"Sites ({len(structure)})\n" f"{sites_table}```\n") except Exception as e: return f"Error: {str(e)}" @llm_tool(name="run_molecular_dynamics_M3GNet", description="Run molecular dynamics simulation on a crystal structure using M3GNet universal potential, with optional structure optimization") async def run_molecular_dynamics_M3GNet( structure_source: str, temperature_K: float = 300, steps: int = 100, optimize_structure: bool = True, fmax: float = 0.01 ) -> str: """ Run molecular dynamics simulation on a crystal structure using M3GNet universal potential. Args: structure_source: The name of the structure file (e.g., POSCAR, CIF) or the content string. temperature_K: Temperature for MD simulation in Kelvin (default: 300). steps: Number of MD steps to run (default: 100). optimize_structure: Whether to optimize the structure before simulation (default: True). fmax: Maximum force tolerance for structure relaxation in eV/Å (default: 0.01). Returns: A Markdown formatted string containing the simulation results, including final potential energy. """ try: # 获取结构(优化或不优化) if optimize_structure: # 使用内部函数优化结构 structure = await _relax_crystal_structure_M3GNet_internal( structure_source=structure_source, fmax=fmax ) # 检查优化是否成功 if isinstance(structure, str) and structure.startswith("Error"): return structure else: # 直接读取结构,不进行优化 structure_content, content_format = read_structure_from_file_name_or_content_string(structure_source) structure = Structure.from_str(structure_content, fmt=content_format) if structure is None: return "Error: Failed to obtain a valid structure" # Load the M3GNet universal potential model pot = matgl.load_model("M3GNet-MP-2021.2.8-PES") # Convert pymatgen structure to ASE atoms ase_adaptor = AseAtomsAdaptor() atoms = ase_adaptor.get_atoms(structure) # Initialize the velocity according to Maxwell Boltzmann distribution MaxwellBoltzmannDistribution(atoms, temperature_K=temperature_K) # Create the MD class and run simulation driver = MolecularDynamics(atoms, potential=pot, temperature=temperature_K) driver.run(steps) # Get final potential energy final_energy = atoms.get_potential_energy() # Get final structure final_structure = ase_adaptor.get_structure(atoms) reduced_formula = final_structure.composition.reduced_formula # 构建结果字符串 optimization_status = "optimized" if optimize_structure else "non-optimized" # 添加结构信息 lattice_info = final_structure.lattice volume = final_structure.volume density = final_structure.density symmetry = final_structure.get_space_group_info() # 构建原子位置表格 sites_table = " # SP a b c\n" sites_table += "--- ---- -------- -------- --------\n" for i, site in enumerate(final_structure): frac_coords = site.frac_coords sites_table += f"{i:3d} {site.species_string:4s} {frac_coords[0]:8.6f} {frac_coords[1]:8.6f} {frac_coords[2]:8.6f}\n" return (f"## Molecular Dynamics Simulation\n\n" f"- **Structure**: `{reduced_formula}`\n" f"- **Structure Status**: `{optimization_status}`\n" f"- **Temperature**: `{temperature_K} K`\n" f"- **Steps**: `{steps}`\n" f"- **Final Potential Energy**: `{float(final_energy):.3f} eV`\n\n" f"### Final Structure Information\n\n" f"- **Space Group**: `{symmetry[0]} (#{symmetry[1]})`\n" f"- **Volume**: `{volume:.2f} ų`\n" f"- **Density**: `{density:.2f} g/cm³`\n" f"- **Lattice Parameters**:\n" f" - a = `{lattice_info.a:.6f} Å`, b = `{lattice_info.b:.6f} Å`, c = `{lattice_info.c:.6f} Å`\n" f" - α = `{lattice_info.alpha:.6f}°`, β = `{lattice_info.beta:.6f}°`, γ = `{lattice_info.gamma:.6f}°`\n\n" f"### Atomic Positions (Fractional Coordinates)\n\n" f"```\n" f"abc : {lattice_info.a:.6f} {lattice_info.b:.6f} {lattice_info.c:.6f}\n" f"angles: {lattice_info.alpha:.6f} {lattice_info.beta:.6f} {lattice_info.gamma:.6f}\n" f"pbc : {final_structure.lattice.pbc[0]!s:5s} {final_structure.lattice.pbc[1]!s:5s} {final_structure.lattice.pbc[2]!s:5s}\n" f"Sites ({len(final_structure)})\n" f"{sites_table}```\n") except Exception as e: return f"Error: {str(e)}" @llm_tool(name="calculate_single_point_energy_M3GNet", description="Calculate single point energy of a crystal structure using M3GNet universal potential, with optional structure optimization") async def calculate_single_point_energy_M3GNet( structure_source: str, optimize_structure: bool = True, fmax: float = 0.01 ) -> str: """ Calculate single point energy of a crystal structure using M3GNet universal potential. Args: structure_source: The name of the structure file (e.g., POSCAR, CIF) or the content string. optimize_structure: Whether to optimize the structure before calculation (default: True). fmax: Maximum force tolerance for structure relaxation in eV/Å (default: 0.01). Returns: A Markdown formatted string containing the calculated potential energy in eV. """ try: # 获取结构(优化或不优化) if optimize_structure: # 使用内部函数优化结构 structure = await _relax_crystal_structure_M3GNet_internal( structure_source=structure_source, fmax=fmax ) # 检查优化是否成功 if isinstance(structure, str) and structure.startswith("Error"): return structure else: # 直接读取结构,不进行优化 structure_content, content_format = read_structure_from_file_name_or_content_string(structure_source) structure = Structure.from_str(structure_content, fmt=content_format) if structure is None: return "Error: Failed to obtain a valid structure" # Load the M3GNet universal potential model pot = matgl.load_model("M3GNet-MP-2021.2.8-PES") # Convert pymatgen structure to ASE atoms ase_adaptor = AseAtomsAdaptor() atoms = ase_adaptor.get_atoms(structure) # Set up the calculator for atoms object calc = PESCalculator(pot) atoms.set_calculator(calc) # Calculate potential energy energy = atoms.get_potential_energy() reduced_formula = structure.composition.reduced_formula # 构建结果字符串 optimization_status = "optimized" if optimize_structure else "non-optimized" # 添加结构信息 lattice_info = structure.lattice volume = structure.volume density = structure.density symmetry = structure.get_space_group_info() # 构建原子位置表格 sites_table = " # SP a b c\n" sites_table += "--- ---- -------- -------- --------\n" for i, site in enumerate(structure): frac_coords = site.frac_coords sites_table += f"{i:3d} {site.species_string:4s} {frac_coords[0]:8.6f} {frac_coords[1]:8.6f} {frac_coords[2]:8.6f}\n" return (f"## Single Point Energy Calculation\n\n" f"- **Structure**: `{reduced_formula}`\n" f"- **Structure Status**: `{optimization_status}`\n" f"- **Potential Energy**: `{float(energy):.3f} eV`\n\n" f"### Structure Information\n\n" f"- **Space Group**: `{symmetry[0]} (#{symmetry[1]})`\n" f"- **Volume**: `{volume:.2f} ų`\n" f"- **Density**: `{density:.2f} g/cm³`\n" f"- **Lattice Parameters**:\n" f" - a = `{lattice_info.a:.6f} Å`, b = `{lattice_info.b:.6f} Å`, c = `{lattice_info.c:.6f} Å`\n" f" - α = `{lattice_info.alpha:.6f}°`, β = `{lattice_info.beta:.6f}°`, γ = `{lattice_info.gamma:.6f}°`\n\n" f"### Atomic Positions (Fractional Coordinates)\n\n" f"```\n" f"abc : {lattice_info.a:.6f} {lattice_info.b:.6f} {lattice_info.c:.6f}\n" f"angles: {lattice_info.alpha:.6f} {lattice_info.beta:.6f} {lattice_info.gamma:.6f}\n" f"pbc : {structure.lattice.pbc[0]!s:5s} {structure.lattice.pbc[1]!s:5s} {structure.lattice.pbc[2]!s:5s}\n" f"Sites ({len(structure)})\n" f"{sites_table}```\n") except Exception as e: return f"Error: {str(e)}" #Error: Bad serialized model or bad model name. It is possible that you have an older model cached. Please clear your cache by running `python -c "import matgl; matgl.clear_cache()"` # @llm_tool(name="predict_band_gap", # description="Predict the band gap of a crystal structure using MEGNet multi-fidelity model from either a chemical formula or CIF file, with structure optimization") # async def predict_band_gap( # formula: str = None, # cif_file_name: str = None, # method: str = "PBE", # fmax: float = 0.01 # ) -> str: # """ # Predict the band gap of a crystal structure using the MEGNet multi-fidelity band gap model. # First optimizes the crystal structure using M3GNet universal potential, then predicts # the band gap based on the relaxed structure for more accurate results. # Accepts either a chemical formula (searches Materials Project database) or a CIF file. # Args: # formula: Chemical formula to retrieve from Materials Project (e.g., "Fe2O3"). # cif_file_name: Name of CIF file in temp directory to use as structure source. # method: The DFT method to use for the prediction. Options are "PBE", "GLLB-SC", "HSE", or "SCAN". # Default is "PBE". # fmax: Maximum force tolerance for structure relaxation in eV/Å (default: 0.01). # Returns: # A string containing the predicted band gap in eV or an error message. # """ # try: # # First, relax the crystal structure # relaxed_result = await relax_crystal_structure( # formula=formula, # cif_file_name=cif_file_name, # fmax=fmax # ) # # Check if relaxation was successful # if isinstance(relaxed_result, str) and relaxed_result.startswith("Error"): # return relaxed_result # # Use the relaxed structure for band gap prediction # structure = relaxed_result # if structure is None: # return "Error: Failed to obtain a valid relaxed structure" # # Load the pre-trained MEGNet band gap model # model = matgl.load_model("MEGNet-MP-2019.4.1-BandGap-mfi") # # Map method name to index # method_map = {"PBE": 0, "GLLB-SC": 1, "HSE": 2, "SCAN": 3} # if method not in method_map: # return f"Error: Unsupported method: {method}. Choose from PBE, GLLB-SC, HSE, or SCAN." # # Set the graph label based on the method # graph_attrs = torch.tensor([method_map[method]]) # # Predict the band gap using the relaxed structure # bandgap = model.predict_structure(structure=structure, state_attr=graph_attrs) # reduced_formula = structure.reduced_formula # # Return the band gap as a string # return f"The predicted band gap for relaxed {reduced_formula} using {method} method is {float(bandgap):.3f} eV." # except Exception as e: # return f"Error: {str(e)}"