diff --git a/pymatgen/io/mw/__init__.py b/pymatgen/io/mw/__init__.py new file mode 100644 index 00000000000..e28be77c9be --- /dev/null +++ b/pymatgen/io/mw/__init__.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from .inputs import MWInput + +__all__ = ["MWInput"] diff --git a/pymatgen/io/mw/electrodes.py b/pymatgen/io/mw/electrodes.py new file mode 100644 index 00000000000..1adcd1404df --- /dev/null +++ b/pymatgen/io/mw/electrodes.py @@ -0,0 +1,222 @@ +"""Electrodes submodule of Metalwalls input file""" + +from __future__ import annotations + +from monty.json import MSONable + + +class ElectrodeType(MSONable): + """Class to represent an electrode type in a simulation configuration. + + :param name: Unique identifier for this electrode species. Must be up to 8 ASCII characters and + cannot contain white spaces, '#' or '!'. + :type name: str + :param species: The name of the species used to fill this electrode. + :type species: str + :param potential: Constant potential for this electrode type. Default is 0.0. + :type potential: float, optional + :param piston: Parameters for NPT-like simulation. The first element is the target pressure, + and the second element is the direction of the applied force (should be ±1). + This option automatically sets compute_force to true. The piston can only be + applied with the conjugate gradient method. Default is (0.0, 1). + :type piston: tuple(float, int), optional + :param thomas_fermi_length: Thomas-Fermi length for this electrode. Default is 0.0. + :type thomas_fermi_length: float, optional + :param voronoi_volume: Voronoi volume for this electrode. + :type voronoi_volume: float, optional + """ + + def __init__( + self, + name: str, + species: str, + potential: float = 0.0, + piston: tuple[float, int] | None = (0.0, 1), + thomas_fermi_length: float | None = 0.0, + voronoi_volume: float | None = None, + ): + self.name = name + self.species = species + self.potential = potential + self.piston = piston + self.thomas_fermi_length = thomas_fermi_length + self.voronoi_volume = voronoi_volume + + @classmethod + def from_dict(cls, d: dict) -> ElectrodeType: + """ + Create an ElectrodeType object from a dictionary. + + :param d: The dictionary containing the electrode type information. + :type d: dict + :return: An ElectrodeType object created from the dictionary. + :rtype: ElectrodeType + """ + return cls( + name=d["name"], + species=d["species"], + potential=d.get("potential", 0.0), + piston=d.get("piston", (0.0, 1)), + thomas_fermi_length=d.get("thomas_fermi_length", 0.0), + voronoi_volume=d.get("voronoi_volume"), + ) + + def as_dict(self) -> dict: + """ + Convert the ElectrodeType object to a dictionary. + + :return: A dictionary representing the ElectrodeType object. + :rtype: dict + """ + return { + "name": self.name, + "species": self.species, + "potential": self.potential, + "piston": self.piston, + "thomas_fermi_length": self.thomas_fermi_length, + "voronoi_volume": self.voronoi_volume, + } + + +class ElectrodeCharges(MSONable): + """ + Class representing electrode charges configuration for a simulation. + + :param method: The method used to compute the charges on electrodes at each time step. + Supported methods: 'constant_charge', 'matrix_inversion', 'cg', 'maze_inversion', + 'maze_iterative_shake'. + :type method: str + :param tolerance: The tolerance criterion for convergence. Default is 1.0e-12. + :type tolerance: float, optional + :param max_iterations: The maximum number of iterations allowed. Default is 100. + :type max_iterations: int, optional + :param preconditioner: The method used for preconditioning the conjugate gradient algorithm. + Only specified if method is 'cg'. Default is None. + :type preconditioner: str, optional + :param nblocks: The level of approximation for the SHAKE matrix. Only specified if method is + 'maze_iterative_shake'. + Default is 0. + :type nblocks: int, optional + """ + + def __init__( + self, + method: str, + tolerance: float | None = 1.0e-12, + max_iterations: int | None = 100, + preconditioner: str | None = None, + nblocks: int | None = 0, + ): + known_methods = [ + "constant_charge", + "matrix_inversion", + "cg", + "maze_inversion", + "maze_iterative_shake", + ] + if method not in known_methods: + raise ValueError(f"Unknown method '{method}'. Supported methods are: {', '.join(known_methods)}") + self.method = method + self.tolerance = tolerance + self.max_iterations = max_iterations + self.preconditioner = preconditioner + self.nblocks = nblocks + + def as_dict(self): + """ + Convert the ElectrodeCharges object to a dictionary. + + :return: A dictionary representing the ElectrodeCharges object. + :rtype: dict + """ + return { + "method": self.method, + "tolerance": self.tolerance, + "max_iterations": self.max_iterations, + "preconditioner": self.preconditioner, + "nblocks": self.nblocks, + } + + @classmethod + def from_dict(cls, d): + """ + Create an ElectrodeCharges object from a dictionary. + + :param d: The dictionary containing the ElectrodeCharges information. + :type d: dict + :return: An ElectrodeCharges object created from the dictionary. + :rtype: ElectrodeCharges + """ + return cls( + method=d["method"], + tolerance=d.get("tolerance", 1.0e-12), + max_iterations=d.get("max_iterations", 100), + preconditioner=d.get("preconditioner"), + nblocks=d.get("nblocks", 0), + ) + + +class DipolesAndElectrodes(MSONable): + """DipolesAndElectrodes class representing the dipoles_and_electrodes block in the simulation. + + Attributes: + algorithm (str): Algorithm used to compute dipoles and electrode charges. + Available options: + - 'cg': Consistently computes dipoles and electrode charges using a conjugate-gradient method. + Additional parameters: tolerance (real), max_iterations (int). + - 'cg_and_constant_charge': Keeps electrode charges constant while computing dipoles using cg. + Additional parameters: tolerance (real), max_iterations (int). + tolerance (float): Tolerance criterion for the convergence. Default is 1.0e-12. + max_iterations (int): Maximum number of iterations allowed. Default is 100. + preconditioner (str): Preconditioner method used for the conjugate-gradient algorithm. + Available options: 'jacobi'. + + charge_neutrality (bool): Indicates whether the charge neutrality constraint is used. + Default is True. + global_or_electrodes (str): Specifies the scope of the charge neutrality constraint. + Available options: 'global', 'electrodes'. + Default is 'global'. + """ + + def __init__( + self, + algorithm: str, + tolerance: float = 1.0e-12, + max_iterations: int = 100, + preconditioner: str | None = None, + charge_neutrality: bool = True, + global_or_electrodes: str | None = "global", + ): + known_algorithms = ["cg", "cg_and_constant_charge"] + if algorithm not in known_algorithms: + raise ValueError( + f"Unknown algorithm '{algorithm}'. Supported algorithms are: {', '.join(known_algorithms)}" + ) + + self.algorithm = algorithm + self.tolerance = tolerance + self.max_iterations = max_iterations + self.preconditioner = preconditioner + self.charge_neutrality = charge_neutrality + self.global_or_electrodes = global_or_electrodes + + def as_dict(self): + return { + "algorithm": self.algorithm, + "tolerance": self.tolerance, + "max_iterations": self.max_iterations, + "preconditioner": self.preconditioner, + "charge_neutrality": self.charge_neutrality, + "global_or_electrodes": self.global_or_electrodes, + } + + @classmethod + def from_dict(cls, d): + return cls( + algorithm=d["algorithm"], + tolerance=d["tolerance"], + max_iterations=d["max_iterations"], + preconditioner=d.get("preconditioner"), + charge_neutrality=d.get("charge_neutrality", True), + global_or_electrodes=d.get("global_or_electrodes", "global"), + ) diff --git a/pymatgen/io/mw/inputs.py b/pymatgen/io/mw/inputs.py new file mode 100644 index 00000000000..6f136549ebf --- /dev/null +++ b/pymatgen/io/mw/inputs.py @@ -0,0 +1,583 @@ +"""This module defines the classes and methods for the input of metalwalls +(https://gitlab.com/ampere2/metalwalls) +""" +from __future__ import annotations + +from monty.json import MSONable + +from .electrodes import ElectrodeCharges, ElectrodeType +from .interactions import Interactions +from .molecules import MoleculeType +from .species import DipolesMinimization, RadiusMinimization, SpeciesType + + +class GlobalParams(MSONable): + """Global simulation parameter for MetalWalls + + Args: + MSONable (num_steps, timestep, temperature, num_pbc): _description_ + """ + + def __init__( + self, + num_steps: int, + timestep: float, + temperature: float, + num_pbc: int = 3, + ): + self.num_steps = num_steps + self.timestep = timestep + self.temperature = temperature + self.num_pbc = num_pbc + + def as_dict(self): + return { + "num_steps": self.num_steps, + "timestep": self.timestep, + "temperature": self.temperature, + "num_pbc": self.num_pbc, + } + + @classmethod + def from_dict(cls, d): + return cls( + num_steps=d["num_steps"], + timestep=d["timestep"], + temperature=d["temperature"], + ) + + +class Thermostat(MSONable): + """_summary_ + + Args: + MSONable (_type_): _description_ + """ + + def __init__( + self, + chain_length: int = 5, + relaxation_time: float = 4134.1, + max_iteration: int = 50, + tolerance: float = 1e-15, + ): + self.chain_length = chain_length + self.relaxation_time = relaxation_time + self.tolerance = tolerance + self.max_iteration = max_iteration + + def as_dict(self): + return { + "chain_length": self.chain_length, + "relaxation_time": self.relaxation_time, + "max_iteration": self.max_iteration, + "tolerance": self.tolerance, + } + + @classmethod + def from_dict(cls, d): + return cls( + chain_length=d["chain_length"], + relaxation_time=d["relaxation_time"], + max_iteration=d["max_iteration"], + tolerance=d["tolerance"], + ) + + +class Barostat(MSONable): + """_summary_ + + Args: + MSONable (_type_): _description_ + """ + + def __init__( + self, + pressure: float = 0, + chain_length: int = 5, + relaxation_time: float = 20670.5, + ): + self.pressure = pressure + self.chain_length = chain_length + self.relaxation_time = relaxation_time + + def as_dict(self): + return { + "pressure": self.pressure, + "chain_length": self.chain_length, + "relaxation_time": self.relaxation_time, + } + + @classmethod + def from_dict(cls, d): + return cls( + pressure=d["pressure"], + chain_length=d["chain_length"], + relaxation_time=d["relaxation_time"], + ) + + +class Velocity(MSONable): + """Velocity class to define velocity-related actions in the simulation. + + Attributes: + create (bool): If True, velocities are sampled from a pseudo-random + normal distribution at the given temperature. + If False, velocities are provided in the data.inpt + file and not resampled. + com_threshold (float): A threshold value triggering a reset of the + centre of mass of all mobile particles if the + kinetic energy of the centre of mass exceeds + the threshold. + scale (tuple): A tuple specifying the method used to scale velocities + and the scaling frequency. + The scale method keywords are: + - "global": All species are scaled together. + - "species": Species types are scaled independently from each other. + - "molecules_global": All species are scaled together, species belonging to the same molecule + are seen as one species type. + - "molecules_independent": Species types are scaled independently from each other, species + belonging to the same molecule are seen as one species type. + If scale keyword is not given, no scaling occurs except at the beginning of the simulation if + velocities are created. In this case, the default method used is "global" and the scaling + frequency is 100. + + """ + + def __init__( + self, + create: bool = False, + com_threshold: float = 1.0e6, + scale: tuple[str, int] = ("global", 100), + ): + self.create = create + self.com_threshold = com_threshold + self.scale = scale + + def as_dict(self): + return { + "create": self.create, + "com_threshold": self.com_threshold, + "scale": self.scale, + } + + @classmethod + def from_dict(cls, d): + return cls( + create=d["create"], + com_threshold=d["com_threshold"], + scale=d["scale"], + ) + + +class SpeciesBlock(MSONable): + """SpeciesBlock class representing the species block in the simulation. + + Attributes: + species (list): List of SpeciesType objects. + dipoles_minimization (DipolesMinimization): Dipoles minimization configuration. + radius_minimization (RadiusMinimization): Radius minimization configuration. + """ + + def __init__( + self, + species: list[SpeciesType] | None, + dipoles_minimization: DipolesMinimization, + radius_minimization: RadiusMinimization, + ): + self.species = species or [] + self.dipoles_minimization = dipoles_minimization + self.radius_minimization = radius_minimization + + def as_dict(self): + return { + "species": [species_type.as_dict() for species_type in self.species], + "dipoles_minimization": self.dipoles_minimization.as_dict(), + "radius_minimization": self.radius_minimization.as_dict(), + } + + @classmethod + def from_dict(cls, d): + species = [SpeciesType.from_dict(species_type_dict) for species_type_dict in d["species"]] + dipoles_minimization = DipolesMinimization.from_dict(d["dipoles_minimization"]) + radius_minimization = RadiusMinimization.from_dict(d["radius_minimization"]) + + return cls(species, dipoles_minimization, radius_minimization) + + +class MoleculesBlock(MSONable): + """MoleculesBlock class representing the molecules block in the simulation. + + Attributes: + molecule_types (list): List of MoleculeType objects. + """ + + def __init__(self, molecule_types: list[MoleculeType] | None): + self.molecule_types = molecule_types or [] + + def add_molecule_type(self, molecule_type: MoleculeType): + """Add a MoleculeType to the block. + + Args: + molecule_type (MoleculeType): MoleculeType object to add. + + """ + self.molecule_types.append(molecule_type) + + def as_dict(self): + return { + "molecule_types": [molecule_type.as_dict() for molecule_type in self.molecule_types], + } + + @classmethod + def from_dict(cls, d): + molecule_types = ( + [MoleculeType.from_dict(molecule_type_dict) for molecule_type_dict in d["molecule_types"]] + if d.get("molecule_types") + else [] + ) + + return cls(molecule_types) + + +class ElectrodesBlock(MSONable): + """ElectrodesBlock class representing the electrodes block in the simulation. + + Attributes: + electrodes (list): List of ElectrodeType objects. + electrode_charges (ElectrodeCharges): Electrode charges configuration. + charge_neutrality (bool): Indicates whether the charge neutrality constraint is used. + Default is True. + global_or_electrodes (str): Specifies the scope of the charge neutrality constraint. + Available options: 'global', 'electrodes'. + Default is 'global'. + """ + + def __init__( + self, + electrodes: list[ElectrodeType] | None, + electrode_charges: ElectrodeCharges, + charge_neutrality: bool = True, + global_or_electrodes: str | tuple[str, float] | None = "global", + ): + self.electrodes = electrodes or [] + self.electrode_charges = electrode_charges + self.charge_neutrality = charge_neutrality + self.global_or_electrodes = global_or_electrodes + + def as_dict(self): + return { + "electrodes": [electrode.as_dict() for electrode in self.electrodes], + "electrode_charges": self.electrode_charges.as_dict(), + "charge_neutrality": self.charge_neutrality, + "global_or_electrodes": self.global_or_electrodes, + } + + @classmethod + def from_dict(cls, d): + electrodes = ( + [ElectrodeType.from_dict(electrode_dict) for electrode_dict in d["electrodes"]] + if d.get("electrodes") + else [] + ) + electrode_charges = ElectrodeCharges.from_dict(d["electrode_charges"]) + charge_neutrality = d["charge_neutrality"] + global_or_electrodes = d["global_or_electrodes"] + + return cls(electrodes, electrode_charges, charge_neutrality, global_or_electrodes) + + +class DipolesElectrodesBlock(MSONable): + """DipolesElectrodesBlock class representing the dipoles_and_electrodes block in the simulation. + + Attributes: + algorithm (str): Algorithm used to compute dipoles and electrode charges. + Available options: + - 'cg': Consistently computes dipoles and electrode charges using a conjugate-gradient method. + Additional parameters: tolerance (real), max_iterations (int). + - 'cg_and_constant_charge': Keeps electrode charges constant while computing dipoles using cg. + Additional parameters: tolerance (real), max_iterations (int). + tolerance (float): Tolerance criterion for the convergence. Default is 1.0e-12. + max_iterations (int): Maximum number of iterations allowed. Default is 100. + preconditioner (str): Preconditioner method used for the conjugate-gradient algorithm. + Available options: 'jacobi'. + + charge_neutrality (bool): Indicates whether the charge neutrality constraint is used. + Default is True. + global_or_electrodes (str): Specifies the scope of the charge neutrality constraint. + Available options: 'global', 'electrodes'. + Default is 'global'. + """ + + def __init__( + self, + algorithm: str, + tolerance: float = 1.0e-12, + max_iterations: int = 100, + preconditioner: str | None = None, + charge_neutrality: bool = True, + global_or_electrodes: str | tuple[str, float] | None = "global", + ): + known_algorithms = ["cg", "cg_and_constant_charge"] + if algorithm not in known_algorithms: + raise ValueError( + f"Unknown algorithm '{algorithm}'. Supported algorithms are: {', '.join(known_algorithms)}" + ) + + self.algorithm = algorithm + self.tolerance = tolerance + self.max_iterations = max_iterations + self.preconditioner = preconditioner + self.charge_neutrality = charge_neutrality + self.global_or_electrodes = global_or_electrodes + + def as_dict(self): + return { + "algorithm": self.algorithm, + "tolerance": self.tolerance, + "max_iterations": self.max_iterations, + "preconditioner": self.preconditioner, + "charge_neutrality": self.charge_neutrality, + "global_or_electrodes": self.global_or_electrodes, + } + + @classmethod + def from_dict(cls, d): + return cls( + algorithm=d["algorithm"], + tolerance=d["tolerance"], + max_iterations=d["max_iterations"], + preconditioner=d.get("preconditioner"), + charge_neutrality=d.get("charge_neutrality", True), + global_or_electrodes=d.get("global_or_electrodes", "global"), + ) + + +class Output: + """Output class representing the output configuration for the simulation. + + Attributes: + default (int): Default output frequency. + step (int): Output frequency for general step data. + restart (int): Output frequency for restart files. + trajectories (int): Output frequency for atom trajectories. + xyz (int): Output frequency for atom positions in xyz format. + pdb (int): Output frequency for atom positions in pdb format. + lammps (int): Output frequency for atom positions in lammps format. + charges (int): Output frequency for atom charges. + total_charges (int): Output frequency for total charges. + energies (int): Output frequency for energy breakdown. + forces (int): Output frequency for atom forces. + elec_forces (int): Output frequency for electrostatic forces. + dipoles (int): Output frequency for dipoles. + radius (int): Output frequency for ion radius. + polarization (int): Output frequency for polarization. + potential_shift (int): Output frequency for potential shift. + density (int): Output frequency for ion and charge density profiles. + temperature (int): Output frequency for temperature. + box_parameters (int): Output frequency for box parameters. + pressure (int): Output frequency for pressure. + stress_tensor (int): Output frequency for stress tensor. + """ + + def __init__( + self, + default: int, + step: int, + restart: int, + trajectories: int, + xyz: int, + pdb: int, + lammps: int, + charges: int, + total_charges: int, + energies: int, + forces: int, + elec_forces: int, + dipoles: int, + radius: int, + polarization: int, + potential_shift: int, + density: int, + temperature: int, + box_parameters: int, + pressure: int, + stress_tensor: int, + ): + self.default = default + self.step = step + self.restart = restart + self.trajectories = trajectories + self.xyz = xyz + self.pdb = pdb + self.lammps = lammps + self.charges = charges + self.total_charges = total_charges + self.energies = energies + self.forces = forces + self.elec_forces = elec_forces + self.dipoles = dipoles + self.radius = radius + self.polarization = polarization + self.potential_shift = potential_shift + self.density = density + self.temperature = temperature + self.box_parameters = box_parameters + self.pressure = pressure + self.stress_tensor = stress_tensor + + def as_dict(self): + return { + "default": self.default, + "step": self.step, + "restart": self.restart, + "trajectories": self.trajectories, + "xyz": self.xyz, + "pdb": self.pdb, + "lammps": self.lammps, + "charges": self.charges, + "total_charges": self.total_charges, + "energies": self.energies, + "forces": self.forces, + "elec_forces": self.elec_forces, + "dipoles": self.dipoles, + "radius": self.radius, + "polarization": self.polarization, + "potential_shift": self.potential_shift, + "density": self.density, + "temperature": self.temperature, + "box_parameters": self.box_parameters, + "pressure": self.pressure, + "stress_tensor": self.stress_tensor, + } + + @classmethod + def from_dict(cls, d): + return cls( + d["default"], + d["step"], + d["restart"], + d["trajectories"], + d["xyz"], + d["pdb"], + d["lammps"], + d["charges"], + d["total_charges"], + d["energies"], + d["forces"], + d["elec_forces"], + d["dipoles"], + d["radius"], + d["polarization"], + d["potential_shift"], + d["density"], + d["temperature"], + d["box_parameters"], + d["pressure"], + d["stress_tensor"], + ) + + +class Plumed(MSONable): + """Plumed class representing Plumed configuration for bias forces and more. + + Attributes: + plumed_file (str): Name of the file that contains the Plumed instructions. + """ + + def __init__(self, plumed_file: str): + self.plumed_file = plumed_file + + def as_dict(self): + return {"plumed_file": self.plumed_file} + + @classmethod + def from_dict(cls, d): + return cls(d["plumed_file"]) + + +class MWInput(MSONable): + """MWInput class representing the overall structure of the simulation input. + + Attributes: + global_params (GlobalParams): Global simulation parameters. + thermostat (Thermostat): Thermostat configuration. + barostat (Barostat): Barostat configuration. + velocity (Velocity): Velocity section configuration. + species_block (SpeciesBlock): Species block configuration. + molecules_block (MoleculesBlock): Molecules block configuration. + electrodes_block (ElectrodesBlock): Electrodes block configuration. + dipoles_electrodes_block (DipolesElectrodesBlock): Dipoles and Electrodes block configuration. + interactions (Interactions): Interatomic interactions block configuration. + plumed (Plumed): Plumed configuration for bias forces and more. + output (Output): Output configuration for the simulation. + """ + + def __init__( + self, + global_params: GlobalParams, + thermostat: Thermostat, + barostat: Barostat, + velocity: Velocity, + species_block: SpeciesBlock, + molecules_block: MoleculesBlock, + electrodes_block: ElectrodesBlock, + dipoles_electrodes_block: DipolesElectrodesBlock, + interactions: Interactions, + plumed: Plumed, + output: Output, + ): + self.global_params = global_params + self.thermostat = thermostat + self.barostat = barostat + self.velocity = velocity + self.species_block = species_block + self.molecules_block = molecules_block + self.electrodes_block = electrodes_block + self.dipoles_electrodes_block = dipoles_electrodes_block + self.interactions = interactions + self.plumed = plumed + self.output = output + + def as_dict(self): + return { + "global_params": self.global_params.as_dict(), + "thermostat": self.thermostat.as_dict(), + "barostat": self.barostat.as_dict(), + "velocity": self.velocity.as_dict(), + "species_block": self.species_block.as_dict(), + "molecules_block": self.molecules_block.as_dict(), + "electrodes_block": self.electrodes_block.as_dict(), + "dipoles_electrodes_block": self.dipoles_electrodes_block.as_dict(), + "interactions": self.interactions.as_dict(), + "plumed": self.plumed.as_dict(), + "output": self.output.as_dict(), + } + + @classmethod + def from_dict(cls, d): + global_params = GlobalParams.from_dict(d["global_params"]) + thermostat = Thermostat.from_dict(d["thermostat"]) + barostat = Barostat.from_dict(d["barostat"]) + velocity = Velocity.from_dict(d["velocity"]) + species_block = SpeciesBlock.from_dict(d["species_block"]) + molecules_block = MoleculesBlock.from_dict(d["molecules_block"]) + electrodes_block = ElectrodesBlock.from_dict(d["electrodes_block"]) + dipoles_electrodes_block = DipolesElectrodesBlock.from_dict(d["dipoles_electrodes_block"]) + interactions = Interactions.from_dict(d["interactions"]) + plumed = Plumed.from_dict(d["plumed"]) + output = Output.from_dict(d["output"]) + + return cls( + global_params, + thermostat, + barostat, + velocity, + species_block, + molecules_block, + electrodes_block, + dipoles_electrodes_block, + interactions, + plumed, + output, + ) diff --git a/pymatgen/io/mw/interactions.py b/pymatgen/io/mw/interactions.py new file mode 100644 index 00000000000..7602c1f9154 --- /dev/null +++ b/pymatgen/io/mw/interactions.py @@ -0,0 +1,398 @@ +from __future__ import annotations + +from monty.json import MSONable + + +class Interactions(MSONable): + """Interactions class representing the interactions block in the simulation. + + Attributes: + coulomb (CoulombInteraction): Coulomb interaction configuration. + lennard_jones (LennardJonesInteraction): Lennard-Jones interaction configuration. + fumi_tosi (FumiTosiInteraction): Fumi-Tosi interaction configuration. + xft (XFTInteraction): XFT interaction configuration. + daim (DaimInteraction): Daim interaction configuration. + damping (DampingInteraction): Damping interaction configuration. + steele (SteeleInteraction): Steele interaction configuration. + """ + + def __init__( + self, + coulomb: Coulomb | None = None, + lennard_jones: LennardJones | None = None, + fumi_tosi: TosiFumi | None = None, + xft: XFT | None = None, + daim: DAIM | None = None, + damping: Damping | None = None, + steele: Steele | None = None, + ): + self.coulomb = coulomb + self.lennard_jones = lennard_jones + self.fumi_tosi = fumi_tosi + self.xft = xft + self.daim = daim + self.damping = damping + self.steele = steele + + def as_dict(self): + return { + "coulomb": self.coulomb.as_dict(), + "lennard_jones": self.lennard_jones.as_dict(), + "fumi_tosi": self.fumi_tosi.as_dict(), + "xft": self.xft.as_dict(), + "daim": self.daim.as_dict(), + "damping": self.damping.as_dict(), + "steele": self.steele.as_dict(), + } + + @classmethod + def from_dict(cls, d): + coulomb = Coulomb.from_dict(d["coulomb"]) + lennard_jones = LennardJones.from_dict(d["lennard_jones"]) + fumi_tosi = TosiFumi.from_dict(d["fumi_tosi"]) + xft = XFT.from_dict(d["xft"]) + daim = DAIM.from_dict(d["daim"]) + damping = Damping.from_dict(d["damping"]) + steele = Steele.from_dict(d["steele"]) + + return cls( + coulomb, + lennard_jones, + fumi_tosi, + xft, + daim, + damping, + steele, + ) + + +class Coulomb(MSONable): + """Coulomb class representing the coulomb block in the simulation. + + Attributes: + coulomb_rcut (float): Cutoff distance for real space interactions. Default is 0.0. + coulomb_rtol (float): Magnitude below which real-space term is not included in the summation. + Default is 1.0e-15. + coulomb_ktol (float): Magnitude below which reciprocal-space term is not included in the + summation. Default is 1.0e-15. + """ + + def __init__( + self, + coulomb_rcut: float = 0.0, + coulomb_rtol: float = 1.0e-15, + coulomb_ktol: float = 1.0e-15, + ): + self.coulomb_rcut = coulomb_rcut + self.coulomb_rtol = coulomb_rtol + self.coulomb_ktol = coulomb_ktol + + def as_dict(self): + return { + "coulomb_rcut": self.coulomb_rcut, + "coulomb_rtol": self.coulomb_rtol, + "coulomb_ktol": self.coulomb_ktol, + } + + @classmethod + def from_dict(cls, d): + return cls( + coulomb_rcut=d.get("coulomb_rcut", 0.0), + coulomb_rtol=d.get("coulomb_rtol", 1.0e-15), + coulomb_ktol=d.get("coulomb_ktol", 1.0e-15), + ) + + +class LennardJones(MSONable): + """LennardJones class representing the lennard-jones block in the simulation. + + Attributes: + lj_rcut (float): Cutoff distance for all Lennard-Jones interactions in atomic units (a0). + lj_3D_tail_correction (bool): Optional flag to enable long-range tail correction in 3D fluid system. + Default is False. + lj_pairs (list[tuple[str, str, float, float]]): List of Lennard-Jones pair parameters. + lj_rule (str | None): Optional mixing rule to calculate cross Lennard-Jones parameters. + Default is None. + + Note: The lj_pairs is a list of tuples, where each tuple represents a Lennard-Jones interaction pair. + The tuple contains the following elements: (species_name1, species_name2, epsilon, sigma). + Epsilon (ϵ) is in kJ/mol, and Sigma (o) is in angstroms, not in atomic units. + If lj_rule is provided, cross Lennard-Jones parameters will be calculated using mixing rules. + + """ + + def __init__( + self, + lj_rcut: float, + lj_3D_tail_correction: bool = False, + lj_pairs: list[tuple[str, str, float, float]] | None = None, + lj_rule: str | None = None, + ): + self.lj_rcut = lj_rcut + self.lj_3D_tail_correction = lj_3D_tail_correction + self.lj_pairs = lj_pairs or [] + self.lj_rule = lj_rule + + def as_dict(self): + return { + "lj_rcut": self.lj_rcut, + "lj_3D_tail_correction": self.lj_3D_tail_correction, + "lj_pairs": self.lj_pairs, + "lj_rule": self.lj_rule, + } + + @classmethod + def from_dict(cls, d): + return cls( + lj_rcut=d["lj_rcut"], + lj_3D_tail_correction=d.get("lj_3D_tail_correction", False), + lj_pairs=d.get("lj_pairs", []), + lj_rule=d.get("lj_rule"), + ) + + +class TosiFumi(MSONable): + r"""TosiFumi class representing the fumi-tosi block in the simulation. + + Attributes: + ft_rcut (float): Cutoff distance for all Fumi-Tosi interactions in atomic units (\(a_0\)). + ft_3D_pressure_tail_correction (bool): Optional flag to enable long-range tail correction in 3D fluid system. + Default is False. + ft_pairs (list[tuple[str, str, float, float, float, float, float, float, float, float]]): List of Fumi-Tosi + pair parameters. + ft_rule (str | None): Optional mixing rule to calculate cross Fumi-Tosi parameters. + Default is None. + + Note: + The `ft_pairs` is a list of tuples, where each tuple represents a Fumi-Tosi interaction pair. + The tuple contains the following elements: (species_name1, species_name2, \(\eta_{ij}\), \(B_{ij}\), + \(C_{ij}\), \(D_{ij}\), \(d_{dd}^{ij}\), \(d_{dq}^{ij}\)). + All parameters are in atomic units (\(a_0\)). + If `ft_rule` is provided, cross Fumi-Tosi parameters will be calculated using mixing rules. + + """ + + def __init__( + self, + ft_rcut: float, + ft_3D_pressure_tail_correction: bool = False, + ft_pairs: list[tuple[str, str, float, float, float, float, float, float, float, float]] | None = None, + ft_rule: str | None = None, + ): + self.ft_rcut = ft_rcut + self.ft_3D_pressure_tail_correction = ft_3D_pressure_tail_correction + self.ft_pairs = ft_pairs or [] + self.ft_rule = ft_rule + + def as_dict(self): + return { + "ft_rcut": self.ft_rcut, + "ft_3D_pressure_tail_correction": self.ft_3D_pressure_tail_correction, + "ft_pairs": self.ft_pairs, + "ft_rule": self.ft_rule, + } + + @classmethod + def from_dict(cls, d): + return cls( + ft_rcut=d["ft_rcut"], + ft_3D_pressure_tail_correction=d.get("ft_3D_pressure_tail_correction", False), + ft_pairs=d.get("ft_pairs", []), + ft_rule=d.get("ft_rule"), + ) + + +class XFT(MSONable): + r"""XFT class representing the xft block in the simulation. + + Attributes: + xft_rcut (float): Cutoff distance for all XFT interactions in atomic units (\(a_0\)). + xft_3D_pressure_tail_correction (bool): Optional flag to enable long-range tail correction in 3D fluid system. + Default is False. + xft_pairs (list[tuple[str, str, float, float, int, float, float, float, float, float, float]]): + List of XFT pair parameters. + xft_rule (str | None): Optional mixing rule to calculate cross XFT parameters. + Default is None. + + Note: + The `xft_pairs` is a list of tuples, where each tuple represents an XFT interaction pair. + The tuple contains the following elements: (species_name1, species_name2, \(\eta_{ij}\), \(B_{ij}\), \(n\), + \(\eta'_{ij}\), \(B'_{ij}\), \(C_{ij}\), \(D_{ij}\), \(d_{dd}^{ij}\), \(d_{dq}^{ij}\)). + All parameters are in atomic units (\(a_0\)). + If `xft_rule` is provided, cross XFT parameters will be calculated using mixing rules. + + """ + + def __init__( + self, + xft_rcut: float, + xft_3D_pressure_tail_correction: bool = False, + xft_pairs: list[tuple[str, str, float, float, int, float, float, float, float, float, float]] | None = None, + xft_rule: str | None = None, + ): + self.xft_rcut = xft_rcut + self.xft_3D_pressure_tail_correction = xft_3D_pressure_tail_correction + self.xft_pairs = xft_pairs or [] + self.xft_rule = xft_rule + + def as_dict(self): + return { + "xft_rcut": self.xft_rcut, + "xft_3D_pressure_tail_correction": self.xft_3D_pressure_tail_correction, + "xft_pairs": self.xft_pairs, + "xft_rule": self.xft_rule, + } + + @classmethod + def from_dict(cls, d): + return cls( + xft_rcut=d["xft_rcut"], + xft_3D_pressure_tail_correction=d.get("xft_3D_pressure_tail_correction", False), + xft_pairs=d.get("xft_pairs", []), + xft_rule=d.get("xft_rule"), + ) + + +class DAIM(MSONable): + r"""DAIM class representing the daim block in the simulation. + + Attributes: + daim_rcut (float): Cutoff distance for all DAIM interactions in atomic units (\(a_0\)). + daim_3D_pressure_tail_correction (bool): Optional flag to enable long-range tail correction in 3D fluid system. + Default is False. + daim_pairs (list[tuple[str, str, float, float, float, float, float, float, float, float, float, float, float]]): + List of DAIM pair parameters. + + Note: + The `daim_pairs` is a list of tuples, where each tuple represents a DAIM interaction pair. + The tuple contains the following elements: (species_name1, species_name2, \(\eta^1_{ij}\), \(\eta^2_{ij}\), + \(\eta^3_{ij}\), \(B^1_{ij}\), \(B^2_{ij}\), \(B^3_{ij}\), \(C_{ij}\), \(D_{ij}\), \(d_{dd}^{ij}\), + \(d_{dq}^{ij}\)). + All parameters are in atomic units (\(a_0\)). + + """ + + def __init__( + self, + daim_rcut: float, + daim_3D_pressure_tail_correction: bool = False, + daim_pairs: list[tuple[str, str, float, float, float, float, float, float, float, float, float, float, float]] + | None = None, + ): + self.daim_rcut = daim_rcut + self.daim_3D_pressure_tail_correction = daim_3D_pressure_tail_correction + self.daim_pairs = daim_pairs or [] + + def as_dict(self): + return { + "daim_rcut": self.daim_rcut, + "daim_3D_pressure_tail_correction": self.daim_3D_pressure_tail_correction, + "daim_pairs": self.daim_pairs, + } + + @classmethod + def from_dict(cls, d): + return cls( + daim_rcut=d["daim_rcut"], + daim_3D_pressure_tail_correction=d.get("daim_3D_pressure_tail_correction", False), + daim_pairs=d.get("daim_pairs", []), + ) + + +class Damping(MSONable): + r"""Damping class representing the damping block in the simulation. + + Attributes: + tt_pairs (list[tuple[str, str, float, int, float]]): List of Tang-Toennies damping function parameters. + + Note: + The `tt_pairs` is a list of tuples, where each tuple represents a pair of species for the damping function. + The tuple contains the following elements: (species_name_charge, species_name_dipole, bn, n, cn). + + The Tang-Toennies damping function is defined as: + + .. math:: + + f_n(r_{ij}) = 1 - c_n \\exp(-b_n r_{ij}) \\sum_{k=0}^{n} \frac{(b_n r_{ij})^k}{k!} + + where: + - :math:`f_n(r_{ij})`: Damping function for charge-dipole interactions. + - :math:`r_{ij}`: Distance between the interacting species. + - :math:`b_n`: Parameter controlling the damping range. + - :math:`n`: Exponent of the damping function. + - :math:`c_n`: Coefficient of the damping function. + + """ + + def __init__( + self, + tt_pairs: list[tuple[str, str, float, int, float]] | None = None, + ): + self.tt_pairs = tt_pairs or [] + + def as_dict(self): + return { + "tt_pairs": self.tt_pairs, + } + + @classmethod + def from_dict(cls, d): + return cls( + tt_pairs=d.get("tt_pairs", []), + ) + + +class Steele(MSONable): + r"""Steele class representing the Steele potential for non-structured walls. + + Attributes: + num_walls (int): The number of different walls (typically 1 or 2). + steele_rcut (float): The cut-off distance for all Steele interactions in atomic units (a0). + + walls (list[tuple[str, str, float, float, float, float]]): List of wall parameters. + Each tuple contains the following elements: (electrode_name, species_name, + rho_w, epsilon_fw, sigma_fw, delta). + + The Steele potential is a one-dimensional potential used to model non-structured walls. The potential is defined as: + + .. math:: + + V(|z_i - z_0|) = 2 \\pi \rho_w \\epsilon_{fw} \\sigma_{fw} \\Delta \\left[ + \frac{2}{5} \\left( \frac{\\sigma_{fw}}{z} \right)^{10} - \\left( \frac{\\sigma_{fw}}{z} \right)^4 - + \frac{\\sigma_{fw}^4}{3 \\Delta (z + 0.61 \\Delta)^3} \right] + + where: + - :math:`V(|z_i - z_0|)`: Steele potential for non-structured walls. + - :math:`z_i`: Distance to the wall. + - :math:`z_0`: Wall position. + - :math:`\rho_w`: Wall density in angstroms^-3. + - :math:`\\epsilon_{fw}`: Energy parameter in kJ/mol. + - :math:`\\sigma_{fw}`: Length parameter in angstroms. + - :math:`\\Delta`: Parameter in angstroms. + + """ + + def __init__( + self, + num_walls: int, + steele_rcut: float, + walls: list[tuple[str, str, float, float, float, float]] | None = None, + ): + self.num_walls = num_walls + self.steele_rcut = steele_rcut + self.walls = walls or [] + + def as_dict(self): + return { + "num_walls": self.num_walls, + "steele_rcut": self.steele_rcut, + "walls": self.walls, + } + + @classmethod + def from_dict(cls, d): + return cls( + num_walls=d.get("num_walls", 0), + steele_rcut=d.get("steele_rcut", 0.0), + walls=d.get("walls", []), + ) diff --git a/pymatgen/io/mw/molecules.py b/pymatgen/io/mw/molecules.py new file mode 100644 index 00000000000..720199354f0 --- /dev/null +++ b/pymatgen/io/mw/molecules.py @@ -0,0 +1,222 @@ +"""Molecular submodule of Metalwalls input file""" + +from __future__ import annotations + +from monty.json import MSONable + +from .potentials import Dihedral, HarmonicAngle, HarmonicBond, Improper + + +class Constraint: + """Parent class for constraints.""" + + @classmethod + def from_dict(cls, d): + """Returns corresponding class object based on input dict. + + Arguments: + d (dict) + + Raises: + ValueError: Raise error if unknown constraint type is given + + Returns: + _description_ + """ + if d["type"] == "rigid": + child_cls = RigidConstraint + elif d["type"] == "rattle": + child_cls = RattleConstraint + else: + raise ValueError(f"Unknown constraint type '{d['type']}'.") + + return child_cls.from_dict(d) + + def as_dict(self): + raise NotImplementedError + + +class RigidConstraint(Constraint, MSONable): + """RigidConstraint class representing a rigid constraint. + + Attributes: + site1 (str): First site involved in the constraint. + site2 (str): Second site involved in the constraint. + distance (float): Distance for the rigid constraint. + + """ + + def __init__( + self, + site1: str, + site2: str, + distance: float, + tolerance: float = 1e-6, + max_iterations: int = 100, + ): + self.site1 = site1 + self.site2 = site2 + self.distance = distance + self.tolerance = tolerance + self.max_iterations = max_iterations + + def as_dict(self): + return { + "type": "rigid", + "site1": self.site1, + "site2": self.site2, + "distance": self.distance, + "tolerance": self.tolerance, + "max_iterations": self.max_iterations, + } + + @classmethod + def from_dict(cls, d): + return cls(site1=d["site1"], site2=d["site2"], distance=d["distance"]) + + +class RattleConstraint(Constraint, MSONable): + """RattleConstraint class representing a rattle constraint. + + Attributes: + site1 (str): First site involved in the constraint. + site2 (str): Second site involved in the constraint. + distance (float): Distance for the rattle constraint. + + """ + + def __init__(self, site1: str, site2: str, distance: float): + self.site1 = site1 + self.site2 = site2 + self.distance = distance + + def as_dict(self): + return { + "type": "rattle", + "site1": self.site1, + "site2": self.site2, + "distance": self.distance, + } + + @classmethod + def from_dict(cls, d): + return cls(site1=d["site1"], site2=d["site2"], distance=d["distance"]) + + +class MoleculeType(MSONable): + """MoleculeType class representing a molecule type. + + Attributes: + name (str): Name of the molecule type. + count (int): Number of molecules of this type in the system. + sites (List[str]): List of site identifiers in the molecule. + fourth_site (float): Fourth site parameter (default 0.0). + + constraints_algorithm (ConstraintAlgorithm): Constraints algorithm for the molecule type. + constraints (List[Constraint]): List of constraints for the molecule type. + + harmonic_bonds (List[HarmonicBond]): List of harmonic bond potentials for the molecule type. + harmonic_angles (List[HarmonicAngle]): List of harmonic angle potentials for the molecule type. + dihedrals (List[Dihedral]): List of dihedral potentials for the molecule type. + impropers (List[Improper]): List of improper potentials for the molecule type. + + """ + + def __init__( + self, + name: str, + count: int = 0, + sites: list[str] | None = None, + fourth_site: float = 0.0, + ): + self.name = name + self.count = count + self.sites = sites or [] + self.fourth_site = fourth_site + + # self.constraints_algorithm = None + self.constraints: list[Constraint] = [] + + self.harmonic_bonds: list[HarmonicBond] = [] + self.harmonic_angles: list[HarmonicAngle] = [] + self.dihedrals: list[Dihedral] = [] + self.impropers: list[Improper] = [] + + def add_constraint(self, constraint: Constraint): + """Add a constraint to the molecule type. + + Args: + constraint (Constraint): Constraint object to add. + + """ + self.constraints.append(constraint) + + def add_harmonic_bond(self, harmonic_bond: HarmonicBond): + """Add a harmonic bond potential to the molecule type. + + Args: + harmonic_bond (HarmonicBond): HarmonicBond object to add. + + """ + self.harmonic_bonds.append(harmonic_bond) + + def add_harmonic_angle(self, harmonic_angle: HarmonicAngle): + """Add a harmonic angle potential to the molecule type. + + Args: + harmonic_angle (HarmonicAngle): HarmonicAngle object to add. + + """ + self.harmonic_angles.append(harmonic_angle) + + def add_dihedral(self, dihedral: Dihedral): + """Add a dihedral potential to the molecule type. + + Args: + dihedral (Dihedral): Dihedral object to add. + + """ + self.dihedrals.append(dihedral) + + def add_improper(self, improper: Improper): + """Add an improper potential to the molecule type. + + Args: + improper (Improper): Improper object to add. + + """ + self.impropers.append(improper) + + def as_dict(self): + return { + "name": self.name, + "count": self.count, + "sites": self.sites, + "fourth_site": self.fourth_site, + # "constraints_algorithm": self.constraints_algorithm.as_dict() if self.constraints_algorithm else None, + "constraints": [constraint.as_dict() for constraint in self.constraints], + "harmonic_bonds": [bond.as_dict() for bond in self.harmonic_bonds], + "harmonic_angles": [angle.as_dict() for angle in self.harmonic_angles], + "dihedrals": [dihedral.as_dict() for dihedral in self.dihedrals], + "impropers": [improper.as_dict() for improper in self.impropers], + } + + @classmethod + def from_dict(cls, d): + molecule_type = cls( + name=d["name"], + count=d["count"], + sites=d["sites"], + fourth_site=d["fourth_site"], + ) + + # if d.get("constraints_algorithm"): + # molecule_type.constraints_algorithm = ConstraintAlgorithm.from_dict(d["constraints_algorithm"]) + + molecule_type.constraints = [Constraint.from_dict(c) for c in d["constraints"]] + molecule_type.harmonic_bonds = [HarmonicBond.from_dict(b) for b in d["harmonic_bonds"]] + molecule_type.harmonic_angles = [HarmonicAngle.from_dict(a) for a in d["harmonic_angles"]] + molecule_type.dihedrals = [Dihedral.from_dict(dh) for dh in d["dihedrals"]] + molecule_type.impropers = [Improper.from_dict(i) for i in d["impropers"]] + + return molecule_type diff --git a/pymatgen/io/mw/outputs.py b/pymatgen/io/mw/outputs.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pymatgen/io/mw/potentials.py b/pymatgen/io/mw/potentials.py new file mode 100644 index 00000000000..52daf6db4be --- /dev/null +++ b/pymatgen/io/mw/potentials.py @@ -0,0 +1,199 @@ +"""Potential submodule defined for MW input""" +from __future__ import annotations + +from monty.json import MSONable + + +class HarmonicBond(MSONable): + """HarmonicBond class representing a harmonic bond potential. + + Attributes: + site1 (str): Identifier of the first site involved in the harmonic bond. + site2 (str): Identifier of the second site involved in the harmonic bond. + k0 (float): Strength of the harmonic bond in atomic units (Eh/a0). + r0 (float): Length of the harmonic bond. + + """ + + def __init__(self, site1: str, site2: str, k0: float, r0: float): + self.site1 = site1 + self.site2 = site2 + self.k0 = k0 + self.r0 = r0 + + def as_dict(self): + return { + "type": "harmonic_bond", + "site1": self.site1, + "site2": self.site2, + "k0": self.k0, + "r0": self.r0, + } + + @classmethod + def from_dict(cls, d): + return cls(site1=d["site1"], site2=d["site2"], k0=d["k0"], r0=d["r0"]) + + +class HarmonicAngle(MSONable): + """HarmonicAngle class representing a harmonic angle potential. + + Attributes: + site1 (str): Identifier of the first site involved in the harmonic angle. + site2 (str): Identifier of the second site involved in the harmonic angle. + site3 (str): Identifier of the third site involved in the harmonic angle. + k0 (float): Strength of the harmonic angle in atomic units (Eh/rad). + theta0 (float): Angle at equilibrium in radians. + + """ + + def __init__(self, site1: str, site2: str, site3: str, k0: float, theta0: float): + self.site1 = site1 + self.site2 = site2 + self.site3 = site3 + self.k0 = k0 + self.theta0 = theta0 + + def as_dict(self): + return { + "type": "harmonic_angle", + "site1": self.site1, + "site2": self.site2, + "site3": self.site3, + "k0": self.k0, + "theta0": self.theta0, + } + + @classmethod + def from_dict(cls, d): + return cls( + site1=d["site1"], + site2=d["site2"], + site3=d["site3"], + k0=d["k0"], + theta0=d["theta0"], + ) + + +class Dihedral(MSONable): + """Dihedral class representing a dihedral potential. + + Attributes: + site1 (str): Identifier of the first site involved in the dihedral. + site2 (str): Identifier of the second site involved in the dihedral. + site3 (str): Identifier of the third site involved in the dihedral. + site4 (str): Identifier of the fourth site involved in the dihedral. + v1 (float): Parameter v1 of the dihedral potential in atomic units (Eh). + v2 (float): Parameter v2 of the dihedral potential in atomic units (Eh). + v3 (float): Parameter v3 of the dihedral potential in atomic units (Eh). + v4 (float): Parameter v4 of the dihedral potential in atomic units (Eh). + + """ + + def __init__( + self, + site1: str, + site2: str, + site3: str, + site4: str, + v1: float, + v2: float, + v3: float, + v4: float, + ): + self.site1 = site1 + self.site2 = site2 + self.site3 = site3 + self.site4 = site4 + self.v1 = v1 + self.v2 = v2 + self.v3 = v3 + self.v4 = v4 + + def as_dict(self): + return { + "type": "dihedral", + "site1": self.site1, + "site2": self.site2, + "site3": self.site3, + "site4": self.site4, + "v1": self.v1, + "v2": self.v2, + "v3": self.v3, + "v4": self.v4, + } + + @classmethod + def from_dict(cls, d): + return cls( + site1=d["site1"], + site2=d["site2"], + site3=d["site3"], + site4=d["site4"], + v1=d["v1"], + v2=d["v2"], + v3=d["v3"], + v4=d["v4"], + ) + + +class Improper(MSONable): + """Improper class representing an improper potential. + + Attributes: + site1 (str): Identifier of the first site involved in the improper. + site2 (str): Identifier of the second site involved in the improper. + site3 (str): Identifier of the third site involved in the improper. + site4 (str): Identifier of the fourth site involved in the improper. + v1 (float): Parameter v1 of the improper potential in atomic units (Eh). + v2 (float): Parameter v2 of the improper potential in atomic units (Eh). + v3 (float): Parameter v3 of the improper potential in atomic units (Eh). + v4 (float): Parameter v4 of the improper potential in atomic units (Eh). + + """ + + def __init__( + self, + site1: str, + site2: str, + site3: str, + site4: str, + v1: float, + v2: float, + v3: float, + v4: float, + ): + self.site1 = site1 + self.site2 = site2 + self.site3 = site3 + self.site4 = site4 + self.v1 = v1 + self.v2 = v2 + self.v3 = v3 + self.v4 = v4 + + def as_dict(self): + return { + "type": "improper", + "site1": self.site1, + "site2": self.site2, + "site3": self.site3, + "site4": self.site4, + "v1": self.v1, + "v2": self.v2, + "v3": self.v3, + "v4": self.v4, + } + + @classmethod + def from_dict(cls, d): + return cls( + site1=d["site1"], + site2=d["site2"], + site3=d["site3"], + site4=d["site4"], + v1=d["v1"], + v2=d["v2"], + v3=d["v3"], + v4=d["v4"], + ) diff --git a/pymatgen/io/mw/species.py b/pymatgen/io/mw/species.py new file mode 100644 index 00000000000..6217b9dc4e7 --- /dev/null +++ b/pymatgen/io/mw/species.py @@ -0,0 +1,207 @@ +"""Species submodule of Metalwalls input file""" +from __future__ import annotations + +from monty.json import MSONable + + +class SpeciesType(MSONable): + """SpeciesType class representing the species_type block. + + Attributes: + name (str): Unique identifier for this species. Up to 8 ASCII characters, no whitespaces, '#' or '!'. + count (int): Number of species of this type in the system. Default is 0. + mass (float): Mass of this species type in the unified atomic mass unit (u). Default is 0.0. + charge (Union[str, Tuple[str, float], Tuple[str, float, float]]): Charge type and its parameters for this + species type. + Possible values: + - 'neutral': Defines a species with no electrostatic interaction. + - ('point', magnitude): Defines a species with a point-charge distribution. 'magnitude' is the + magnitude of the charge in the atomic unit (e). + - ('gaussian', width, magnitude): Defines a species with a gaussian-charge distribution. 'width' + is the η width parameter, and 'magnitude' is the magnitude of the charge in the atomic unit + (e). + Default is 'neutral'. + polarizability (float): Polarizability of the species as described in the force field section. Default is 0.0. + deformability (Tuple[float, float, float]): Deformability parameters of the species as described in the force + field section. + Default is (0.0, 0.0, 0.0). + mobile (bool): Determines if particles of this species type are mobile during the dynamics evolution. Default + is True. + dump_lammps (bool): If True, information for the given species is dumped in the trajectories.lammpstrj file. + Frequency is set in the output block. Default is True. + dump_xyz (bool): If True, particle positions of the given species are dumped in the trajectories.xyz file. + Frequency is set in the output block. Default is True. + dump_trajectories (bool): If True, information for the given species is dumped in the trajectories.out file. + Frequency is set in the output block. Default is True. + dump_pdb (bool): If True, information for the given species is dumped in the trajectories.pdb file. + Frequency is set in the output block. Default is True. + fourth_site_atom (bool): Flag that states that the atoms of this species are virtual atoms of four-site water + models such as TIP4P or Dang-Chang water models, described in the force field section. + If True, such four-site model is used. Default is False. + + """ + + def __init__( + self, + name: str, + count: int = 0, + mass: float = 0.0, + charge: str | tuple[str, float] | tuple[str, float, float] = "neutral", + polarizability: float = 0.0, + deformability: tuple[float, float, float] = (0.0, 0.0, 0.0), + mobile: bool = True, + dump_lammps: bool = True, + dump_xyz: bool = True, + dump_trajectories: bool = True, + dump_pdb: bool = True, + fourth_site_atom: bool = False, + ): + self.name = name + self.count = count + self.mass = mass + self.charge = charge + self.polarizability = polarizability + self.deformability = deformability + self.mobile = mobile + self.dump_lammps = dump_lammps + self.dump_xyz = dump_xyz + self.dump_trajectories = dump_trajectories + self.dump_pdb = dump_pdb + self.fourth_site_atom = fourth_site_atom + + def as_dict(self): + return { + "name": self.name, + "count": self.count, + "mass": self.mass, + "charge": self.charge, + "polarizability": self.polarizability, + "deformability": self.deformability, + "mobile": self.mobile, + "dump_lammps": self.dump_lammps, + "dump_xyz": self.dump_xyz, + "dump_trajectories": self.dump_trajectories, + "dump_pdb": self.dump_pdb, + "fourth_site_atom": self.fourth_site_atom, + } + + @classmethod + def from_dict(cls, d): + return cls( + name=d["name"], + count=d["count"], + mass=d["mass"], + charge=d["charge"], + polarizability=d["polarizability"], + deformability=d["deformability"], + mobile=d["mobile"], + dump_lammps=d["dump_lammps"], + dump_xyz=d["dump_xyz"], + dump_trajectories=d["dump_trajectories"], + dump_pdb=d["dump_pdb"], + fourth_site_atom=d["fourth_site_atom"], + ) + + +class DipolesMinimization(MSONable): + """DipolesMinimization class representing the dipoles_minimization block in the simulation. + + Attributes: + method (str): Method to compute the ionic dipoles. Available options: + - 'cg': Solves μ = aE for each ion using conjugate gradient minimization. + Additional parameters: tolerance (real), max_iterations (int). + - 'matrix_inversion': Solves μ = aE for each ion using direct matrix inversion. + - 'maze_iterative_shake': Solves mass-zero constrained equations using iterative SHAKE algorithm. + Additional parameters: tolerance (real), max_iterations (int), nblocks (Optional[int]). + - 'maze_inversion': Solves mass-zero constrained equations using direct matrix inversion. + + tolerance (float): Tolerance criterion for the convergence. Default is 1.0e-12. + + max_iterations (int): Maximum number of iterations allowed. Default is 100. + + nblocks (Optional[int]): Level of approximation for the SHAKE matrix. Optional parameter for the + 'maze_iterative_shake' method. Default is 0. + + """ + + def __init__( + self, + method: str, + tolerance: float = 1.0e-12, + max_iterations: int = 100, + nblocks: int | None = 0, + # TODO: add preconditioner jacobi + ): + known_methods = [ + "cg", + "matrix_inversion", + "maze_iterative_shake", + "maze_inversion", + ] + if method not in known_methods: + raise ValueError(f"Unknown method '{method}'. Supported methods are: {', '.join(known_methods)}") + self.method = method + self.tolerance = tolerance + self.max_iterations = max_iterations + self.nblocks = nblocks + + def as_dict(self): + return { + "method": self.method, + "tolerance": self.tolerance, + "max_iterations": self.max_iterations, + "nblocks": self.nblocks, + } + + @classmethod + def from_dict(cls, d): + return cls( + method=d["method"], + tolerance=d["tolerance"], + max_iterations=d["max_iterations"], + nblocks=d.get("nblocks"), + ) + + +class RadiusMinimization(MSONable): + """RadiusMinimization class representing the radius_minimization block in the simulation. + + Attributes: + method (str): Method to compute the radii. Available option: + - 'cg': Uses the nonlinear conjugate gradient algorithm for radii computation. + Additional parameters: tolerance (real), max_iterations (int). + + tolerance (float): Tolerance criterion for the convergence. Default is 1.0e-12. + + max_iterations (int): Maximum number of iterations allowed. Default is 100. + + """ + + def __init__( + self, + method: str, + tolerance: float = 1.0e-12, + max_iterations: int = 100, + ): + known_methods = ["cg"] + if method not in known_methods: + raise ValueError(f"Unknown method '{method}'. Supported methods are: {', '.join(known_methods)}") + + self.method = method + self.tolerance = tolerance + self.max_iterations = max_iterations + + def as_dict(self): + return { + "method": self.method, + "tolerance": self.tolerance, + "max_iterations": self.max_iterations, + } + + @classmethod + def from_dict(cls, d): + return cls( + method=d["method"], + tolerance=d["tolerance"], + max_iterations=d["max_iterations"], + )