Skip to content

[WIP] Add I/O interface with Metalwalls #3193

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pymatgen/io/mw/__init__.py
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think better to be explicit here and write out the name: pymatgen/io/metalwalls/__init__.py

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @janosh! Will change that when I get back to this. It has been put off in my TODOs and is going to stale for a while.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from __future__ import annotations

from .inputs import MWInput

__all__ = ["MWInput"]
222 changes: 222 additions & 0 deletions pymatgen/io/mw/electrodes.py
Original file line number Diff line number Diff line change
@@ -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 = 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"),
)
Loading