# RAD-tools - Sandbox (mainly condense matter plotting).
# Copyright (C) 2022-2024 Andrey Rybakov
#
# e-mail: anry@uv.es, web: rad-tools.org
#
# 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/>.
import os
import numpy as np
from radtools.crystal.atom import Atom
from radtools.crystal.crystal import Crystal
from radtools.decorate.array import print_2d_array
from radtools.geometry import absolute_to_relative, volume
__all__ = ["load_poscar", "dump_poscar"]
[docs]
def load_poscar(file_object=None, return_crystal=True, return_comment=False):
r"""
Read the 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 given by the ``file_object``.
* Tries to open the file with the name "POSCAR" in the directory given by the ``file_object``.
return_crystal: bool, default True
If True, returns :py:class:`.Crystal` object. Otherwise returns a tuple of
``(cell, atoms)``.
return_comment: bool, default False
Whether to return the comment from the first line of the file.
Returns
-------
crystal: :py:class:`.Crystal`
Crystal structure read from the file. If ``return_crystal`` is ``True``.
cell: (3, 3) :numpy:`ndarray`
Cell of the crystal structure. If ``return_crystal`` is ``False``.
atoms: list of :py:class:`.Atom`
Atoms of the crystal structure. If ``return_crystal`` is ``False``.
Positions are always relative to the cell.
comment: str
Comment from the first line of the file. If ``return_comment`` is ``True``.
"""
# Open file if needed
if file_object is None:
try:
file_object = open("POSCAR")
except FileNotFoundError:
raise FileNotFoundError("POSCAR file not found")
elif isinstance(file_object, str):
try:
file_object = open(file_object)
except FileNotFoundError:
try:
file_object = open(os.path.join(file_object, "POSCAR"))
except FileNotFoundError:
raise FileNotFoundError("POSCAR file not found")
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] / 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] in ["C", "c", "K", "k"]:
CARTESIAN = True
index += 1
atoms = []
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
coordinates = absolute_to_relative(cell, coordinates)
if species_names is None:
atoms.append(Atom(f"X{i+1}", coordinates))
else:
atoms.append(Atom(species_names[i], coordinates))
if return_crystal:
if return_comment:
return Crystal(cell=cell, atoms=atoms), comment
return Crystal(cell=cell, atoms=atoms)
if return_comment:
return cell, atoms, comment
return cell, atoms
[docs]
def dump_poscar(
crystal_like,
file_object="POSCAR",
comment: str = None,
decimals=8,
mode: str = "Direct",
):
r"""
Write :py:class:`.Crystal`-like object to the |POSCAR|_ file.
Parameters
----------
crystal_like: any
Object to be written. It has to have ``.cell`` and ``.atoms`` attributes.
``cell`` has to be a 3x3 array-like object, ``atoms`` has to be a list of
:py:class:`.Atom` objects.
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 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".
"""
# Prepare comment
if comment is None:
try:
comment = crystal_like.name
except AttributeError:
comment = "Data from RAD-tools"
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
if mode == "Direct":
atoms = [(atom.type, atom.position) for atom in crystal_like.atoms]
else:
atoms = [
(atom.type, atom.position @ crystal_like.cell)
for atom in crystal_like.atoms
]
# Sort atoms by type
atoms = sorted(atoms, key=lambda x: x[0])
# Prepare atom species and coordinates
atom_species = {}
atom_coordinates = []
for atom in atoms:
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")
file_object.write(
print_2d_array(
crystal_like.cell, fmt=f".{decimals}f", print_result=False, borders=False
)
+ "\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")
file_object.write(
print_2d_array(
atom_coordinates, fmt=f".{decimals}f", print_result=False, borders=False
)
+ "\n"
)
if __name__ == "__main__":
from radtools import HEX, Crystal
c = Crystal(HEX(1, 2))
c.add_atom("Cr2", position=[0.5, 0, 0])
c.add_atom("C2", position=[0, 0, 0])
c.add_atom("C1", position=[0, 0, 0.5])
c.add_atom("C3", position=[0, 0.5, 0])
c.add_atom("Cr1", position=[0, 0.5, 0.5])
# dump_poscar(c)
# dump_poscar(c, "test-cart.vasp", mode="Cartesian")
crystal = load_poscar()
print_2d_array(crystal.cell)
for atom in crystal.atoms:
print(atom.name, atom.position)
crystal = load_poscar("test-cart.vasp")
print_2d_array(crystal.cell)
for atom in crystal.atoms:
print(atom.name, atom.position)