Source code for aiida_vasp.parsers.content_parsers.potcar
"""
POTCAR parser.
The file parser that handles the parsing of POTCAR files. Also contains methods to
find, import, compose and write POTCAR files.
"""
from __future__ import annotations
import os
import re
from itertools import groupby
from pathlib import Path
from typing import Any, TextIO
from aiida import orm
from parsevasp.potcar import Potcar
from aiida_vasp.data.potcar import PotcarData, PotcarFileData
from aiida_vasp.parsers.content_parsers.base import BaseFileParser
from aiida_vasp.utils.delegates import delegate_method_kwargs
[docs]
class PotcarParser(BaseFileParser):
"""A lightweight interface that provides access to POTCAR metadata parsing.
Similar to the other content parser for VASP in structure, but only used directly in the POTCAR
handling logic.
"""
DEFAULT_SETTINGS = {}
PARSABLE_QUANTITIES = {}
[docs]
def _init_from_handler(self, handler: TextIO) -> None:
"""Initialize using a file like handler.
:param handler: A file like object that provides the necessary content to be parsed.
:type handler: file-like object
"""
try:
self._content_parser = Potcar(file_handler=handler, logger=self._logger)
except SystemExit:
self._logger.warning('Parsevasp exited abnormally.')
@property
def metadata(self) -> Potcar:
"""Return the metadata Potcar instance."""
return self._content_parser
[docs]
def _init_from_data(self, data: dict) -> None:
"""No need to init from an AiiDA data structure."""
raise NotImplementedError('PotcarParser does not implement a _init_from_data() method.')
[docs]
def _content_data_to_content_parser(self) -> None:
"""Since no need to accept AiiDA data structure, no need to convert it."""
raise NotImplementedError('PotcarParser does not implement a _content_data_to_content_parser() method.')
[docs]
class PotcarIo:
"""
Deals with VASP input output of POTCAR files.
Instantiate with one of the following kwargs:
:param path: (string) absolute path to the POTCAR file
:param potcar_node: a PotcarData node
:param potcar_file_node: a PotcarFileNode
:param contents: a string with the POTCAR content
"""
def __init__(self, **kwargs: Any) -> None:
"""Init from Potcar object or delegate to kwargs initializers."""
self.potcar_obj = None
self.sha512 = None
self.init_with_kwargs(**kwargs)
[docs]
@delegate_method_kwargs(prefix='_init_with_')
def init_with_kwargs(self, **kwargs: Any) -> None:
"""Delegate initialization to _init_with - methods."""
[docs]
def _init_with_path(self, file_path: str | Path) -> None:
"""Initialize with a path."""
node, _ = PotcarData.get_or_create_from_file(file_path=file_path)
self.sha512 = node.sha512
[docs]
def _init_with_potcar_file_node(self, node: PotcarFileData) -> None:
"""Initialize with an existing potential file node."""
self.sha512 = node.sha512
[docs]
def _init_with_potcar_node(self, node: PotcarData) -> None:
"""Initialize with an existing potential node."""
self._init_with_potcar_file_node(node.find_file_node())
[docs]
def _init_with_contents(self, contents: str) -> None:
"""Initialize with a string."""
from aiida_vasp.data.potcar import PotcarData # noqa: PLC0415
try:
contents = contents.encode('utf-8')
except AttributeError:
pass
node, _ = PotcarData.get_or_create_from_contents(contents)
self.sha512 = node.sha512
@property
def file_node(self) -> PotcarFileData:
from aiida_vasp.data.potcar import PotcarData # noqa: PLC0415
return PotcarData.find_one(sha512=self.sha512).find_file_node()
@property
def node(self) -> PotcarData:
from aiida_vasp.data.potcar import PotcarData # noqa: PLC0415
return PotcarData.find_one(sha512=self.sha512)
@property
def content(self) -> bytes:
return self.file_node.get_content()
[docs]
@classmethod
def from_(cls, potcar: str | Path | PotcarData | PotcarFileData | PotcarIo) -> PotcarIo:
"""Determine the best guess at how the input represents a POTCAR file and construct
a PotcarIo instance based on that."""
from aiida_vasp.data.potcar import PotcarData, PotcarFileData # noqa: PLC0415
if isinstance(potcar, (str, os.PathLike)):
try:
path_exists = Path(potcar).exists()
except OSError:
# We failed possibly due to a too long filename or that the potcar content is in fact
# potcar content, revert to the os module to check if it exists
path_exists = os.path.exists(potcar)
if path_exists:
potcar = cls(path=potcar)
else:
potcar = cls(contents=potcar)
elif isinstance(potcar, PotcarData):
potcar = cls(potcar_node=potcar)
elif isinstance(potcar, PotcarFileData):
potcar = cls(potcar_file_node=potcar)
elif isinstance(potcar, PotcarIo):
pass
else:
raise TypeError(f'Invalid type of potcar: {type(potcar)}')
return potcar
[docs]
def __eq__(self, other: PotcarIo) -> bool:
return self.sha512 == other.sha512
[docs]
def __hash__(self) -> int:
return hash(self.sha512)
[docs]
class MultiPotcarIo:
"""
Handle file i/o for POTCAR files with one or more potentials.
"""
def __init__(self, potcars: list[Any] | None = None) -> None:
self._potcars = []
if potcars:
for potcar in potcars:
self.append(PotcarIo.from_(potcar))
[docs]
def append(self, potcar: Any) -> None:
self._potcars.append(PotcarIo.from_(potcar))
[docs]
def write(self, path: str | Path) -> None:
path = Path(path)
with path.open('wb') as dest_fo:
for potcar in self._potcars:
dest_fo.write(potcar.content)
[docs]
@classmethod
def read(cls, path: str | Path) -> MultiPotcarIo:
"""Read a POTCAR file that may contain one or more potentials into a list of PotcarIo objects."""
potcars = cls()
path = Path(path)
with path.open('r', encoding='utf8') as potcar_fo:
potcar_strings = re.compile(r'\n?(\s*.*?End of Dataset\n)', re.S).findall(potcar_fo.read())
for potcar_contents in potcar_strings:
potcars.append(PotcarIo.from_(potcar_contents))
return potcars
@property
def potcars(self) -> list[PotcarIo]:
return self._potcars
[docs]
@classmethod
def from_structure(cls, structure: orm.StructureData, potentials_dict: dict[str, Any]) -> MultiPotcarIo:
"""Create a MultiPotcarIo from an AiiDA `StructureData` object and a dictionary with a
potential for each kind in the structure."""
symbol_order = cls.potentials_order(structure)
return cls(potcars=[potentials_dict[symbol] for symbol in symbol_order])
[docs]
def get_potentials_dict(self, structure: orm.StructureData) -> dict[str, Any]:
"""
Get a dictionary {kind_name: PotcarData} that would fit the structure.
If the PotcarData contained in MultiPotcarIo do not match the structure, an exception is raised.
"""
structure_elements = structure.get_symbols_set()
if structure_elements != self.element_symbols:
raise ValueError('structure elements do not match POTCAR elements')
if len(structure.get_kind_names()) != len(structure_elements):
raise ValueError('structure has more kind names than elements')
element_potcars = {potcario.node.element: potcario.node for potcario in self.potcars}
return {kind.name: element_potcars[kind.symbol] for kind in structure.kinds}
@property
def element_symbols(self) -> set[str]:
return {potcario.node.element for potcario in self.potcars}
[docs]
@classmethod
def potentials_order(cls, structure: orm.StructureData) -> list[str]:
return [kind[0] for kind in cls.count_kinds(structure)]
[docs]
@classmethod
def count_kinds(cls, structure: orm.StructureData) -> list[tuple[str, int]]:
"""Count consecutive kinds that compose the different sites.
:param structure: Structure containing sites and kinds
:type structure: object
:returns: List of tuples with kind names and counts
:rtype: list
"""
kind_name_order = [site.kind_name for site in structure.sites]
groups = groupby(kind_name_order)
counts = [(label, sum(1 for _ in group)) for label, group in groups]
return counts