Source code for wulfric.io._vasp

# ================================== LICENSE ===================================
# Wulfric - Cell, Atoms, K-path, visualization.
# Copyright (C) 2023 Andrey Rybakov
#
# e-mail: anry@uv.es, web: adrybakov.com
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
# ================================ END LICENSE =================================
import os
from calendar import month_name
from datetime import datetime

import numpy as np

from wulfric.crystal._atoms import get_atom_species
from wulfric.geometry._geometry import get_volume

__all__ = ["load_poscar", "dump_poscar"]


[docs] def load_poscar(file_object=None): r""" Reads crystal structure from the |POSCAR|_ file. Parameters ---------- file_object : str of file-like object, optional File to be read. If str, then file is opened with the given name. Otherwise it has to have ``.readlines()`` method. By default it looks for the "POSCAR" file in the current directory. Behaviour for ``str``: * Tries to open the file with the name ``file_object``. * Tries to open the file with the name "POSCAR" in the directory ``file_object``. Returns ------- cell : (3, 3) :numpy:`ndarray` Cell of a crystal structure, rows are interpreted as vectors. .. code-block:: python cell = [[a1_x, a1_y, a1_z], [a2_x, a2_y, a2_z], [a3_x, a3_y, a3_z]] atoms : dict Atoms of the crystal structure. Positions are always relative to the cell. comment : str Comment from the first line of the file. Examples -------- .. doctest:: >>> # Load a POSCAR file >>> cell, atoms, comment = wulfric.io.load_poscar("POSCAR") # doctest: +SKIP """ # Open file if needed if file_object is None: try: file_object = open("POSCAR") except FileNotFoundError: raise FileNotFoundError( f'"POSCAR" file not found, looked here: {os.getcwd()}' ) elif isinstance(file_object, str): try: file_object = open(file_object, "r", encoding="utf-8") except FileNotFoundError: try: file_object = open( os.path.join(file_object, "POSCAR"), "r", encoding="utf-8" ) except FileNotFoundError: raise FileNotFoundError( f'"POSCAR" file not found, looked here: {file_object}' ) lines = file_object.readlines() comment = lines[0].strip() # 1 or 3 numbers scale_factor = np.array(list(map(float, lines[1].split()))) cell = np.array(list(map(lambda x: list(map(float, x.split())), lines[2:5]))) if len(scale_factor) == 1: if scale_factor[0] < 0: scale_factor = abs(scale_factor[0] / get_volume(cell)) cell *= scale_factor elif len(scale_factor) == 3 and np.all(scale_factor > 0): cell[0] *= scale_factor[0] cell[1] *= scale_factor[1] cell[2] *= scale_factor[2] else: raise ValueError( "Scale factor has to be a single positive ot negative number or " + f"a list of 3 positive numbers, got: {scale_factor}" ) # Read species name and numbers species = lines[5].split() GOT_SPECIES_NAMES = False index = 6 for i in species: try: int(i) except ValueError: GOT_SPECIES_NAMES = True if GOT_SPECIES_NAMES: species_names = species species = lines[6].split() index = 7 else: species_names = None ions_per_species = list(map(int, species)) # Skip selective dynamics if lines[index][0] in ["S", "s"]: index += 1 # Get mode CARTESIAN = False if lines[index][0].lower() in ["c", "k"]: CARTESIAN = True index += 1 atoms = {"names": [], "positions": []} for i in range(len(species)): for j in range(ions_per_species[i]): coordinates = np.array(list(map(float, lines[index].split()[:3]))) index += 1 if CARTESIAN: # Both cases (1 or 3 numbers) are covered coordinates *= scale_factor # Transform from Cartesian coordinates to absolute coordinates coordinates = coordinates @ np.linalg.inv(cell) if species_names is None: atoms["names"].append(f"X{i + 1}") atoms["positions"].append(coordinates) else: atoms["names"].append(species_names[i]) atoms["positions"].append(coordinates) return cell, atoms, comment
[docs] def dump_poscar( cell, atoms, file_object="POSCAR", comment: str = None, decimals=8, mode: str = "Direct", ): r""" Writes crystal structure to the |POSCAR|_ file. Parameters ---------- cell : (3, 3) |array-like|_, Matrix of a cell, rows are interpreted as vectors. atoms : dict Dictionary with atoms. Must have a ``"positions"`` with value of (N,3) |array-like|_. Must have either ``"names"`` key with value of ``list`` of ``str`` of length N or ``"species"`` key with value of ``list`` of ``str`` of length N. If ``"species"`` key is not present, try to deduce atom's species from ``"names"``, raise error on fail. file_object : str of file-like object, optional File to be written. If str, then file is opened with the given name. Otherwise it has to have ``.write()`` method. comment : str, optional Comment to be written in the first line of the file. Has to be a single line. All new lines symbols are replaced with spaces. decimals : int, default 8 Number of decimals to be written. mode : str, default "Direct" Mode of the coordinates to be written. Can be "Direct" or "Cartesian". Examples -------- .. doctest:: >>> # Dump a POSCAR file >>> wulfric.io.dump_poscar(cell, atoms, "POSCAR") # doctest: +SKIP >>> # If you want to write a comment as well: >>> wulfric.io.dump_poscar( ... cell, atoms, "POSCAR", comment="This is a comment" ... ) # doctest: +SKIP >>> # You can control the amount of decimals in the output: >>> wulfric.io.dump_poscar(cell, atoms, "POSCAR", decimals=6) # doctest: +SKIP >>> # You can switch the mode of coordinates between 'Cartesian' and 'Direct' (default): >>> wulfric.io.dump_poscar( ... cell, atoms, "POSCAR", mode="Cartesian" ... ) # doctest: +SKIP """ cell = np.array(cell, dtype=float) # Prepare comment if comment is None: cd = datetime.now() comment = ( f"Written by wulfric (wulfric.org) " f"on {cd.day} {month_name[cd.month]} {cd.year} " f"at {cd.hour}:{cd.minute}:{cd.second}" ) else: comment = comment.replace("\n", " ") # Check mode if mode not in ("Direct", "Cartesian"): raise ValueError(f'mode has to be "Direct" or "Cartesian", given: {mode}') # Prepare atoms atoms_list = [] for i in range(len(atoms["positions"])): if "species" in atoms: atom_type = atoms["species"][i] else: atom_type = get_atom_species(atoms["names"][i]) if atom_type == "X": raise ValueError( f"Can not deduce atom's type from the name '{atoms['name'][i]}', while dumping to POSCAR." ) if mode == "Direct": atom_position = atoms["positions"][i] else: atom_position = atoms["positions"][i] @ cell atoms_list.append((atom_type, atom_position)) # Sort atoms by type atoms_list = sorted(atoms_list, key=lambda x: x[0]) # Prepare atom species and coordinates atom_species = {} atom_coordinates = [] for atom in atoms_list: if atom[0] not in atom_species: atom_species[atom[0]] = 1 else: atom_species[atom[0]] += 1 atom_coordinates.append(atom[1]) # Open file if needed if isinstance(file_object, str): file_object = open(file_object, "w", encoding="utf-8") # Write file_object.write(comment + "\n") file_object.write("1.0\n") for vector_index in range(3): for component_index in range(3): file_object.write( f"{cell[vector_index][component_index]:{decimals + 5}.{decimals}f} " ) file_object.write("\n") for species in atom_species: file_object.write(f"{species} ") file_object.write("\n") for species in atom_species: file_object.write(f"{atom_species[species]} ") file_object.write("\n") file_object.write(mode + "\n") for coordinate in atom_coordinates: for component_index in range(3): file_object.write( f"{coordinate[component_index]:{decimals + 5}.{decimals}f} " ) file_object.write("\n")