diff --git a/docs/pymatgen.md b/docs/pymatgen.md index 90f1e934b24..a72f8e125bf 100644 --- a/docs/pymatgen.md +++ b/docs/pymatgen.md @@ -2454,6 +2454,7 @@ nav_order: 6 * [`ElementBase.is_noble_gas`](pymatgen.core.md#pymatgen.core.periodic_table.ElementBase.is_noble_gas) * [`ElementBase.is_post_transition_metal`](pymatgen.core.md#pymatgen.core.periodic_table.ElementBase.is_post_transition_metal) * [`ElementBase.is_quadrupolar`](pymatgen.core.md#pymatgen.core.periodic_table.ElementBase.is_quadrupolar) + * [`ElementBase.is_rare_earth_metal`](pymatgen.core.md#pymatgen.core.periodic_table.ElementBase.is_rare_earth_metal) * [`ElementBase.is_transition_metal`](pymatgen.core.md#pymatgen.core.periodic_table.ElementBase.is_transition_metal) * [`ElementBase.is_valid_symbol()`](pymatgen.core.md#pymatgen.core.periodic_table.ElementBase.is_valid_symbol) * [`ElementBase.iupac_ordering`](pymatgen.core.md#pymatgen.core.periodic_table.ElementBase.iupac_ordering) diff --git a/src/pymatgen/io/lammps/data.py b/src/pymatgen/io/lammps/data.py index 55148b4b67b..363ddce0513 100644 --- a/src/pymatgen/io/lammps/data.py +++ b/src/pymatgen/io/lammps/data.py @@ -36,10 +36,11 @@ from collections.abc import Sequence from typing import Any, Literal + from numpy.typing import ArrayLike from typing_extensions import Self from pymatgen.core.sites import Site - from pymatgen.core.structure import IStructure, SiteCollection + from pymatgen.core.structure import IMolecule, IStructure, SiteCollection __author__ = "Kiran Mathew, Zhi Deng, Tingzheng Hou" __copyright__ = "Copyright 2018, The Materials Virtual Lab" @@ -259,8 +260,8 @@ class 2 force field are valid keys, and each value is a keys, and each value is a DataFrame. atom_style (str): Output atom_style. Default to "full". """ - if velocities is not None and len(atoms) != len(velocities): - raise ValueError(f"{len(atoms)=} and {len(velocities)=} mismatch") + # if velocities is not None and len(atoms) != len(velocities): + # raise ValueError(f"{len(atoms)=} and {len(velocities)=} mismatch") if force_field: all_ff_kws = SECTION_KEYWORDS["ff"] + SECTION_KEYWORDS["class2"] @@ -851,7 +852,7 @@ def from_structure( cls, structure: Structure | IStructure, ff_elements: Sequence[str] | None = None, - atom_style: Literal["atomic", "charge"] = "charge", + atom_style: Literal["atomic", "charge", "full"] = "charge", is_sort: bool = False, ) -> Self: """Simple constructor building LammpsData from a structure without @@ -863,7 +864,7 @@ def from_structure( be present due to force field settings but not necessarily in the structure. Default to None. atom_style (str): Choose between "atomic" (neutral) and - "charge" (charged). Default to "charge". + "charge" (charged) and "full". Default to "charge". is_sort (bool): whether to sort sites """ struct = structure.get_sorted_structure() if is_sort else structure.copy() @@ -892,6 +893,48 @@ def from_structure( topo = Topology(boxed_s) return cls.from_ff_and_topologies(box=box, ff=ff, topologies=[topo], atom_style=atom_style) + @classmethod + def from_molecule( + cls, molecule: Molecule | IMolecule, box_or_lattice: LammpsBox | Lattice | ArrayLike, **kwargs + ) -> Self: + """Simple constructor building LammpsData from a molecule and a Lattice without + force field parameters. When dealing with molecules however, it is RECOMMENDED to + define a LammpsData object from a ForceField object and a Topology object + instead, which allows for more flexibility in defining force field parameters and + topologies. This implementation is mainly for convenience, and does not assign + "bonds" and "angles" sections in the topology! + + Args: + molecule (Molecule): Input molecule. + box_or_lattice (LammpsBox or Lattice or ArrayLike): Simulation box or lattice. + ff_elements ([str]): List of strings of elements that must + be present due to force field settings but not + necessarily in the molecule. Default to None. + atom_style (str): Choose between "atomic" (neutral) and + "charge" (charged) and "full". Default to "charge". + """ + + if isinstance(box_or_lattice, LammpsBox): + box_or_lattice = box_or_lattice.to_lattice() + + box_or_lattice.pbc = (False, False, False) + + structure = Structure( + lattice=box_or_lattice, + species=molecule.species, + coords=molecule.cart_coords, + site_properties=molecule.site_properties if hasattr(molecule, "site_properties") else {}, + charge=molecule.charge if hasattr(molecule, "charge") else 0, + coords_are_cartesian=True, + labels=molecule.labels if hasattr(molecule, "labels") else None, + properties=molecule.properties if hasattr(molecule, "properties") else {}, + ) + + return cls.from_structure( + structure=structure, + **kwargs, + ) + def set_charge_atom(self, charges: dict[int, float]) -> None: """Set the charges of specific atoms of the data. @@ -1299,7 +1342,7 @@ def __init__( self._coordinates.index = self._coordinates.index.map(int) max_xyz = self._coordinates[["x", "y", "z"]].max().max() min_xyz = self._coordinates[["x", "y", "z"]].min().min() - self.box = LammpsBox(np.array(3 * [[min_xyz - 0.5, max_xyz + 0.5]])) + self.box = LammpsBox(3 * [[min_xyz - 0.5, max_xyz + 0.5]]) self.atom_style = atom_style self.n = sum(self._list_of_numbers) self.names = [] diff --git a/src/pymatgen/io/lammps/generators.py b/src/pymatgen/io/lammps/generators.py index d541dd1adf4..636e9a92391 100644 --- a/src/pymatgen/io/lammps/generators.py +++ b/src/pymatgen/io/lammps/generators.py @@ -10,24 +10,468 @@ from __future__ import annotations import os +import warnings from dataclasses import dataclass, field +from pathlib import Path from string import Template +from typing import TYPE_CHECKING +import numpy as np from monty.io import zopen +from monty.json import MSONable -from pymatgen.core import Structure +from pymatgen.core import Lattice, Molecule, Structure from pymatgen.io.core import InputGenerator -from pymatgen.io.lammps.data import CombinedData, LammpsData +from pymatgen.io.lammps.data import CombinedData, LammpsBox, LammpsData from pymatgen.io.lammps.inputs import LammpsInputFile from pymatgen.io.lammps.sets import LammpsInputSet +from pymatgen.util.typing import PathLike -__author__ = "Ryan Kingsbury, Guillaume Brunin (Matgenix)" -__copyright__ = "Copyright 2021, The Materials Project" -__version__ = "0.2" +if TYPE_CHECKING: + from typing import Literal + + from numpy.typing import ArrayLike MODULE_DIR = os.path.dirname(os.path.abspath(__file__)) TEMPLATE_DIR = f"{MODULE_DIR}/templates" +_COMMON_LAMMPS_SETTINGS = { + "dimension": 3, + "pair_style": "lj/cut 10.0", + "thermo": 100, + "start_temp": 300.0, + "end_temp": 300.0, + "start_pressure": 0.0, + "end_pressure": 0.0, + "log_interval": 100, + "traj_interval": 100, + "ensemble": "nvt", + "thermostat": "nose-hoover", + "barostat": None, + "nsteps": 1000, + "restart": "", + "tol": 1e-6, + "min_style": "cg", +} + +_BASE_LAMMPS_SETTINGS = { + "periodic": _COMMON_LAMMPS_SETTINGS + | { + "units": "metal", + "atom_style": "atomic", + "boundary": ("p", "p", "p"), + "timestep": 0.001, + "friction": 0.1, + }, + "molecular": _COMMON_LAMMPS_SETTINGS + | { + "units": "real", + "atom_style": "full", + "boundary": ("f", "f", "f"), + "timestep": 1, + "friction": 100, + }, +} + + +FF_STYLE_KEYS = ["pair_style", "bond_style", "angle_style", "dihedral_style", "improper_style", "kspace_style"] +FF_COEFF_KEYS = ["pair_coeff", "bond_coeff", "angle_coeff", "dihedral_coeff", "improper_coeff", "kspace_coeff"] + +LAMMPS_DEFINED_TYPES: dict[str, set[str | None]] = { + "units": {"metal", "lj", "real", "si", "cgs", "electron", "micro", "nano"}, + "atom_style": {"atomic", "angle", "body", "bond", "charge", "electron", "full", "molecular"}, + "boundary": {"p", "f", "s", "m", "fs", "fm"}, + "ensemble": {"nve", "nvt", "npt", "nph", "minimize"}, + "thermostat": {"nose-hoover", "langevin", None}, + "barostat": {"nose-hoover", "berendsen", "langevin", None}, + "min_style": {"cg", "sd", "fire", "hftn", "quickmin", "spin", "spin/cg", "spin/lbfgs"}, +} + + +@dataclass +class LammpsSettings(MSONable): + """Define schema for LAMMPS convenience input class. + + The idea is to dynamically set the attributes of this class + based on the LAMMPS input file template and the settings + dictionary provided by the user. This way, standard inputs + that do not typically vary (e.g., units, atom style, dimension, + boundary conditions, styles) can be validated and set + while allowing the user to specify the rest of the settings in + a flexible manner (which won't be explicitly validated here). + + Args: + units : str + LAMMPS units style. + atom_style : str + LAMMPS atom style. + dimension : int + LAMMPS box dimension. + boundary : tuple[str,str,str] + Box boundary conditions. Each boundary can be 'p', 'f', 's', 'm', 'fs', 'fm'. + pair_style : str + FF pair style type. Default is 'lj/cut 10.0'. + bond_style : str + FF bond style type. + angle_style : str + FF angle style type. + dihedral_style : str + FF dihedral style type. + start_temp : float + Initial temperature. Default is 300 K. Initial velocities are generated based on this temperature. + end_temp : float + Final temperature. Default is 300 K. + start_pressure : float | list | np.ndarray + Initial pressure. Default is 0 atm. Can be a single value or a list for anisotropic pressure. + end_pressure : float | list | np.ndarray + Final pressure. Default is 0 atm. Can be a single value or a list for anisotropic pressure. + timestep : float + Simulation timestep. Default is 0.001 ps for periodic solids and 1fs for molecular systems. + friction : float + Thermostat/Barostat friction coefficient. Default is 0.1 ps^-1 for periodic solids + and 100 fs^-1 for molecular systems. + log_interval : int + Interval for logging thermodynamic data (energy, temperature, pressure, volume). + Default is 100 steps. + traj_interval : int + Interval for writing trajectory data (positions, velocities, forces) to a dump file. + Default is 100 steps. + ensemble : str + Simulation ensemble. Default is 'nvt'. + thermostat : str + Thermostat type. Default is 'nose-hoover'. + barostat : str + Barostat type. Default is 'nose-hoover'. + nsteps : int + Number of simulation steps. Default is 1000. This is also the maximum number of + steps for minimization simulations. + """ + + validate_params: bool = field(default=False) + + def __init__(self, validate_params: bool = True, **kwargs): + for key, value in kwargs.items(): + setattr(self, key, value) + if validate_params: + self.__post_init__() + + def __post_init__(self) -> None: + """Validate input values.""" + + for attr, accept_vals in LAMMPS_DEFINED_TYPES.items(): + curr_val = getattr(self, attr, None) + if isinstance(curr_val, list | tuple): + is_ok = all(v in accept_vals for v in curr_val) + else: + is_ok = curr_val in accept_vals + if not is_ok: + raise ValueError(f"Error validating key {attr}: set to {curr_val}, should be one of {accept_vals}.") + + if self.restart and not isinstance(self.restart, str): + raise ValueError( + f"restart should be the path to the restart file from the previous run, got {self.restart}." + ) + + if self.ensemble not in ["nve", "minimize"]: + if isinstance(self.start_pressure, (list, np.ndarray)) and len(self.start_pressure) != 3: + raise ValueError(f"start_pressure should be a list of 3 values, got {self.start_pressure}.") + if isinstance(self.end_pressure, (list, np.ndarray)) and len(self.end_pressure) != 3: + raise ValueError(f"end_pressure should be a list of 3 values, got {self.end_pressure}.") + + if (self.thermostat or self.barostat) and self.friction < self.timestep: + warnings.warn( + f"Friction ({self.friction}) is smaller than the timestep ({self.timestep}). " + "This may lead to numerical instability.", + stacklevel=2, + ) + + if self.ensemble == "minimize": + if self.nsteps < 1: + raise ValueError(f"nsteps should be greater than 0 for minimization simulations, got {self.nsteps}.") + if self.tol > 1e-4: + warnings.warn( + f"Tolerance for minimization ({self.tol}) is larger than 1e-4. " + "This may lead to inaccurate results.", + stacklevel=2, + ) + + @property + def dict(self) -> dict: + return self.__dict__ + + +@dataclass +class BaseLammpsSetGenerator(InputGenerator): + """ + Base class for generating LAMMPS input sets. + + Args: + inputfile : LammpsInputFile | str | Path + Premade input file for the LAMMPS simulation. + Useful if the user wants to use a custom input file + (to make use of Lammps' flexibility). + Default format based on the md.template file in the templates directory. + settings : dict | LammpsInputSettings + Settings for the LAMMPS simulation. Default settings are given in the + _BASE_LAMMPS_SETTINGS object in 'metal' units for reference. + force_field : Path | dict | ForceField + Force field file or dictionary containing the force field parameters. + Default is None. + calc_type : str + Type of calculation to be performed by LAMMPS. + keep_stages : bool + Whether to keep the stages of the input file or not. Default is True. + override_updates : bool + Whether to override the updates to the input file, i.e., + keep the input file as is. Default is False. + """ + + inputfile: LammpsInputFile | str | PathLike = field(default=None) + settings: dict | LammpsSettings = field(default_factory=dict) + force_field: Path | dict | None = field(default=None) + data_type: Literal["periodic", "molecular"] = field(default="periodic") + calc_type: str = field(default="lammps") + include_defaults: bool = field(default=True) + validate_params: bool = field(default=True) + keep_stages: bool = field(default=True) + + def __post_init__(self): + """Determine correct template requested by user. + + If not inputfile, we assume the user is going to use the default template for + one the defined flows (NVT/NVE/NPT, Minimize, MeltQuench, etc.) + Instead, if the user/another flow specifies a template, we'll use that and an + optional settings dict with it and not bother with validating the inputs in the + inputfile due to the amount of flexibility LAMMPS allows + If another flow is used that defines it's own inputfile template, we'll leave the + validation of the user's inputs to that flow's init method. + + """ + if self.inputfile is None: + ensemble = ( + self.settings.get("ensemble", "nvt") if isinstance(self.settings, dict) else self.settings.ensemble + ) + + file: str | None = None + if ensemble in ["nve", "nvt", "npt", "nph"]: + file = "md.template" + elif ensemble in ["minimize"]: + file = "minimization.template" + if not file: + raise ValueError( + f"Unknown {ensemble=}; acceptable values are\n{', '.join(LAMMPS_DEFINED_TYPES['ensemble'])}" + ) + + self.inputfile = LammpsInputFile.from_file(os.path.join(TEMPLATE_DIR, file), keep_stages=self.keep_stages) + + if isinstance(self.settings, dict): + settings = self.settings.copy() + if self.include_defaults: + base_settings = _BASE_LAMMPS_SETTINGS[self.data_type].copy() + settings = base_settings | settings + self.settings = LammpsSettings(validate_params=self.validate_params, **settings) + + def update_settings(self, updates: dict, validate_params: bool = True) -> None: + """ + Update the settings for the LammpsSettings object. + Args: + updates : dict + Dictionary containing the settings to update. + """ + if isinstance(self.settings, LammpsSettings): + present_settings = self.settings.dict + for k, v in updates.items(): + present_settings.update({k: v}) + self.settings = LammpsSettings(validate_params=validate_params, **present_settings) + else: + self.settings.update(updates) + + def get_input_set( + self, + data: Structure | Molecule | LammpsData | CombinedData, + additional_data: LammpsData | CombinedData | None = None, + box_or_lattice: Lattice | LammpsBox | ArrayLike | None = None, + **kwargs, + ) -> LammpsInputSet: + """ + Generate a LAMMPS input set. + Args: + data : Structure | Molecule | LammpsData | CombinedData + Structure, Molecule, LammpsData or CombinedData object containing the atomic structure + to be simulated. + additional_data : LammpsData | CombinedData | None + Additional data to be written to the input set, such as extra data files. + Default is None. + data_type : Literal["periodic", "molecular"] + Type of data provided. Used to determine the atom style and other settings. + Default is "periodic". If a Molecule is provided, this will be set to "molecular". + box_or_lattice : Lattice | LammpsBox | ArrayLike | None + Optional arg needed when data is a Molecule object. + **kwargs : dict + Additional keyword arguments to be passed to the LammpsInputSet constructor. + """ + + if not self.force_field: + warnings.warn( + "Force field not specified! Ensure you have the correct force field parameters " + "in the data file/settings or will specify it manually using " + "maker.input_set_generator.force_field.", + stacklevel=2, + ) + + if isinstance(self.inputfile, PathLike): + try: + self.inputfile = LammpsInputFile.from_file(self.inputfile, keep_stages=self.keep_stages) + except FileNotFoundError: + try: + self.inputfile = LammpsInputFile.from_str(self.inputfile, keep_stages=self.keep_stages) + except ValueError: + raise FileNotFoundError( + f"Input file {self.inputfile} not found. It was neither a path " + "nor a string repr of the inputfile. Please check your inputs!" + ) + + data_type = "molecular" if isinstance(data, Molecule) else "periodic" + if data_type != self.data_type and self.include_defaults: + molecule_updates = _BASE_LAMMPS_SETTINGS[data_type].copy() + self.update_settings(molecule_updates, validate_params=self.validate_params) + + settings_dict = self.settings.dict.copy() if isinstance(self.settings, LammpsSettings) else self.settings + atom_style = settings_dict.get("atom_style", "full") + print(f"Generating LAMMPS input set with settings: {settings_dict}") + + # Attempt to read data file and convert to LammpsData object + if isinstance(data, Path): + try: + data = LammpsData.from_file(data, atom_style=atom_style) + except FileNotFoundError: + raise FileNotFoundError(f"Data file {data} not found. Please check the path.") + + if isinstance(data, str): + try: + data = LammpsData.from_str(data, atom_style=atom_style) + except ValueError: + raise ValueError(f"Data file {data} not recognized. Please check the format.") + + species = "" + if isinstance(data, Structure | Molecule): + species = " ".join({s.symbol for s in data.species}) + warnings.warn("Structure or Molecule provided, converting to LammpsData object.", stacklevel=2) + if isinstance(data, Structure): + data = LammpsData.from_structure(data, atom_style=atom_style) + elif isinstance(data, Molecule): + data = LammpsData.from_molecule(data, atom_style=atom_style, box_or_lattice=box_or_lattice) + + # Housekeeping to fill up the default settings for the MD template + settings_dict.update({f"{sys}_flag": "###" for sys in ["nve", "nvt", "npt", "nph", "restart", "extra_data"]}) + settings_dict.update({"read_data_flag": "read_data", "psymm": "iso"}) + # If the ensemble is not 'minimize', we set the read_data_flag to read_data + + # Accounts for the restart file + if settings_dict.get("read_restart", None): + print("Restart file provided, setting read_restart flag.") + settings_dict.update( + { + "read_restart": f"{settings_dict['read_restart']}", + "restart_flag": "read_restart", + "read_data_flag": "###", + } + ) + + # Convert start and end pressure to string if they are lists or arrays, and set psymm to accordingly + if isinstance(settings_dict.get("start_pressure", None), (list, np.ndarray)): + settings_dict.update( + {"start_pressure": " ".join(map(str, settings_dict["start_pressure"])), "psymm": "aniso"} + ) + if isinstance(settings_dict.get("end_pressure", None), (list, np.ndarray)): + settings_dict.update({"end_pressure": " ".join(map(str, settings_dict["end_pressure"])), "psymm": "aniso"}) + + # Loop over the LammpsSettings object and update the settings dictionary + for attr, val in self.settings.dict.items(): # type: ignore[union-attr] + if attr == "boundary": + settings_dict.update({"boundary": " ".join(list(val))}) + + elif attr == "ensemble": + settings_dict.update({f"{val}_flag": "fix"}) + + elif attr == "thermostat": + if val == "langevin": + settings_dict.update({"nve_flag": "fix", "thermseed": 42, "thermostat": "langevin"}) + if val == "nose-hoover": + settings_dict.update({"thermostat": "nvt temp", "thermseed": ""}) + + elif attr == "barostat": + if val == "nose-hoover": + settings_dict.update( + { + "barostat": "npt temp", + "start_temp_barostat": settings_dict["start_temp"], + "end_temp_barostat": settings_dict["end_temp"], + "tfriction_barostat": settings_dict["friction"], + } + ) + + if val in ["berendsen", "langevin"]: + settings_dict.update( + { + "barostat": "langevin" if val == "langevin" else "press/berendsen", + "nve_flag": "fix", + "nvt_flag": "fix", + "start_temp_barostat": "", + "end_temp_barostat": "", + "tfriction_barostat": "", + "thermostat": f"temp/{val}", + } + ) + settings_dict.update({"thermoseed": 42 if val == "langevin" else ""}) + + elif attr == "friction": + settings_dict.update({"tfriction": val, "pfriction": val}) + + elif val: + settings_dict.update({attr: val}) + + # Handle the force field input by writing a separate FF file + # and making the necessary updates to the settings dict + FF_string = "" + if isinstance(self.force_field, str): + FF_string += self.force_field + settings_dict.update({f"{ff}_flag": "###" for ff in FF_STYLE_KEYS}) + + if isinstance(self.force_field, dict): + for key, value in self.force_field.items(): + if key in FF_STYLE_KEYS and value: + settings_dict.update({f"{key}": value, f"{key}_flag": f"{key}"}) + if key in FF_COEFF_KEYS and value: + FF_string += f"{key} {value}\n" + if key in ["species"]: + # makes species specified in FF dict take precedence + species = " ".join(value) if isinstance(value, list) else value + else: + warnings.warn(f"Force field key {key} not recognized, will be ignored.", stacklevel=2) + + for ff_key in FF_STYLE_KEYS: + if ff_key not in self.settings.dict or not self.settings.dict[ff_key]: # type: ignore[union-attr] + settings_dict.update({f"{ff_key}_flag": "###"}) + warnings.warn(f"Force field key {ff_key} not found in the force field dictionary.", stacklevel=2) + + settings_dict.update({"dump_modify_flag": "dump_modify" if species else "###", "species": species}) + write_data = {"forcefield.lammps": FF_string} + if additional_data: + write_data.update({"extra.data": additional_data}) + settings_dict.update({"extra_data_flag": "include"}) + + # Replace all variables + input_str = Template(self.inputfile.get_str()).safe_substitute(**settings_dict) # type: ignore[union-attr] + lines = input_str.split("\n") + # Filter out the lines where the substitution resulted in a line starting with '###' + filtered_input_str = "\n".join([line for line in lines if not line.lstrip().startswith("###")]) + input_file = LammpsInputFile.from_str(filtered_input_str, keep_stages=self.keep_stages) + + return LammpsInputSet( + inputfile=input_file, data=data, calc_type=self.calc_type, additional_data=write_data, **kwargs + ) + @dataclass class BaseLammpsGenerator(InputGenerator): @@ -56,7 +500,7 @@ class BaseLammpsGenerator(InputGenerator): """ inputfile: LammpsInputFile | None = field(default=None) - template: str = field(default_factory=str) + template: str | Path | LammpsInputFile = field(default_factory=str) data: LammpsData | CombinedData | None = field(default=None) settings: dict = field(default_factory=dict) calc_type: str = field(default="lammps") @@ -64,11 +508,21 @@ class BaseLammpsGenerator(InputGenerator): def get_input_set(self, structure: Structure | LammpsData | CombinedData) -> LammpsInputSet: """Generate a LammpsInputSet from the structure/data, tailored to the template file.""" - data: LammpsData = LammpsData.from_structure(structure) if isinstance(structure, Structure) else structure + + data = ( + LammpsData.from_structure(structure, atom_style=self.settings.get("atom_style", "full")) + if isinstance(structure, Structure) + else structure + ) # Load the template - with zopen(self.template, mode="rt", encoding="utf-8") as file: - template_str = file.read() + if Path(self.template).is_file(): + with zopen(self.template, mode="rt", encoding="utf-8") as file: + template_str = file.read() + elif isinstance(self.template, LammpsInputFile): + template_str = self.template.get_str() + else: + template_str = self.template # Replace all variables input_str = Template(template_str).safe_substitute(**self.settings) @@ -82,98 +536,3 @@ def get_input_set(self, structure: Structure | LammpsData | CombinedData) -> Lam calc_type=self.calc_type, template_file=self.template, ) - - -class LammpsMinimization(BaseLammpsGenerator): - """ - Generator that yields a LammpsInputSet tailored for minimizing the energy of a system by iteratively - adjusting atom coordinates. - Example usage: - ``` - structure = Structure.from_file("mp-149.cif") - lmp_minimization = LammpsMinimization(units="atomic").get_input_set(structure) - ```. - - Do not forget to specify the force field, otherwise LAMMPS will not be able to run! - - This InputSet and InputGenerator implementation is based on templates and is not intended to be very flexible. - For instance, pymatgen will not detect whether a given variable should be adapted based on others - (e.g., the number of steps from the temperature), it will not check for convergence nor will it actually run LAMMPS. - For additional flexibility and automation, use the atomate2-lammps implementation - (https://github.com/Matgenix/atomate2-lammps). - """ - - def __init__( - self, - template: str | None = None, - units: str = "metal", - atom_style: str = "full", - dimension: int = 3, - boundary: str = "p p p", - read_data: str = "system.data", - force_field: str = "Unspecified force field!", - keep_stages: bool = False, - ) -> None: - r""" - Args: - template: Path (string) to the template file used to create the InputFile for LAMMPS. - units: units to be used for the LAMMPS calculation (see LAMMPS docs). - atom_style: atom_style to be used for the LAMMPS calculation (see LAMMPS docs). - dimension: dimension to be used for the LAMMPS calculation (see LAMMPS docs). - boundary: boundary to be used for the LAMMPS calculation (see LAMMPS docs). - read_data: read_data to be used for the LAMMPS calculation (see LAMMPS docs). - force_field: force field to be used for the LAMMPS calculation (see LAMMPS docs). - Note that you should provide all the required information as a single string. - In case of multiple lines expected in the input file, - separate them with '\n' in force_field. - keep_stages: If True, the string is formatted in a block structure with stage names - and newlines that differentiate commands in the respective stages of the InputFile. - If False, stage names are not printed and all commands appear in a single block. - """ - if template is None: - template = f"{TEMPLATE_DIR}/minimization.template" - settings = { - "units": units, - "atom_style": atom_style, - "dimension": dimension, - "boundary": boundary, - "read_data": read_data, - "force_field": force_field, - } - - super().__init__( - template=template, - settings=settings, - calc_type="minimization", - keep_stages=keep_stages, - ) - - @property - def units(self) -> str: - """The argument of the command 'units' passed to the generator.""" - return self.settings["units"] - - @property - def atom_style(self) -> str: - """The argument of the command 'atom_style' passed to the generator.""" - return self.settings["atom_style"] - - @property - def dimension(self) -> int: - """The argument of the command 'dimension' passed to the generator.""" - return self.settings["dimension"] - - @property - def boundary(self) -> str: - """The argument of the command 'boundary' passed to the generator.""" - return self.settings["boundary"] - - @property - def read_data(self) -> str: - """The argument of the command 'read_data' passed to the generator.""" - return self.settings["read_data"] - - @property - def force_field(self) -> str: - """The details of the force field commands passed to the generator.""" - return self.settings["force_field"] diff --git a/src/pymatgen/io/lammps/sets.py b/src/pymatgen/io/lammps/sets.py index 4aab10d3a2b..7e30d21cb7b 100644 --- a/src/pymatgen/io/lammps/sets.py +++ b/src/pymatgen/io/lammps/sets.py @@ -48,6 +48,7 @@ def __init__( data: LammpsData | CombinedData, calc_type: str = "", template_file: PathLike = "", + additional_data: dict | None = None, keep_stages: bool = False, ) -> None: """ @@ -68,8 +69,13 @@ def __init__( self.calc_type = calc_type self.template_file = template_file self.keep_stages = keep_stages + self.additional_data = additional_data - super().__init__(inputs={"in.lammps": self.inputfile, "system.data": self.data}) + inputs = {"in.lammps": self.inputfile, "input.data": self.data} + if self.additional_data: + inputs.update(self.additional_data) + + super().__init__(inputs=inputs) @classmethod def from_directory(cls, directory: PathLike, keep_stages: bool = False) -> Self: diff --git a/src/pymatgen/io/lammps/templates/md.template b/src/pymatgen/io/lammps/templates/md.template index 4f9a069b014..b2789d3f507 100644 --- a/src/pymatgen/io/lammps/templates/md.template +++ b/src/pymatgen/io/lammps/templates/md.template @@ -2,34 +2,44 @@ # Initialization -units metal -atom_style atomic +units $units +atom_style $atom_style +dimension $dimension +boundary $boundary # Atom definition -read_data md.data -#read_restart md.restart +$restart_flag $read_restart +$read_data_flag input.data + # Force field settings (consult official document for detailed formats) -$force_field +$pair_style_flag $pair_style +$bond_style_flag $bond_style +$angle_style_flag $angle_style +$dihedral_style_flag $dihedral_style +$improper_style_flag $improper_style + +include forcefield.lammps +$extra_data_flag extra.data # Create velocities -velocity all create $temperature 142857 mom yes rot yes dist gaussian +velocity all create $start_temp 42 mom yes rot yes dist gaussian +neigh_modify delay 5 every 1 check yes # Ensemble constraints -#fix 1 all nve -fix 1 all nvt temp $temperature $temperature 0.1 -#fix 1 all npt temp $temperature $temperature 0.1 iso $pressure $pressure 1.0 - -# Various operations within timestepping -#fix ... -#compute ... +$nve_flag 1 all nve +$nvt_flag 2 all $thermostat $start_temp $end_temp $tfriction $thermseed +$npt_flag 3 all $barostat $start_temp_barostat $end_temp_barostat $tfriction_barostat $psymm $start_pressure $end_pressure $pfriction # Output settings -#thermo_style custom ... # control the thermo data type to output -thermo 100 # output thermo data every N steps -#dump 1 all atom 100 traj.*.gz # dump a snapshot every 100 steps +thermo $log_interval +dump d1 all custom $traj_interval traj.dump id element x y z vx vy vz fx fy fz +$dump_modify_flag d1 sort id element $species # Actions +timestep $timestep run $nsteps +write_restart md.restart +write_data output.data diff --git a/src/pymatgen/io/lammps/templates/minimization.template b/src/pymatgen/io/lammps/templates/minimization.template index e87f8f01297..204236d0339 100644 --- a/src/pymatgen/io/lammps/templates/minimization.template +++ b/src/pymatgen/io/lammps/templates/minimization.template @@ -5,21 +5,28 @@ dimension $dimension boundary $boundary # 2) System definition -read_data $read_data -neigh_modify every 1 delay 5 check yes +read_data input.data +neigh_modify every 1 delay 0 check yes # 3) Simulation settings -$force_field +$pair_style_flag $pair_style +$bond_style_flag $bond_style +$angle_style_flag $angle_style +$dihedral_style_flag $dihedral_style +$improper_style_flag $improper_style + +include forcefield.lammps +$extra_data_flag extra.data # 4) Energy minimization thermo 5 thermo_style custom step lx ly lz press pxx pyy pzz pe dump dmp all atom 5 run.dump -min_style cg -fix 1 all box/relax iso 0.0 vmax 0.001 -minimize 1.0e-16 1.0e-16 5000 100000 +min_style $min_style +fix 1 all box/relax $psymm $start_pressure vmax 0.001 +minimize $tol $tol $nsteps 10000000 # 5) Write output data -write_data run.data +write_data output.data write_restart run.restart