Skip to content

Commit

Permalink
Merge 4cad606 into 9048e06
Browse files Browse the repository at this point in the history
  • Loading branch information
rkingsbury committed Nov 23, 2021
2 parents 9048e06 + 4cad606 commit 7c9a85d
Show file tree
Hide file tree
Showing 19 changed files with 1,839 additions and 86 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/test-macwin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,16 @@ jobs:
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- uses: conda-incubator/setup-miniconda@v2
with:
python-version: 3.8
- name: Install dependencies
run: |
pip install --upgrade pip wheel
pip install -r requirements.txt
pip install -r requirements-optional.txt -r requirements-dev.txt
pip install -e .
conda install -c conda-forge packmol
- name: pytest ${{ matrix.pkg }}
run: |
pytest ${{ matrix.pkg }}
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,13 @@ jobs:
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install OpenBabel and Libs
- name: Install OpenBabel, Packmol, and Libs
shell: bash -l {0}
run: |
# Required for gulp.
sudo apt-get install csh gfortran
conda install -c conda-forge openbabel
conda install -c conda-forge packmol
- name: Install dependencies
shell: bash -l {0}
run: |
Expand Down
39 changes: 38 additions & 1 deletion docs_rst/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,43 @@ he should use the following procedure::
plotter = PDPlotter(pd)
plotter.show()

pymatgen.io - Managing calculation inputs and outputs
=====================================================

The :mod:`pymatgen.io` module contains classes to facilitate writing input files
and parsing output files from a variety of computational codes, including VASP,
Q-Chem, LAMMPS, CP2K, AbInit, and many more.

The core class for managing inputs is the :class:`InputSet`. An :class:`InputSet` object contains
all the data necessary to write one or more input files for a calculation.
Specifically, every :class:`InputSet` has a `write_input()` method that writes all the
necessary files to a location you specify. There are also :class:`InputGenerator` classes
that yield :class:`InputSet` with settings tailored to specific calculation types (for example,
a structure relaxation). You can think of :class:`InputGenerator` classes as "recipes" for
accomplishing specific computational tasks, while :class:`InputSet` contain those recipes
applied to a specific system or structure.

Custom settings can be provided to :class:`InputGenerator` on instantiation. For example,
to construct an :class:`InputSet` for a VASP structure relaxation using default Materials
Project parameters, but change the `NSW` parameter from the default (99) to 500::

from pymatgen.io.generators import MPRelaxGen

input_gen = MPRelaxGen(user_incar_settings={"NSW": 500})
vasp_input_set = input_gen.get_input_set(structure)
vasp_input_set.write_input('/path/to/calc/directory')

You can also use `InputSet.from_directory()` to construct a pymatgen :class:`InputSet`
from a directory containing calculation inputs.

Many codes also contain classes for parsing output files into pymatgen objects that
inherit from :class:`InputFile`, which provides a standard interface for reading and
writing individual files.

Use of :class:`InputFile`, :class:`InputSet`, and :class:`InputGenerator` classes is
not yet fully implemented by all codes supported by pymatgen, so please refer to the
respective module documentation for each code for more details.

pymatgen.borg - High-throughput data assimilation
=================================================

Expand Down Expand Up @@ -543,7 +580,7 @@ document schema used in the Materials Project and how best to query for the
relevant information you need.

Setting the PMG_MAPI_KEY in the config file
---------------------------------------
-------------------------------------------

MPRester can also read the API key via the pymatgen config file. Simply run::

Expand Down
232 changes: 232 additions & 0 deletions pymatgen/io/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
# coding: utf-8
# Copyright (c) Pymatgen Development Team.
# Distributed under the terms of the MIT License.

"""
This module defines the abstract interface for reading and writing calculation
inputs in pymatgen. The interface comprises a 3-tiered hierarchy of clases.
1. An InputFile object represents the contents of a single input file, e.g.
the INCAR. This class standardizes file read and write operations.
2. An InputSet is a dict-like container that maps filenames (keys) to file
contents (either strings or InputFile objects). This class provides a standard
write_input() method.
3. InputGenerator classes implement a get_input_set method that, when provided
with a structure, return an InputSet object with all parameters set correctly.
Calculation input files can be written to disk with the write_inputs method.
If you want to implement a new InputGenerator, please take note of the following:
1. You must implement a get_input_set method that returns an InputSet
2. All customization of calculation parameters should be done in the __init__
method of the InputGenerator. The idea is that the generator contains
the "recipe", but nothing that is specific to a particular system. get_input_set
takes system-specific information (such as structure) and applies the recipe.
3. All InputGenerator must save all supplied args and kwargs as instance variables.
E.g., self.my_arg = my_arg and self.kwargs = kwargs in the __init__. This
ensures the as_dict and from_dict work correctly.
"""

import abc
import os
from pathlib import Path
from typing import Union, Dict
from zipfile import ZipFile
from collections.abc import MutableMapping
from monty.io import zopen
from monty.json import MSONable

__author__ = "Ryan Kingsbury"
__email__ = "RKingsbury@lbl.gov"
__status__ = "Development"
__date__ = "October 2021"


class InputFile(MSONable):
"""
Abstract base class to represent a single input file. Note that use
of this class is optional; it is possible create an InputSet that
does not rely on underlying Inputfile objects.
All InputFile classes must implement a get_string method, which
is called by write_file.
"""

@abc.abstractmethod
def get_string(self) -> str:
"""
Return a string representation of an entire input file.
"""

def write_file(self, filename: Union[str, Path]) -> None:
"""
Write the input file.
Args:
filename: The filename to output to, including path.
kwargs: Keyword arguments passed to get_string()
"""
filename = filename if isinstance(filename, Path) else Path(filename)
with zopen(filename, "wt") as f:
f.write(self.get_string())

@classmethod
@abc.abstractmethod
def from_string(cls, contents: str):
"""
Create an InputFile object from a string
Args:
contents: The contents of the file as a single string
Returns:
InputFile
"""

@classmethod
def from_file(cls, path: Union[str, Path]):
"""
Creates an InputFile object from a file.
Args:
path: Filename to read, including path.
Returns:
InputFile
"""
filename = path if isinstance(path, Path) else Path(path)
with zopen(filename, "rt") as f:
return cls.from_string(f.read())


class InputSet(MSONable, MutableMapping):
"""
Abstract base class for all InputSet classes. InputSet are dict-like
containers for all calculation input data.
Since InputSet inherits dict, it can be instantiated in the same manner,
or a custom __init__ can be provided. Either way, `self` should be
populated with keys that are filenames to be written, and values that are
InputFile objects or strings representing the entire contents of the file.
All InputSet must implement from_directory. Implementing the validate method
is optional.
"""

def __init__(self, inputs: Dict[Union[str, Path], Union[str, InputFile]] = {}, **kwargs):
"""
Instantiate an InputSet.
Args:
inputs: The core mapping of filename: file contents that defines the InputSet data.
This should be a dict where keys are filenames and values are InputFile objects
or strings representing the entire contents of the file. This mapping will
become the .inputs attribute of the InputSet.
**kwargs: Any kwargs passed will be set as class attributes e.g.
InputSet(inputs={}, foo='bar') will make InputSet.foo == 'bar'.
"""
self.inputs = inputs
self.__dict__.update(**kwargs)

def __getattr__(self, k):
# allow accessing keys as attributes
return self.get(k)

def __len__(self):
return len(self.inputs.keys())

def __iter__(self):
return iter(self.inputs.items())

def __getitem__(self, key):
return self.inputs[key]

def __setitem__(self, key, value):
self.inputs[key] = value

def __delitem__(self, key):
del self.inputs[key]

def write_input(
self,
directory: Union[str, Path],
make_dir: bool = True,
overwrite: bool = True,
zip_inputs: bool = False,
):
"""
Write Inputs to one or more files
Args:
directory: Directory to write input files to
make_dir: Whether to create the directory if it does not already exist.
overwrite: Whether to overwrite an input file if it already exists.
Additional kwargs are passed to generate_inputs
zip_inputs: If True, inputs will be zipped into a file with the
same name as the InputSet (e.g., InputSet.zip)
"""
path = directory if isinstance(directory, Path) else Path(directory)

for fname, contents in self.inputs.items():
file = path / fname

if not path.exists():
if make_dir:
path.mkdir(parents=True, exist_ok=True)

if file.exists() and not overwrite:
raise FileExistsError(f"File {str(fname)} already exists!")
file.touch()

# write the file
if isinstance(contents, str):
with zopen(file, "wt") as f:
f.write(contents)
else:
contents.write_file(file)

if zip_inputs:
zipfilename = path / f"{self.__class__.__name__}.zip"
with ZipFile(zipfilename, "w") as zip:
for fname, contents in self.inputs.items():
file = path / fname
try:
zip.write(file)
os.remove(file)
except FileNotFoundError:
pass

@classmethod
def from_directory(cls, directory: Union[str, Path]):
"""
Construct an InputSet from a directory of one or more files.
Args:
directory: Directory to read input files from
"""
raise NotImplementedError(f"from_directory has not been implemented in {cls}")

def validate(self) -> bool:
"""
A place to implement basic checks to verify the validity of an
input set. Can be as simple or as complex as desired.
Will raise a NotImplementedError unless overloaded by the inheriting class.
"""
raise NotImplementedError(f".validate() has not been implemented in {self.__class__}")


class InputGenerator(MSONable):
"""
InputGenerator classes serve as generators for Input objects. They contain
settings or sets of instructions for how to create Input from a set of
coordinates or a previous calculation directory.
"""

@abc.abstractmethod
def get_input_set(self) -> InputSet:
"""
Generate an InputSet object. Typically the first argument to this method
will be a Structure or other form of atomic coordinates.
"""
pass
50 changes: 49 additions & 1 deletion pymatgen/io/lammps/inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@
import re
import shutil
import warnings
from pathlib import Path
from string import Template
from typing import Union, Optional, Dict

from monty.json import MSONable
from monty.dev import deprecated

from pymatgen.io.lammps.data import LammpsData
from pymatgen.io.template import TemplateInputGen
from pymatgen.io.lammps.data import LammpsData, CombinedData

__author__ = "Kiran Mathew, Brandon Wood, Zhi Deng"
__copyright__ = "Copyright 2018, The Materials Virtual Lab"
Expand Down Expand Up @@ -107,6 +111,50 @@ def md(cls, data, force_field, temperature, nsteps, other_settings=None):
)


class LammpsTemplateGen(TemplateInputGen):
"""
Creates an InputSet object for a LAMMPS run based on a template file.
The input script is constructed by substituting variables into placeholders
in the template file using python's Template.safe_substitute() function.
The data file containing coordinates and topology information can be provided
as a LammpsData instance. Alternatively, you can include a read_data command
in the template file that points to an existing data file.
Other supporting files are not handled at the moment.
To write the input files to a directory, call LammpsTemplateSet.write_input()
See pymatgen.io.template.py for additional documentation of this method.
"""

def get_input_set( # type: ignore
self,
script_template: Union[str, Path],
settings: Optional[Dict] = None,
script_filename: str = "in.lammps",
data: Union[LammpsData, CombinedData] = None,
data_filename: str = "system.data",
):
"""
Args:
script_template: String template for input script with
placeholders. The format for placeholders has to be
'$variable_name', e.g., '$temperature'
settings: Contains values to be written to the
placeholders, e.g., {'temperature': 1}. Default to None.
data: Data file as a LammpsData instance. Default to None, i.e., no
data file supplied. Note that a matching 'read_data' command
must be provided in the script template in order for the data
file to actually be read.
script_filename: Filename for the input file.
data_filename: Filename for the data file, if provided.
"""
input_set = super().get_input_set(template=script_template, variables=settings, filename=script_filename)

if data:
input_set.update({data_filename: data})
return input_set


@deprecated(LammpsTemplateGen, "This method will be retired in the future. Consider using LammpsTemplateSet instead.")
def write_lammps_inputs(
output_dir,
script_template,
Expand Down

0 comments on commit 7c9a85d

Please sign in to comment.