diff --git a/.github/workflows/test_full.yml b/.github/workflows/test_full.yml index b9fa6a3c..9efd5d75 100644 --- a/.github/workflows/test_full.yml +++ b/.github/workflows/test_full.yml @@ -26,7 +26,7 @@ jobs: run: | python --version python -m pip install --progress-bar=off --upgrade pip - pip install --progress-bar=off "poetry!=1.4.1" + pip install --progress-bar=off "poetry<2" - name: Install py4vasp shell: bash -el {0} run: | diff --git a/poetry.lock b/poetry.lock index e5364802..122424ae 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2125,8 +2125,8 @@ files = [ [package.dependencies] numpy = [ {version = ">=1.22.4", markers = "python_version < \"3.11\""}, - {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, ] python-dateutil = ">=2.8.2" pytz = ">=2020.1" @@ -2480,8 +2480,8 @@ astroid = ">=3.2.0,<=3.3.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, + {version = ">=0.3.6", markers = "python_version >= \"3.11\""}, {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, - {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, ] isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" mccabe = ">=0.6,<0.8" @@ -2647,7 +2647,6 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, diff --git a/src/py4vasp/__init__.py b/src/py4vasp/__init__.py index 511cfcb2..b9f30e4c 100644 --- a/src/py4vasp/__init__.py +++ b/src/py4vasp/__init__.py @@ -1,10 +1,10 @@ # Copyright © VASP Software GmbH, # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) from py4vasp._analysis.mlff import MLFFErrorAnalysis -from py4vasp._calculations import Calculations +from py4vasp._batch import Batch +from py4vasp._calculation import Calculation, calculation from py4vasp._third_party.graph import plot from py4vasp._third_party.interactive import set_error_handling -from py4vasp.calculation._class import Calculation __version__ = "0.10.0" set_error_handling("Minimal") diff --git a/src/py4vasp/_analysis/mlff.py b/src/py4vasp/_analysis/mlff.py index a6bdfd29..5fa5dea3 100644 --- a/src/py4vasp/_analysis/mlff.py +++ b/src/py4vasp/_analysis/mlff.py @@ -50,9 +50,9 @@ def __init__(self, *args, **kwargs): self.dft = SimpleNamespace() @classmethod - def _from_data(cls, _calculations): + def _from_data(cls, batch): mlff_error_analysis = cls(_internal=True) - mlff_error_analysis._calculations = _calculations + mlff_error_analysis._batch = batch set_appropriate_attrs(mlff_error_analysis) return mlff_error_analysis @@ -72,10 +72,8 @@ def from_paths(cls, dft_data, mlff_data): Path to the MLFF data. Accepts wildcards. """ mlff_error_analysis = cls(_internal=True) - calculations = py4vasp.Calculations.from_paths( - dft_data=dft_data, mlff_data=mlff_data - ) - mlff_error_analysis._calculations = calculations + batch = py4vasp.Batch.from_paths(dft_data=dft_data, mlff_data=mlff_data) + mlff_error_analysis._batch = batch set_appropriate_attrs(mlff_error_analysis) return mlff_error_analysis @@ -95,10 +93,8 @@ def from_files(cls, dft_data, mlff_data): Path to the MLFF data. Accepts wildcards. """ mlff_error_analysis = cls(_internal=True) - calculations = py4vasp.Calculations.from_files( - dft_data=dft_data, mlff_data=mlff_data - ) - mlff_error_analysis._calculations = calculations + batch = py4vasp.Batch.from_files(dft_data=dft_data, mlff_data=mlff_data) + mlff_error_analysis._batch = batch set_appropriate_attrs(mlff_error_analysis) return mlff_error_analysis @@ -213,7 +209,7 @@ def set_number_of_configurations(cls): cls : MLFFErrorAnalysis An instance of MLFFErrorAnalysis. """ - number_of_calculations = cls._calculations.number_of_calculations() + number_of_calculations = cls._batch.number_of_calculations() cls.dft.nconfig = number_of_calculations["dft_data"] cls.mlff.nconfig = number_of_calculations["mlff_data"] @@ -229,7 +225,7 @@ def set_number_of_ions(cls): cls : MLFFErrorAnalysis An instance of MLFFErrorAnalysis. """ - force_data = cls._calculations.forces.read() + force_data = cls._batch.forces.read() structures_dft = _dict_to_list(force_data["dft_data"], "structure") structures_mlff = _dict_to_list(force_data["mlff_data"], "structure") elements_dft = _dict_to_array(structures_dft, "elements") @@ -252,11 +248,11 @@ def set_paths_and_files(cls): cls : MLFFErrorAnalysis An instance of MLFFErrorAnalysis. """ - paths = cls._calculations.paths() + paths = cls._batch.paths() cls.dft.paths = paths["dft_data"] cls.mlff.paths = paths["mlff_data"] - if hasattr(cls._calculations, "_files"): - files = cls._calculations.files() + if hasattr(cls._batch, "_files"): + files = cls._batch.files() cls.dft.files = files["dft_data"] cls.mlff.files = files["mlff_data"] @@ -273,7 +269,7 @@ def set_energies(cls): An instance of MLFFErrorAnalysis. """ tag = "free energy TOTEN" - energies_data = cls._calculations.energies.read() + energies_data = cls._batch.energies.read() cls.mlff.energies = _dict_to_array(energies_data["mlff_data"], tag) cls.dft.energies = _dict_to_array(energies_data["dft_data"], tag) @@ -298,7 +294,7 @@ def set_force_related_attributes(cls): cls : MLFFErrorAnalysis An instance of MLFFErrorAnalysis. """ - force_data = cls._calculations.forces.read() + force_data = cls._batch.forces.read() cls.dft.forces = _dict_to_array(force_data["dft_data"], "forces") cls.mlff.forces = _dict_to_array(force_data["mlff_data"], "forces") dft_structures = _dict_to_list(force_data["dft_data"], "structure") @@ -320,6 +316,6 @@ def set_stresses(cls): cls : MLFFErrorAnalysis An instance of MLFFErrorAnalysis. """ - stress_data = cls._calculations.stresses.read() + stress_data = cls._batch.stresses.read() cls.dft.stresses = _dict_to_array(stress_data["dft_data"], "stress") cls.mlff.stresses = _dict_to_array(stress_data["mlff_data"], "stress") diff --git a/src/py4vasp/_calculations.py b/src/py4vasp/_batch.py similarity index 89% rename from src/py4vasp/_calculations.py rename to src/py4vasp/_batch.py index 32cb2bf4..8d7d0768 100644 --- a/src/py4vasp/_calculations.py +++ b/src/py4vasp/_batch.py @@ -8,17 +8,17 @@ from py4vasp._util import convert -class Calculations: - """A class to handle multiple Calculations all at once. +class Batch: + """A class to handle batch of multiple calculations at once. This class combines the functionality of the Calculation class for more than one - calculation. Create a Calculations object using either a wildcard for a set of + calculation. Create a Batch object using either a wildcard for a set of paths or files or pass in paths and files directly. Then you can access the properties of all calculations via the attributes of the object. Examples -------- - >>> calcs = Calculations.from_paths(calc1="path_to_calc1", calc2="path_to_calc2") + >>> calcs = Batch.from_paths(calc1="path_to_calc1", calc2="path_to_calc2") >>> calcs.energies.read() # returns a dictionary with the energies of calc1 and calc2 >>> calcs.forces.read() # returns a dictionary with the forces of calc1 and calc2 >>> calcs.stresses.read() # returns a dictionary with the stresses of calc1 and calc2 @@ -34,8 +34,8 @@ class Calculations: def __init__(self, *args, **kwargs): if not kwargs.get("_internal"): message = """\ -Please setup new CompareCalculations instance using the classmethod CompareCalculations.from_paths() -or CompareCalculations.from_files() instead of the constructor CompareCalculations().""" +Please setup new Batch instance using the classmethod Batch.from_paths() +or Batch.from_files() instead of the constructor Batch().""" raise exception.IncorrectUsage(message) def _path_finder(**kwargs): @@ -53,7 +53,7 @@ def _path_finder(**kwargs): @classmethod def from_paths(cls, **kwargs): - """Set up a Calculations object for paths. + """Set up a Batch object for paths. Setup a calculation for paths by passing in a dictionary with the name of the calculation as key and the path to the calculation as value. @@ -75,7 +75,7 @@ def from_paths(cls, **kwargs): @classmethod def from_files(cls, **kwargs): - """Set up a Calculations object from files. + """Set up a Batch object from files. Setup a calculation for files by passing in a dictionary with the name of the calculation as key and the path to the calculation as value. Note that this diff --git a/src/py4vasp/calculation/_CONTCAR.py b/src/py4vasp/_calculation/_CONTCAR.py similarity index 91% rename from src/py4vasp/calculation/_CONTCAR.py rename to src/py4vasp/_calculation/_CONTCAR.py index e4eaccc1..0a631a9d 100644 --- a/src/py4vasp/calculation/_CONTCAR.py +++ b/src/py4vasp/_calculation/_CONTCAR.py @@ -1,12 +1,12 @@ # Copyright © VASP Software GmbH, # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) from py4vasp import calculation +from py4vasp._calculation import base, structure from py4vasp._third_party import view from py4vasp._util import convert -from py4vasp.calculation import _base, _structure -class CONTCAR(_base.Refinery, view.Mixin, _structure.Mixin): +class CONTCAR(base.Refinery, view.Mixin, structure.Mixin): """CONTCAR contains structural restart-data after a relaxation or MD simulation. The CONTCAR contains the final structure of the VASP calculation. It can be used as @@ -14,7 +14,7 @@ class CONTCAR(_base.Refinery, view.Mixin, _structure.Mixin): CONTCAR might contain additional information about the system such as the ion and lattice velocities.""" - @_base.data_access + @base.data_access def to_dict(self): """Extract the structural data and the available additional data to a dictionary. @@ -36,7 +36,7 @@ def _read(self, key): data = getattr(self._raw_data, key) return {key: data[:]} if not data.is_none() else {} - @_base.data_access + @base.data_access def to_view(self, supercell=None): """Generate a visualization of the final structure. @@ -52,7 +52,7 @@ def to_view(self, supercell=None): """ return self._structure.plot(supercell) - @_base.data_access + @base.data_access def __str__(self): return "\n".join(self._line_generator()) @@ -62,7 +62,7 @@ def _line_generator(self): selective_dynamics = self._raw_data.selective_dynamics yield convert.text_to_string(self._raw_data.system) yield from _cell_lines(cell) - yield self._topology().to_POSCAR() + yield self._stoichiometry().to_POSCAR() if not selective_dynamics.is_none(): yield "Selective dynamics" yield "Direct" @@ -70,8 +70,10 @@ def _line_generator(self): yield from _lattice_velocity_lines(self._raw_data.lattice_velocities, cell) yield from _ion_velocity_lines(self._raw_data.ion_velocities) - def _topology(self): - return calculation.topology.from_data(self._raw_data.structure.topology) + def _stoichiometry(self): + return calculation._stoichiometry.from_data( + self._raw_data.structure.stoichiometry + ) def _cell_lines(cell): diff --git a/src/py4vasp/calculation/_class.py b/src/py4vasp/_calculation/__init__.py similarity index 60% rename from src/py4vasp/calculation/_class.py rename to src/py4vasp/_calculation/__init__.py index 041a9253..9c1cc68d 100644 --- a/src/py4vasp/calculation/_class.py +++ b/src/py4vasp/_calculation/__init__.py @@ -1,8 +1,84 @@ # Copyright © VASP Software GmbH, # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +"""Provide refinement functions for a the raw data of a VASP calculation run in the +current directory. + +Usually one is not directly interested in the raw data that is produced but +wants to produce either a figure for a publication or some post-processing of +the data. This package contains multiple modules that enable these kinds of +workflows by extracting the relevant data from the HDF5 file and transforming +them into an accessible format. The modules also provide plotting functionality +to get a quick insight about the data, which can then be refined either within +python or a different tool to obtain publication-quality figures. + +Generally, all modules provide a `read` function that extracts the data from the +HDF5 file and puts it into a Python dictionary. Where it makes sense in addition +a `plot` function is available that converts the data into a figure for Jupyter +notebooks. In addition, data conversion routines `to_X` may be available +transforming the data into another format or file, which may be useful to +generate plots with tools other than Python. For the specifics, please refer to +the documentation of the individual modules. + +The raw data is read from the current directory. The :class:`~py4vasp.Calculation` +class provides a more flexible interface with which you can determine the source +directory or file for the VASP calculation manually. That class exposes functions +of the modules as methods of attributes, i.e., the two following examples are +equivalent: + +.. rubric:: using :mod:`~py4vasp.calculation` module + +>>> from py4vasp import calculation +>>> calculation.dos.read() + +.. rubric:: using :class:`~py4vasp.Calculation` class + +>>> from py4vasp import Calculation +>>> calc = Calculation.from_path(".") +>>> calc.dos.read() + +In the latter example, you can change the path from which the data is extracted. +""" +import importlib import pathlib -from py4vasp import calculation, control, exception +from py4vasp import control, exception +from py4vasp._util import convert + +INPUT_FILES = ("INCAR", "KPOINTS", "POSCAR") +QUANTITIES = ( + "band", + "bandgap", + "born_effective_charge", + "density", + "dielectric_function", + "dielectric_tensor", + "dos", + "elastic_modulus", + "electronic_minimization", + "energy", + "fatband", + "force", + "force_constant", + "internal_strain", + "kpoint", + "magnetism", + "pair_correlation", + "partial_density", + "phonon_band", + "phonon_dos", + "piezoelectric_tensor", + "polarization", + "potential", + "projector", + "stress", + "structure", + "system", + "velocity", + "workfunction", + "_CONTCAR", + "_dispersion", + "_stoichiometry", +) class Calculation: @@ -124,13 +200,15 @@ def path(self): def _add_all_refinement_classes(calc, add_single_class): - for name in calculation._quantities: + for name in QUANTITIES: calc = add_single_class(calc, name) return calc def _add_attribute_from_path(calc, name): - class_ = getattr(calculation, name) + class_name = convert.to_camelcase(name) + module = importlib.import_module(f"py4vasp._calculation.{name}") + class_ = getattr(module, class_name) instance = class_.from_path(calc.path()) setattr(calc, name, instance) return calc @@ -141,7 +219,9 @@ def __init__(self, file_name): self._file_name = file_name def __call__(self, calc, name): - class_ = getattr(calculation, name) + class_name = convert.to_camelcase(name) + module = importlib.import_module(f"py4vasp._calculation.{name}") + class_ = getattr(module, class_name) instance = class_.from_file(self._file_name) setattr(calc, name, instance) return calc @@ -152,13 +232,26 @@ def _add_to_documentation(calc, name): return calc -Calculation = _add_all_refinement_classes(Calculation, _add_to_documentation) - - def _add_input_files(calc): return calc # Input files are not in current release - for name in calculation._input_files: + for name in INPUT_FILES: file_ = getattr(control, name)(calc.path()) setattr(calc, f"_{name}", file_) return calc + + +Calculation = _add_all_refinement_classes(Calculation, _add_to_documentation) + + +class DefaultCalculationFactory: + def __getattr__(self, attr): + calc = Calculation.from_path(".") + return getattr(calc, attr) + + def __setattr__(self, attr, value): + calc = Calculation.from_path(".") + return setattr(calc, attr, value) + + +calculation = DefaultCalculationFactory() diff --git a/src/py4vasp/calculation/_dispersion.py b/src/py4vasp/_calculation/_dispersion.py similarity index 96% rename from src/py4vasp/calculation/_dispersion.py rename to src/py4vasp/_calculation/_dispersion.py index e2025f17..375ac7ab 100644 --- a/src/py4vasp/calculation/_dispersion.py +++ b/src/py4vasp/_calculation/_dispersion.py @@ -4,22 +4,22 @@ import py4vasp._third_party.graph as _graph from py4vasp import calculation -from py4vasp.calculation import _base +from py4vasp._calculation import base -class Dispersion(_base.Refinery): +class Dispersion(base.Refinery): """Generic class for all dispersions (electrons, phonons). Provides some utility functionalities common to all dispersions to avoid duplication of code.""" - @_base.data_access + @base.data_access def __str__(self): return f"""band data: {self._kpoints.number_kpoints()} k-points {self._raw_data.eigenvalues.shape[-1]} bands""" - @_base.data_access + @base.data_access def to_dict(self): """Read the dispersion into a dictionary. @@ -39,7 +39,7 @@ def to_dict(self): def _kpoints(self): return calculation.kpoint.from_data(self._raw_data.kpoints) - @_base.data_access + @base.data_access def plot(self, projections=None): """Generate a graph of the dispersion. diff --git a/src/py4vasp/calculation/_topology.py b/src/py4vasp/_calculation/_stoichiometry.py similarity index 80% rename from src/py4vasp/calculation/_topology.py rename to src/py4vasp/_calculation/_stoichiometry.py index 35612096..7df83671 100644 --- a/src/py4vasp/calculation/_topology.py +++ b/src/py4vasp/_calculation/_stoichiometry.py @@ -5,9 +5,9 @@ import numpy as np from py4vasp import raw +from py4vasp._calculation import base +from py4vasp._calculation.selection import Selection from py4vasp._util import check, convert, import_, select -from py4vasp.calculation import _base -from py4vasp.calculation._selection import Selection mdtraj = import_.optional("mdtraj") pd = import_.optional("pandas") @@ -15,35 +15,27 @@ _subscript = "_" -class Topology(_base.Refinery): - """The topology of the crystal describes the ions of a crystal and their connectivity. - - At the current stage, this class only exposes the name of the atoms in the unit - cell. In the future, we could add functionality for the user to group multiple - atoms. If you are interested in this feature and have a specific use case in mind, - please create an issue on Github_. - - .. _Github: https://github.com/vasp-dev/py4vasp - """ +class Stoichiometry(base.Refinery): + """The stoichiometry of the crystal describes how many ions of each type exist in a crystal.""" @classmethod def from_ase(cls, structure): - """Generate a Topology from the given ase Atoms object.""" - return cls.from_data(raw_topology_from_ase(structure)) + """Generate a stoichiometry from the given ase Atoms object.""" + return cls.from_data(raw_stoichiometry_from_ase(structure)) - @_base.data_access + @base.data_access def __str__(self): number_suffix = lambda number: str(number) if number > 1 else "" return self._create_repr(number_suffix) - @_base.data_access + @base.data_access def _repr_html_(self): number_suffix = lambda number: f"{number}" if number > 1 else "" return self._create_repr(number_suffix) - @_base.data_access + @base.data_access def to_dict(self): - """Read the topology and convert it to a dictionary. + """Read the stoichiometry and convert it to a dictionary. Returns ------- @@ -57,9 +49,9 @@ def to_dict(self): """ return {**self._default_selection(), **self._specific_selection()} - @_base.data_access + @base.data_access def to_frame(self): - """Convert the topology to a DataFrame + """Convert the stoichiometry to a DataFrame Returns ------- @@ -68,9 +60,9 @@ def to_frame(self): """ return pd.DataFrame({"name": self.names(), "element": self.elements()}) - @_base.data_access + @base.data_access def to_mdtraj(self): - """Convert the topology to a mdtraj.Topology.""" + """Convert the stoichiometry to a mdtraj.Topology.""" df = self.to_frame() df["serial"] = None df["resSeq"] = 0 @@ -78,9 +70,9 @@ def to_mdtraj(self): df["chainID"] = 0 return mdtraj.Topology.from_dataframe(df) - @_base.data_access + @base.data_access def to_POSCAR(self, format_newline=""): - """Generate the topology lines for the POSCAR file. + """Generate the stoichiometry lines for the POSCAR file. Parameters ---------- @@ -100,24 +92,24 @@ def to_POSCAR(self, format_newline=""): number_ion_types = " ".join(str(x) for x in self._raw_data.number_ion_types) return ion_types + format_newline + "\n" + number_ion_types - @_base.data_access + @base.data_access def names(self): """Extract the labels of all atoms.""" atom_dict = self.to_dict() return [val.label for val in atom_dict.values() if _subscript in val.label] - @_base.data_access + @base.data_access def elements(self): """Extract the element of all atoms.""" repeated_types = (itertools.repeat(*x) for x in self._type_numbers()) return list(itertools.chain.from_iterable(repeated_types)) - @_base.data_access + @base.data_access def ion_types(self): "Return the type of all ions in the system as string." return list(dict.fromkeys(self._ion_types)) - @_base.data_access + @base.data_access def number_atoms(self): "Return the number of atoms in the system." return np.sum(self._raw_data.number_ion_types) @@ -156,8 +148,8 @@ def _ion_types(self): return (clean_string(ion_type) for ion_type in self._raw_data.ion_types) -def raw_topology_from_ase(structure): - """Convert the given ase Atoms object to a raw.Topology.""" +def raw_stoichiometry_from_ase(structure): + """Convert the given ase Atoms object to a raw.Stoichiometry.""" number_ion_types = [] ion_types = [] for element in structure.symbols: @@ -166,7 +158,7 @@ def raw_topology_from_ase(structure): else: ion_types.append(element) number_ion_types.append(1) - return raw.Topology(number_ion_types, ion_types) + return raw.Stoichiometry(number_ion_types, ion_types) def _merge_to_slice_if_possible(selections): diff --git a/src/py4vasp/calculation/_band.py b/src/py4vasp/_calculation/band.py similarity index 87% rename from src/py4vasp/calculation/_band.py rename to src/py4vasp/_calculation/band.py index 4e18d705..70284ce5 100644 --- a/src/py4vasp/calculation/_band.py +++ b/src/py4vasp/_calculation/band.py @@ -3,15 +3,15 @@ import numpy as np from py4vasp import calculation +from py4vasp._calculation import base, projector from py4vasp._third_party import graph from py4vasp._util import check, documentation, import_ -from py4vasp.calculation import _base, _projector pd = import_.optional("pandas") pretty = import_.optional("IPython.lib.pretty") -class Band(_base.Refinery, graph.Mixin): +class Band(base.Refinery, graph.Mixin): """The band structure contains the **k** point resolved eigenvalues. The most common use case of this class is to produce the electronic band @@ -21,19 +21,19 @@ class Band(_base.Refinery, graph.Mixin): **k**-point distances that are calculated are meaningless. """ - @_base.data_access + @base.data_access def __str__(self): return f""" {"spin polarized" if self._spin_polarized() else ""} band data: {self._raw_data.dispersion.eigenvalues.shape[1]} k-points {self._raw_data.dispersion.eigenvalues.shape[2]} bands -{pretty.pretty(self._projector)} +{pretty.pretty(self._projector())} """.strip() - @_base.data_access + @base.data_access @documentation.format( - selection_doc=_projector.selection_doc, - examples=_projector.selection_examples("band", "to_dict"), + selection_doc=projector.selection_doc, + examples=projector.selection_examples("band", "to_dict"), ) def to_dict(self, selection=None): """Read the data into a dictionary. @@ -52,7 +52,7 @@ def to_dict(self, selection=None): {examples} """ - dispersion = self._dispersion.read() + dispersion = self._dispersion().read() return { "kpoint_distances": dispersion["kpoint_distances"], "kpoint_labels": dispersion["kpoint_labels"], @@ -62,10 +62,10 @@ def to_dict(self, selection=None): "projections": self._read_projections(selection), } - @_base.data_access + @base.data_access @documentation.format( - selection_doc=_projector.selection_doc, - examples=_projector.selection_examples("band", "to_graph"), + selection_doc=projector.selection_doc, + examples=projector.selection_examples("band", "to_graph"), ) def to_graph(self, selection=None, width=0.5): """Read the data and generate a graph. @@ -86,15 +86,15 @@ def to_graph(self, selection=None, width=0.5): {examples} """ projections = self._projections(selection, width) - graph = self._dispersion.plot(projections) + graph = self._dispersion().plot(projections) graph = self._shift_series_by_fermi_energy(graph) graph.ylabel = "Energy (eV)" return graph - @_base.data_access + @base.data_access @documentation.format( - selection_doc=_projector.selection_doc, - examples=_projector.selection_examples("band", "to_frame"), + selection_doc=projector.selection_doc, + examples=projector.selection_examples("band", "to_frame"), ) def to_frame(self, selection=None): """Read the data into a DataFrame. @@ -119,11 +119,9 @@ def to_frame(self, selection=None): def _spin_polarized(self): return len(self._raw_data.dispersion.eigenvalues) == 2 - @property def _dispersion(self): - return calculation.dispersion.from_data(self._raw_data.dispersion) + return calculation._dispersion.from_data(self._raw_data.dispersion) - @property def _projector(self): return calculation.projector.from_data(self._raw_data.projectors) @@ -138,7 +136,7 @@ def _projections(self, selection, width): } def _read_projections(self, selection): - return self._projector.project(selection, self._raw_data.projections) + return self._projector().project(selection, self._raw_data.projections) def _read_occupations(self): if self._spin_polarized(): diff --git a/src/py4vasp/calculation/_bandgap.py b/src/py4vasp/_calculation/bandgap.py similarity index 93% rename from src/py4vasp/calculation/_bandgap.py rename to src/py4vasp/_calculation/bandgap.py index 52a0d524..5410ebed 100644 --- a/src/py4vasp/calculation/_bandgap.py +++ b/src/py4vasp/_calculation/bandgap.py @@ -6,9 +6,9 @@ import numpy as np from py4vasp import exception +from py4vasp._calculation import base, slice_ from py4vasp._third_party import graph from py4vasp._util import convert, documentation, select -from py4vasp.calculation import _base, _slice class Gap(typing.NamedTuple): @@ -24,8 +24,8 @@ class Gap(typing.NamedTuple): COMPONENTS = ("independent", "up", "down") -@documentation.format(examples=_slice.examples("bandgap")) -class Bandgap(_slice.Mixin, _base.Refinery, graph.Mixin): +@documentation.format(examples=slice_.examples("bandgap")) +class Bandgap(slice_.Mixin, base.Refinery, graph.Mixin): """This class describes the band extrema during the relaxation or MD simulation. The bandgap represents the energy difference between the highest energy electrons @@ -44,7 +44,7 @@ class Bandgap(_slice.Mixin, _base.Refinery, graph.Mixin): {examples} """ - @_base.data_access + @base.data_access def __str__(self): template = """\ Band structure @@ -95,8 +95,8 @@ def _output_kpoint(self, label): to_string = lambda kpoint: " ".join(map("{:8.4f}".format, kpoint)) return " " + " ".join(map(to_string, kpoints)) - @_base.data_access - @documentation.format(examples=_slice.examples("bandgap", "to_dict")) + @base.data_access + @documentation.format(examples=slice_.examples("bandgap", "to_dict")) def to_dict(self): """Read the bandgap data from a VASP relaxation or MD trajectory. @@ -131,8 +131,8 @@ def _kpoint_dict(self, label): def _suffixes(self): return ("", "_up", "_down") if self._spin_polarized() else ("",) - @_base.data_access - @documentation.format(examples=_slice.examples("bandgap", "fundamental")) + @base.data_access + @documentation.format(examples=slice_.examples("bandgap", "fundamental")) def fundamental(self): """Return the fundamental bandgap. @@ -148,8 +148,8 @@ def fundamental(self): """ return self._gap("fundamental", component=0) - @_base.data_access - @documentation.format(examples=_slice.examples("bandgap", "direct")) + @base.data_access + @documentation.format(examples=slice_.examples("bandgap", "direct")) def direct(self): """Return the direct bandgap. @@ -165,8 +165,8 @@ def direct(self): """ return self._gap("direct", component=0) - @_base.data_access - @documentation.format(examples=_slice.examples("bandgap", "valence_band_maximum")) + @base.data_access + @documentation.format(examples=slice_.examples("bandgap", "valence_band_maximum")) def valence_band_maximum(self): """Return the valence band maximum. @@ -179,9 +179,9 @@ def valence_band_maximum(self): """ return self._get(GAPS["fundamental"].bottom, component=0) - @_base.data_access + @base.data_access @documentation.format( - examples=_slice.examples("bandgap", "conduction_band_minimum") + examples=slice_.examples("bandgap", "conduction_band_minimum") ) def conduction_band_minimum(self): """Return the conduction band minimum. @@ -195,8 +195,8 @@ def conduction_band_minimum(self): """ return self._get(GAPS["fundamental"].top, component=0) - @_base.data_access - @documentation.format(examples=_slice.examples("bandgap", "to_graph")) + @base.data_access + @documentation.format(examples=slice_.examples("bandgap", "to_graph")) def to_graph(self, selection="fundamental, direct"): """Plot the direct and fundamental bandgap along the trajectory. diff --git a/src/py4vasp/calculation/_base.py b/src/py4vasp/_calculation/base.py similarity index 100% rename from src/py4vasp/calculation/_base.py rename to src/py4vasp/_calculation/base.py diff --git a/src/py4vasp/calculation/_born_effective_charge.py b/src/py4vasp/_calculation/born_effective_charge.py similarity index 93% rename from src/py4vasp/calculation/_born_effective_charge.py rename to src/py4vasp/_calculation/born_effective_charge.py index c81d4650..2100c76f 100644 --- a/src/py4vasp/calculation/_born_effective_charge.py +++ b/src/py4vasp/_calculation/born_effective_charge.py @@ -1,9 +1,9 @@ # Copyright © VASP Software GmbH, # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) -from py4vasp.calculation import _base, _structure +from py4vasp._calculation import base, structure -class BornEffectiveCharge(_base.Refinery, _structure.Mixin): +class BornEffectiveCharge(base.Refinery, structure.Mixin): """The Born effective charge tensors couple electric field and atomic displacement. You can use this class to extract the Born effective charges of a linear @@ -15,7 +15,7 @@ class BornEffectiveCharge(_base.Refinery, _structure.Mixin): piezoelectric and ferroelectric behavior. """ - @_base.data_access + @base.data_access def __str__(self): data = self.to_dict() result = """ @@ -32,7 +32,7 @@ def __str__(self): 3 {vec_to_string(charge_tensor[2])}""" return result - @_base.data_access + @base.data_access def to_dict(self): """Read structure information and Born effective charges into a dictionary. diff --git a/src/py4vasp/calculation/_density.py b/src/py4vasp/_calculation/density.py similarity index 97% rename from src/py4vasp/calculation/_density.py rename to src/py4vasp/_calculation/density.py index bb06f75f..f7dddfb1 100644 --- a/src/py4vasp/calculation/_density.py +++ b/src/py4vasp/_calculation/density.py @@ -3,9 +3,9 @@ import numpy as np from py4vasp import _config, calculation, exception +from py4vasp._calculation import base, structure from py4vasp._third_party import graph, view from py4vasp._util import documentation, import_, index, select, slicing -from py4vasp.calculation import _base, _structure pretty = import_.optional("IPython.lib.pretty") @@ -68,7 +68,7 @@ def _join_with_emphasis(data): return ", ".join(emph_data) -class Density(_base.Refinery, _structure.Mixin, view.Mixin): +class Density(base.Refinery, structure.Mixin, view.Mixin): """This class accesses various densities (charge, magnetization, ...) of VASP. The charge density is one key quantity optimized by VASP. With this class you @@ -79,11 +79,12 @@ class Density(_base.Refinery, _structure.Mixin, view.Mixin): metaGGA calculations. """ - @_base.data_access + @base.data_access def __str__(self): _raise_error_if_no_data(self._raw_data.charge) grid = self._raw_data.charge.shape[1:] - topology = calculation.topology.from_data(self._raw_data.structure.topology) + raw_stoichiometry = self._raw_data.structure.stoichiometry + stoichiometry = calculation._stoichiometry.from_data(raw_stoichiometry) if self._selection == "kinetic_energy": name = "Kinetic energy" elif self.is_nonpolarized(): @@ -93,7 +94,7 @@ def __str__(self): else: name = "Noncollinear" return f"""{name} density: - structure: {pretty.pretty(topology)} + structure: {pretty.pretty(stoichiometry)} grid: {grid[2]}, {grid[1]}, {grid[0]}""" @documentation.format( @@ -102,7 +103,7 @@ def __str__(self): component2=_join_with_emphasis(_COMPONENTS[2]), component3=_join_with_emphasis(_COMPONENTS[3]), ) - @_base.data_access + @base.data_access def selections(self): """Returns possible densities VASP can produce along with all available components. @@ -165,7 +166,7 @@ def selections(self): components = [_COMPONENTS[i][_DEFAULT] for i in range(4)] return {**sources, "component": components} - @_base.data_access + @base.data_access def to_dict(self): """Read the density into a dictionary. @@ -198,7 +199,7 @@ def _read_density(self): elif self.is_noncollinear(): yield "magnetization", density[1:] - @_base.data_access + @base.data_access def to_numpy(self): """Convert the density to a numpy array. @@ -213,7 +214,7 @@ def to_numpy(self): """ return np.moveaxis(self._raw_data.charge, 0, -1).T - @_base.data_access + @base.data_access def to_view(self, selection=None, supercell=None, **user_options): """Plot the selected density as a 3d isosurface within the structure. @@ -329,7 +330,7 @@ def _use_symmetric_isosurface(self, component): _raise_is_collinear_error() return component > 0 - @_base.data_access + @base.data_access @documentation.format(plane=_PLANE, common_parameters=_COMMON_PARAMETERS) def to_contour( self, selection=None, *, a=None, b=None, c=None, supercell=None, normal=None @@ -391,7 +392,7 @@ def _contour(self, selector, selection, plane, fraction, supercell): contour.supercell = np.ones(2, dtype=np.int_) * supercell return contour - @_base.data_access + @base.data_access @documentation.format(plane=_PLANE, common_parameters=_COMMON_PARAMETERS) def to_quiver(self, *, a=None, b=None, c=None, supercell=None, normal=None): """Generate a quiver plot of magnetization density. @@ -451,17 +452,17 @@ def _get_cut(self, a, b, c): return "b", b return "c", c - @_base.data_access + @base.data_access def is_nonpolarized(self): "Returns whether the density is not spin polarized." return len(self._raw_data.charge) == 1 - @_base.data_access + @base.data_access def is_collinear(self): "Returns whether the density has a collinear magnetization." return len(self._raw_data.charge) == 2 - @_base.data_access + @base.data_access def is_noncollinear(self): "Returns whether the density has a noncollinear magnetization." return len(self._raw_data.charge) == 4 diff --git a/src/py4vasp/calculation/_dielectric_function.py b/src/py4vasp/_calculation/dielectric_function.py similarity index 97% rename from src/py4vasp/calculation/_dielectric_function.py rename to src/py4vasp/_calculation/dielectric_function.py index eab984ce..c232c178 100644 --- a/src/py4vasp/calculation/_dielectric_function.py +++ b/src/py4vasp/_calculation/dielectric_function.py @@ -2,12 +2,12 @@ # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) import numpy as np +from py4vasp._calculation import base from py4vasp._third_party import graph from py4vasp._util import convert, index, select -from py4vasp.calculation import _base -class DielectricFunction(_base.Refinery, graph.Mixin): +class DielectricFunction(base.Refinery, graph.Mixin): """The dielectric function describes the material response to an electric field. The dielectric function is a fundamental concept that describes how a material @@ -28,7 +28,7 @@ class provides a common interface to all of them. You can pass a `selection` one of the six components as selection. """ - @_base.data_access + @base.data_access def __str__(self): energies = self._raw_data.energies return f""" @@ -43,7 +43,7 @@ def _components(self): else: return "" - @_base.data_access + @base.data_access def to_dict(self): """Read the data into a dictionary. @@ -69,7 +69,7 @@ def _add_current_current_if_available(self): def _has_current_component(self): return not self._raw_data.current_current.is_none() - @_base.data_access + @base.data_access def to_graph(self, selection=None): """Read the data and generate a figure with the selected directions. @@ -93,7 +93,7 @@ def to_graph(self, selection=None): ylabel="dielectric function ϵ", ) - @_base.data_access + @base.data_access def selections(self): "Returns a dictionary of possible selections for component, direction, and complex value." components = ( diff --git a/src/py4vasp/calculation/_dielectric_tensor.py b/src/py4vasp/_calculation/dielectric_tensor.py similarity index 95% rename from src/py4vasp/calculation/_dielectric_tensor.py rename to src/py4vasp/_calculation/dielectric_tensor.py index a4b734dd..e841a325 100644 --- a/src/py4vasp/calculation/_dielectric_tensor.py +++ b/src/py4vasp/_calculation/dielectric_tensor.py @@ -1,11 +1,11 @@ # Copyright © VASP Software GmbH, # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) from py4vasp import exception +from py4vasp._calculation import base from py4vasp._util import convert -from py4vasp.calculation import _base -class DielectricTensor(_base.Refinery): +class DielectricTensor(base.Refinery): """The dielectric tensor is the static limit of the :attr:`dielectric function`. The dielectric tensor represents how a material's response to an external electric @@ -14,7 +14,7 @@ class DielectricTensor(_base.Refinery): tensor corresponds to the dielectric function along a specific crystallographic axis.""" - @_base.data_access + @base.data_access def to_dict(self): """Read the dielectric tensor into a dictionary. @@ -31,7 +31,7 @@ def to_dict(self): "method": convert.text_to_string(self._raw_data.method), } - @_base.data_access + @base.data_access def __str__(self): data = self.to_dict() return f""" diff --git a/src/py4vasp/calculation/_dos.py b/src/py4vasp/_calculation/dos.py similarity index 90% rename from src/py4vasp/calculation/_dos.py rename to src/py4vasp/_calculation/dos.py index 978eb9ea..04a3e331 100644 --- a/src/py4vasp/calculation/_dos.py +++ b/src/py4vasp/_calculation/dos.py @@ -1,15 +1,15 @@ # Copyright © VASP Software GmbH, # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) from py4vasp import calculation +from py4vasp._calculation import base, projector from py4vasp._third_party import graph from py4vasp._util import documentation, import_ -from py4vasp.calculation import _base, _projector pd = import_.optional("pandas") pretty = import_.optional("IPython.lib.pretty") -class Dos(_base.Refinery, graph.Mixin): +class Dos(base.Refinery, graph.Mixin): """The density of states (DOS) describes the number of states per energy. The DOS quantifies the distribution of electronic states within an energy range @@ -31,7 +31,7 @@ class Dos(_base.Refinery, graph.Mixin): _missing_data_message = "No DOS data found, please verify that LORBIT flag is set." - @_base.data_access + @base.data_access def __str__(self): energies = self._raw_data.energies return f""" @@ -40,10 +40,10 @@ def __str__(self): {pretty.pretty(self._projectors())} """.strip() - @_base.data_access + @base.data_access @documentation.format( - selection_doc=_projector.selection_doc, - examples=_projector.selection_examples("dos", "to_dict"), + selection_doc=projector.selection_doc, + examples=projector.selection_examples("dos", "to_dict"), ) def to_dict(self, selection=None): """Read the data into a dictionary. @@ -67,10 +67,10 @@ def to_dict(self, selection=None): "fermi_energy": self._raw_data.fermi_energy, } - @_base.data_access + @base.data_access @documentation.format( - selection_doc=_projector.selection_doc, - examples=_projector.selection_examples("dos", "to_graph"), + selection_doc=projector.selection_doc, + examples=projector.selection_examples("dos", "to_graph"), ) def to_graph(self, selection=None): """Generate a graph of the selected data reading it from the VASP output. @@ -96,10 +96,10 @@ def to_graph(self, selection=None): ylabel="DOS (1/eV)", ) - @_base.data_access + @base.data_access @documentation.format( - selection_doc=_projector.selection_doc, - examples=_projector.selection_examples("dos", "to_frame"), + selection_doc=projector.selection_doc, + examples=projector.selection_examples("dos", "to_frame"), ) def to_frame(self, selection=None): """Read the data into a pandas DataFrame. diff --git a/src/py4vasp/calculation/_elastic_modulus.py b/src/py4vasp/_calculation/elastic_modulus.py similarity index 95% rename from src/py4vasp/calculation/_elastic_modulus.py rename to src/py4vasp/_calculation/elastic_modulus.py index 5d1feab5..e8af9ea2 100644 --- a/src/py4vasp/calculation/_elastic_modulus.py +++ b/src/py4vasp/_calculation/elastic_modulus.py @@ -2,10 +2,10 @@ # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) import numpy as np -from py4vasp.calculation import _base +from py4vasp._calculation import base -class ElasticModulus(_base.Refinery): +class ElasticModulus(base.Refinery): """The elastic modulus is the second derivative of the energy with respect to strain. The elastic modulus, also known as the modulus of elasticity, is a measure of a @@ -18,7 +18,7 @@ class ElasticModulus(_base.Refinery): atoms are allowed to relax when the cell is deformed. """ - @_base.data_access + @base.data_access def to_dict(self): """Read the clamped-ion and relaxed-ion elastic modulus into a dictionary. @@ -32,7 +32,7 @@ def to_dict(self): "relaxed_ion": self._raw_data.relaxed_ion[:], } - @_base.data_access + @base.data_access def __str__(self): return f"""Elastic modulus (kBar) Direction XX YY ZZ XY YZ ZX diff --git a/src/py4vasp/calculation/_electronic_minimization.py b/src/py4vasp/_calculation/electronic_minimization.py similarity index 96% rename from src/py4vasp/calculation/_electronic_minimization.py rename to src/py4vasp/_calculation/electronic_minimization.py index 51b2d670..d5e2acc8 100644 --- a/src/py4vasp/calculation/_electronic_minimization.py +++ b/src/py4vasp/_calculation/electronic_minimization.py @@ -4,11 +4,11 @@ import numpy as np from py4vasp import exception, raw +from py4vasp._calculation import base, slice_ from py4vasp._third_party import graph -from py4vasp.calculation import _base, _slice -class ElectronicMinimization(_slice.Mixin, _base.Refinery, graph.Mixin): +class ElectronicMinimization(slice_.Mixin, base.Refinery, graph.Mixin): """Access the convergence data for each electronic step. The OSZICAR file written out by VASP stores information related to convergence. @@ -18,7 +18,7 @@ class ElectronicMinimization(_slice.Mixin, _base.Refinery, graph.Mixin): def _more_than_one_ionic_step(self, data): return any(isinstance(_data, list) for _data in data) == True - @_base.data_access + @base.data_access def __str__(self): format_rep = "{0:g}\t{1:0.12E}\t{2:0.6E}\t{3:0.6E}\t{4:g}\t{5:0.3E}\t{6:0.3E}\n" label_rep = "{}\t\t{}\t\t{}\t\t{}\t\t{}\t{}\t\t{}\n" @@ -44,7 +44,7 @@ def __str__(self): string += format_rep.format(*_data) return string - @_base.data_access + @base.data_access def to_dict(self, selection=None): """Extract convergence data from the HDF5 file and make it available in a dict @@ -79,7 +79,7 @@ def to_dict(self, selection=None): def _from_bytes_to_utf(self, quantity: list): return [_quantity.decode("utf-8") for _quantity in quantity] - @_base.data_access + @base.data_access def _read(self, key): # data represents all of the electronic steps for all ionic steps data = getattr(self._raw_data, "convergence_data") @@ -121,7 +121,7 @@ def to_graph(self, selection="E"): ylabel=ylabel, ) - @_base.data_access + @base.data_access def is_converged(self): is_elmin_converged = self._raw_data.is_elmin_converged[self._steps] converged = is_elmin_converged == 0 diff --git a/src/py4vasp/calculation/_energy.py b/src/py4vasp/_calculation/energy.py similarity index 94% rename from src/py4vasp/calculation/_energy.py rename to src/py4vasp/_calculation/energy.py index 88ca3c47..fa545d83 100644 --- a/src/py4vasp/calculation/_energy.py +++ b/src/py4vasp/_calculation/energy.py @@ -2,9 +2,9 @@ # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) import numpy as np +from py4vasp._calculation import base, slice_ from py4vasp._third_party import graph from py4vasp._util import convert, documentation, index, select -from py4vasp.calculation import _base, _slice def _selection_string(default): @@ -31,8 +31,8 @@ def _selection_string(default): } -@documentation.format(examples=_slice.examples("energy")) -class Energy(_slice.Mixin, _base.Refinery, graph.Mixin): +@documentation.format(examples=slice_.examples("energy")) +class Energy(slice_.Mixin, base.Refinery, graph.Mixin): """The energy data for one or several steps of a relaxation or MD simulation. You can use this class to inspect how the ionic relaxation converges or @@ -49,7 +49,7 @@ class Energy(_slice.Mixin, _base.Refinery, graph.Mixin): {examples} """ - @_base.data_access + @base.data_access def __str__(self): text = f"Energies at {self._step_string()}:" values = self._raw_data.values[self._last_step_in_slice] @@ -69,10 +69,10 @@ def _step_string(self): else: return f"step {self._steps + 1}" - @_base.data_access + @base.data_access @documentation.format( selection=_selection_string("all energies"), - examples=_slice.examples("energy", "to_dict"), + examples=slice_.examples("energy", "to_dict"), ) def to_dict(self, selection=None): """Read the energy data and store it in a dictionary. @@ -101,10 +101,10 @@ def _default_dict(self): for label, value in zip(self._raw_data.labels, raw_values) } - @_base.data_access + @base.data_access @documentation.format( selection=_selection_string("the total energy"), - examples=_slice.examples("energy", "to_graph"), + examples=slice_.examples("energy", "to_graph"), ) def to_graph(self, selection="TOTEN"): """Read the energy data and generate a figure of the selected components. @@ -129,10 +129,10 @@ def to_graph(self, selection="TOTEN"): y2label=yaxes.y2label, ) - @_base.data_access + @base.data_access @documentation.format( selection=_selection_string("the total energy"), - examples=_slice.examples("energy", "to_numpy"), + examples=slice_.examples("energy", "to_numpy"), ) def to_numpy(self, selection="TOTEN"): """Read the energy of the selected steps. @@ -153,7 +153,7 @@ def to_numpy(self, selection="TOTEN"): tree = select.Tree.from_selection(selection) return np.squeeze([values for _, values in self._read_data(tree, self._steps)]) - @_base.data_access + @base.data_access def selections(self): """Returns all possible selections you can use for the other routines. diff --git a/src/py4vasp/calculation/_fatband.py b/src/py4vasp/_calculation/fatband.py similarity index 92% rename from src/py4vasp/calculation/_fatband.py rename to src/py4vasp/_calculation/fatband.py index 0417f730..ef6dafd0 100644 --- a/src/py4vasp/calculation/_fatband.py +++ b/src/py4vasp/_calculation/fatband.py @@ -1,11 +1,11 @@ # Copyright © VASP Software GmbH, # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) from py4vasp import calculation +from py4vasp._calculation import base from py4vasp._util import convert -from py4vasp.calculation import _base -class Fatband(_base.Refinery): +class Fatband(base.Refinery): """BSE fatbands illustrate the excitonic properties of materials. The Bethe-Salpeter Equation (BSE) accounts for electron-hole interactions @@ -18,7 +18,7 @@ class Fatband(_base.Refinery): in materials. """ - @_base.data_access + @base.data_access def __str__(self): shape = self._raw_data.bse_index.shape return f"""BSE fatband data: @@ -26,7 +26,7 @@ def __str__(self): {shape[3]} valence bands {shape[2]} conduction bands""" - @_base.data_access + @base.data_access def to_dict(self): """Read the data into a dictionary. @@ -55,4 +55,4 @@ def to_dict(self): @property def _dispersion(self): - return calculation.dispersion.from_data(self._raw_data.dispersion) + return calculation._dispersion.from_data(self._raw_data.dispersion) diff --git a/src/py4vasp/calculation/_force.py b/src/py4vasp/_calculation/force.py similarity index 90% rename from src/py4vasp/calculation/_force.py rename to src/py4vasp/_calculation/force.py index fe94aff7..94f0be9a 100644 --- a/src/py4vasp/calculation/_force.py +++ b/src/py4vasp/_calculation/force.py @@ -3,13 +3,13 @@ import numpy as np from py4vasp import _config +from py4vasp._calculation import base, slice_, structure from py4vasp._third_party import view from py4vasp._util import documentation, reader -from py4vasp.calculation import _base, _slice, _structure -@documentation.format(examples=_slice.examples("force")) -class Force(_slice.Mixin, _base.Refinery, _structure.Mixin, view.Mixin): +@documentation.format(examples=slice_.examples("force")) +class Force(slice_.Mixin, base.Refinery, structure.Mixin, view.Mixin): """The forces determine the path of the atoms in a trajectory. You can use this class to analyze the forces acting on the atoms. The forces @@ -27,7 +27,7 @@ class Force(_slice.Mixin, _base.Refinery, _structure.Mixin, view.Mixin): force_rescale = 1.5 "Scaling constant to convert forces to Å." - @_base.data_access + @base.data_access def __str__(self): "Convert the forces to a format similar to the OUTCAR file." result = """ @@ -42,8 +42,8 @@ def __str__(self): result += f"\n{position_to_string(position)} {force_to_string(force)}" return result - @_base.data_access - @documentation.format(examples=_slice.examples("force", "to_dict")) + @base.data_access + @documentation.format(examples=slice_.examples("force", "to_dict")) def to_dict(self): """Read the forces and associated structural information for one or more selected steps of the trajectory. @@ -61,8 +61,8 @@ def to_dict(self): "forces": self._force[self._steps], } - @_base.data_access - @documentation.format(examples=_slice.examples("force", "to_view")) + @base.data_access + @documentation.format(examples=slice_.examples("force", "to_view")) def to_view(self, supercell=None): """Visualize the forces showing arrows at the atoms. diff --git a/src/py4vasp/calculation/_force_constant.py b/src/py4vasp/_calculation/force_constant.py similarity index 94% rename from src/py4vasp/calculation/_force_constant.py rename to src/py4vasp/_calculation/force_constant.py index 25096579..3e9e6c7b 100644 --- a/src/py4vasp/calculation/_force_constant.py +++ b/src/py4vasp/_calculation/force_constant.py @@ -2,10 +2,10 @@ # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) import itertools -from py4vasp.calculation import _base, _structure +from py4vasp._calculation import base, structure -class ForceConstant(_base.Refinery, _structure.Mixin): +class ForceConstant(base.Refinery, structure.Mixin): """Force constants are the 2nd derivatives of the energy with respect to displacement. Force constants quantify the strength of interactions between atoms in a crystal @@ -18,7 +18,7 @@ class ForceConstant(_base.Refinery, _structure.Mixin): a careful relaxation is required to eliminate the first derivative (i.e. forces). """ - @_base.data_access + @base.data_access def __str__(self): result = """ Force constants (eV/Ų): @@ -35,7 +35,7 @@ def __str__(self): result += f"\n{i + 1:6d} {j + 1:6d} {string_representation}" return result - @_base.data_access + @base.data_access def to_dict(self): """Read structure information and force constants into a dictionary. diff --git a/src/py4vasp/calculation/_internal_strain.py b/src/py4vasp/_calculation/internal_strain.py similarity index 93% rename from src/py4vasp/calculation/_internal_strain.py rename to src/py4vasp/_calculation/internal_strain.py index d40cc727..601b10c0 100644 --- a/src/py4vasp/calculation/_internal_strain.py +++ b/src/py4vasp/_calculation/internal_strain.py @@ -1,9 +1,9 @@ # Copyright © VASP Software GmbH, # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) -from py4vasp.calculation import _base, _structure +from py4vasp._calculation import base, structure -class InternalStrain(_base.Refinery, _structure.Mixin): +class InternalStrain(base.Refinery, structure.Mixin): """The internal strain is the derivative of energy with respect to displacement and strain. The internal strain tensor characterizes the deformation within a material at @@ -14,7 +14,7 @@ class InternalStrain(_base.Refinery, _structure.Mixin): with linear response and this class provides access to the resulting data. """ - @_base.data_access + @base.data_access def __str__(self): result = """ Internal strain tensor (eV/Å): @@ -28,7 +28,7 @@ def __str__(self): ion_string = " " return result.strip() - @_base.data_access + @base.data_access def to_dict(self): """Read the internal strain to a dictionary. diff --git a/src/py4vasp/calculation/_kpoint.py b/src/py4vasp/_calculation/kpoint.py similarity index 97% rename from src/py4vasp/calculation/_kpoint.py rename to src/py4vasp/_calculation/kpoint.py index ea301810..838fd2bf 100644 --- a/src/py4vasp/calculation/_kpoint.py +++ b/src/py4vasp/_calculation/kpoint.py @@ -6,8 +6,8 @@ import numpy as np from py4vasp import exception +from py4vasp._calculation import base from py4vasp._util import convert, documentation -from py4vasp.calculation import _base _kpoints_selection = """\ selection : str, optional @@ -16,7 +16,7 @@ """ -class Kpoint(_base.Refinery): +class Kpoint(base.Refinery): """The **k**-point mesh used in the VASP calculation. In VASP calculations, **k** points play an important role in discretizing the @@ -39,7 +39,7 @@ class Kpoint(_base.Refinery): selected **k** point mesh or take subsets along high symmetry lines. """ - @_base.data_access + @base.data_access def __str__(self): text = f"""k-points {len(self._raw_data.coordinates)} @@ -48,7 +48,7 @@ def __str__(self): text += "\n" + f"{kpoint[0]} {kpoint[1]} {kpoint[2]} {weight}" return text - @_base.data_access + @base.data_access @documentation.format(selection=_kpoints_selection) def to_dict(self): """Read the **k** points data into a dictionary. @@ -75,7 +75,7 @@ def to_dict(self): "labels": self.labels(), } - @_base.data_access + @base.data_access @documentation.format(selection=_kpoints_selection) def line_length(self): """Get the number of points per line in the Brillouin zone. @@ -93,7 +93,7 @@ def line_length(self): return self._raw_data.number return self.number_kpoints() - @_base.data_access + @base.data_access @documentation.format(selection=_kpoints_selection) def number_lines(self): """Get the number of lines in the Brillouin zone. @@ -109,7 +109,7 @@ def number_lines(self): """ return self.number_kpoints() // self.line_length() - @_base.data_access + @base.data_access @documentation.format(selection=_kpoints_selection) def number_kpoints(self): """Get the number of points in the Brillouin zone. @@ -125,7 +125,7 @@ def number_kpoints(self): """ return len(self._raw_data.coordinates) - @_base.data_access + @base.data_access @documentation.format(selection=_kpoints_selection) def distances(self): """Convert the coordinates of the **k** points into a one dimensional array @@ -154,7 +154,7 @@ def distances(self): ) return functools.reduce(concatenate_distances, kpoint_norms) - @_base.data_access + @base.data_access @documentation.format(selection=_kpoints_selection) def mode(self): """Get the **k**-point generation mode specified in the Vasp input file @@ -187,7 +187,7 @@ def mode(self): f"Could not understand the mode '{mode}' when refining the raw kpoints data." ) - @_base.data_access + @base.data_access @documentation.format(selection=_kpoints_selection) def labels(self): """Get any labels given in the input file for specific **k** points. @@ -209,7 +209,7 @@ def labels(self): else: return None - @_base.data_access + @base.data_access @documentation.format(selection=_kpoints_selection) def path_indices(self, start, finish): """Find linear dependent k points between start and finish diff --git a/src/py4vasp/calculation/_magnetism.py b/src/py4vasp/_calculation/magnetism.py similarity index 88% rename from src/py4vasp/calculation/_magnetism.py rename to src/py4vasp/_calculation/magnetism.py index 85669cf4..dc38b632 100644 --- a/src/py4vasp/calculation/_magnetism.py +++ b/src/py4vasp/_calculation/magnetism.py @@ -3,9 +3,9 @@ import numpy as np from py4vasp import _config, exception +from py4vasp._calculation import base, slice_, structure from py4vasp._third_party import view from py4vasp._util import documentation -from py4vasp.calculation import _base, _slice, _structure _index_note = """\ Notes @@ -22,8 +22,8 @@ """ -@documentation.format(examples=_slice.examples("magnetism")) -class Magnetism(_slice.Mixin, _base.Refinery, _structure.Mixin, view.Mixin): +@documentation.format(examples=slice_.examples("magnetism")) +class Magnetism(slice_.Mixin, base.Refinery, structure.Mixin, view.Mixin): """The local moments describe the charge and magnetization near an atom. The projection on local moments is particularly relevant in the context of @@ -51,7 +51,7 @@ class Magnetism(_slice.Mixin, _base.Refinery, _structure.Mixin, view.Mixin): length_moments = 1.5 "Length in Å how a magnetic moment is displayed relative to the largest moment." - @_base.data_access + @base.data_access def __str__(self): magmom = "MAGMOM = " moments_last_step = self.total_moments() @@ -65,9 +65,9 @@ def __str__(self): generator = (moments_to_string(vec) for vec in moments_last_step) return magmom + separator.join(generator) - @_base.data_access + @base.data_access @documentation.format( - index_note=_index_note, examples=_slice.examples("magnetism", "to_dict") + index_note=_index_note, examples=slice_.examples("magnetism", "to_dict") ) def to_dict(self): """Read the charges and magnetization data into a dictionary. @@ -88,9 +88,9 @@ def to_dict(self): **self._add_spin_and_orbital_moments(), } - @_base.data_access + @base.data_access @documentation.format( - selection=_moment_selection, examples=_slice.examples("magnetism", "to_view") + selection=_moment_selection, examples=slice_.examples("magnetism", "to_view") ) def to_view(self, selection="total", supercell=None): """Visualize the magnetic moments as arrows inside the structure. @@ -121,8 +121,8 @@ def to_view(self, selection="total", supercell=None): viewer.ion_arrows = [ion_arrows] return viewer - @_base.data_access - @documentation.format(examples=_slice.examples("magnetism", "charges")) + @base.data_access + @documentation.format(examples=slice_.examples("magnetism", "charges")) def charges(self): """Read the charges of the selected steps. @@ -136,11 +136,11 @@ def charges(self): self._raise_error_if_steps_out_of_bounds() return self._raw_data.spin_moments[self._steps, 0] - @_base.data_access + @base.data_access @documentation.format( selection=_moment_selection, index_note=_index_note, - examples=_slice.examples("magnetism", "moments"), + examples=slice_.examples("magnetism", "moments"), ) def moments(self, selection="total"): """Read the magnetic moments of the selected steps. @@ -168,8 +168,8 @@ def moments(self, selection="total"): else: return self._noncollinear_moments(selection) - @_base.data_access - @documentation.format(examples=_slice.examples("magnetism", "total_charges")) + @base.data_access + @documentation.format(examples=slice_.examples("magnetism", "total_charges")) def total_charges(self): """Read the total charges of the selected steps. @@ -183,11 +183,11 @@ def total_charges(self): """ return _sum_over_orbitals(self.charges()) - @_base.data_access + @base.data_access @documentation.format( selection=_moment_selection, index_note=_index_note, - examples=_slice.examples("magnetism", "total_moments"), + examples=slice_.examples("magnetism", "total_moments"), ) def total_moments(self, selection="total"): """Read the total magnetic moments of the selected steps. @@ -228,11 +228,8 @@ def _collinear_moments(self): return self._raw_data.spin_moments[self._steps, 1] def _noncollinear_moments(self, selection): - spin_moments = self._raw_data.spin_moments[self._steps, 1:] - if self._has_orbital_moments: - orbital_moments = self._raw_data.orbital_moments[self._steps, 1:] - else: - orbital_moments = np.zeros_like(spin_moments) + spin_moments = self._spin_moments() + orbital_moments = self._orbital_moments(spin_moments) if selection == "orbital": moments = orbital_moments elif selection == "spin": @@ -242,11 +239,21 @@ def _noncollinear_moments(self, selection): direction_axis = 1 if moments.ndim == 4 else 0 return np.moveaxis(moments, direction_axis, -1) + def _spin_moments(self): + return self._raw_data.spin_moments[self._steps, 1:] + + def _orbital_moments(self, spin_moments): + if not self._has_orbital_moments: + return np.zeros_like(spin_moments) + zero_s_moments = np.zeros((*spin_moments.shape[:-1], 1)) + orbital_moments = self._raw_data.orbital_moments[self._steps] + return np.concatenate((zero_s_moments, orbital_moments), axis=-1) + def _add_spin_and_orbital_moments(self): if not self._has_orbital_moments: return {} - spin_moments = self._raw_data.spin_moments[self._steps, 1:] - orbital_moments = self._raw_data.orbital_moments[self._steps, 1:] + spin_moments = self._spin_moments() + orbital_moments = self._orbital_moments(spin_moments) direction_axis = 1 if spin_moments.ndim == 4 else 0 return { "spin_moments": np.moveaxis(spin_moments, direction_axis, -1), diff --git a/src/py4vasp/calculation/_pair_correlation.py b/src/py4vasp/_calculation/pair_correlation.py similarity index 91% rename from src/py4vasp/calculation/_pair_correlation.py rename to src/py4vasp/_calculation/pair_correlation.py index 92fbf296..74dcd4af 100644 --- a/src/py4vasp/calculation/_pair_correlation.py +++ b/src/py4vasp/_calculation/pair_correlation.py @@ -1,8 +1,8 @@ # Copyright © VASP Software GmbH, # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +from py4vasp._calculation import base, slice_ from py4vasp._third_party import graph from py4vasp._util import convert, documentation, index, select -from py4vasp.calculation import _base, _slice def _selection_string(default): @@ -19,8 +19,8 @@ def _selection_string(default): """ -@documentation.format(examples=_slice.examples("pair_correlation", step="block")) -class PairCorrelation(_slice.Mixin, _base.Refinery, graph.Mixin): +@documentation.format(examples=slice_.examples("pair_correlation", step="block")) +class PairCorrelation(slice_.Mixin, base.Refinery, graph.Mixin): """The pair-correlation function measures the distribution of atoms. A pair-correlation function is a statistical measure to describe the spatial @@ -40,10 +40,10 @@ class PairCorrelation(_slice.Mixin, _base.Refinery, graph.Mixin): {examples} """ - @_base.data_access + @base.data_access @documentation.format( selection=_selection_string("all possibilities are read"), - examples=_slice.examples("pair_correlation", "to_dict", "block"), + examples=slice_.examples("pair_correlation", "to_dict", "block"), ) def to_dict(self, selection=None): """Read the pair-correlation function and store it in a dictionary. @@ -68,10 +68,10 @@ def to_dict(self, selection=None): **self._read_data(selection), } - @_base.data_access + @base.data_access @documentation.format( selection=_selection_string("the total pair correlation is used"), - examples=_slice.examples("pair_correlation", "to_graph", "block"), + examples=slice_.examples("pair_correlation", "to_graph", "block"), ) def to_graph(self, selection="total"): """Plot selected pair-correlation functions. @@ -92,7 +92,7 @@ def to_graph(self, selection="total"): series = self._make_series(self.to_dict(selection)) return graph.Graph(series, xlabel="Distance (Å)", ylabel="Pair correlation") - @_base.data_access + @base.data_access def labels(self): "Return all possible labels for the selection string." return tuple(convert.text_to_string(label) for label in self._raw_data.labels) diff --git a/src/py4vasp/calculation/_partial_charge.py b/src/py4vasp/_calculation/partial_density.py similarity index 95% rename from src/py4vasp/calculation/_partial_charge.py rename to src/py4vasp/_calculation/partial_density.py index 7834bb49..4867d7be 100644 --- a/src/py4vasp/calculation/_partial_charge.py +++ b/src/py4vasp/_calculation/partial_density.py @@ -7,11 +7,11 @@ import numpy as np from py4vasp import exception +from py4vasp._calculation import base, structure from py4vasp._third_party.graph import Graph from py4vasp._third_party.graph.contour import Contour from py4vasp._util import import_, select from py4vasp._util.slicing import plane -from py4vasp.calculation import _base, _structure interpolate = import_.optional("scipy.interpolate") ndimage = import_.optional("scipy.ndimage") @@ -50,7 +50,7 @@ class STM_settings: interpolation_factor: int = 10 -class PartialCharge(_base.Refinery, _structure.Mixin): +class PartialDensity(base.Refinery, structure.Mixin): """Partial charges describe the fraction of the charge density in a certain energy, band, or k-point range. @@ -70,21 +70,21 @@ class PartialCharge(_base.Refinery, _structure.Mixin): def stm_settings(self): return STM_settings() - @_base.data_access + @base.data_access def __str__(self): """Return a string representation of the partial charge density.""" return f""" - {"spin polarized" if self._spin_polarized() else ""} partial charge density of {self._topology()}: + {"spin polarized" if self._spin_polarized() else ""} partial charge density of {self._stoichiometry()}: on fine FFT grid: {self.grid()} {"summed over all contributing bands" if 0 in self.bands() else f" separated for bands: {self.bands()}"} {"summed over all contributing k-points" if 0 in self.kpoints() else f" separated for k-points: {self.kpoints()}"} """.strip() - @_base.data_access + @base.data_access def grid(self): return self._raw_data.grid[:] - @_base.data_access + @base.data_access def to_dict(self): """Store the partial charges in a dictionary. @@ -101,10 +101,10 @@ def to_dict(self): "grid": self.grid(), "bands": self.bands(), "kpoints": self.kpoints(), - "partial_charge": parchg, + "partial_density": parchg, } - @_base.data_access + @base.data_access def to_stm( self, selection: str = "constant_height", @@ -208,16 +208,16 @@ def _constant_current_stm(self, smoothed_charge, current, spin, stm_settings): scan = z_grid[np.argmax(splines(z_grid) >= current, axis=-1)] scan = z_step * (scan - scan.min()) spin_label = "both spin channels" if spin == "total" else f"spin {spin}" - topology = self._topology() - label = f"STM of {topology} for {spin_label} at constant current={current*1e9:.2f} nA" + stoichiometry = self._stoichiometry() + label = f"STM of {stoichiometry} for {spin_label} at constant current={current*1e9:.2f} nA" return Contour(data=scan, lattice=self._get_stm_plane(), label=label) def _constant_height_stm(self, smoothed_charge, tip_height, spin, stm_settings): zz = self._z_index_for_height(tip_height + self._get_highest_z_coord()) height_scan = smoothed_charge[:, :, zz] * stm_settings.enhancement_factor spin_label = "both spin channels" if spin == "total" else f"spin {spin}" - topology = self._topology() - label = f"STM of {topology} for {spin_label} at constant height={float(tip_height):.2f} Angstrom" + stoichiometry = self._stoichiometry() + label = f"STM of {stoichiometry} for {spin_label} at constant height={float(tip_height):.2f} Angstrom" return Contour(data=height_scan, lattice=self._get_stm_plane(), label=label) def _z_index_for_height(self, tip_height): @@ -242,8 +242,8 @@ def _get_lowest_z_coord(self): cart_coords = _get_sanitized_cartesian_positions(self._structure) return np.min(cart_coords[:, 2]) - def _topology(self): - return str(self._structure._topology()) + def _stoichiometry(self): + return str(self._structure._stoichiometry()) def _estimate_vacuum(self): _raise_error_if_vacuum_not_along_z(self._structure) @@ -297,7 +297,7 @@ def _out_of_plane_vector(self): def _spin_polarized(self): return self._raw_data.partial_charge.shape[2] == 2 - @_base.data_access + @base.data_access def to_numpy(self, selection="total", band=0, kpoint=0): """Return the partial charge density as a 3D array. @@ -331,7 +331,7 @@ def to_numpy(self, selection="total", band=0, kpoint=0): message = f"Spin '{selection}' not understood. Use 'up', 'down' or 'total'." raise exception.IncorrectUsage(message) - @_base.data_access + @base.data_access def bands(self): """Return the band array listing the contributing bands. @@ -355,7 +355,7 @@ def _check_band_index(self, band): Make sure to set IBAND, EINT, and LSEPB correctly in the INCAR file.""" raise exception.NoData(message) - @_base.data_access + @base.data_access def kpoints(self): """Return the k-points array listing the contributing k-points. diff --git a/src/py4vasp/calculation/_phonon.py b/src/py4vasp/_calculation/phonon.py similarity index 88% rename from src/py4vasp/calculation/_phonon.py rename to src/py4vasp/_calculation/phonon.py index c65a13bc..c1936561 100644 --- a/src/py4vasp/calculation/_phonon.py +++ b/src/py4vasp/_calculation/phonon.py @@ -1,8 +1,8 @@ # Copyright © VASP Software GmbH, # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) from py4vasp import calculation +from py4vasp._calculation import base from py4vasp._util import select -from py4vasp.calculation import _base selection_doc = """\ selection : str @@ -27,7 +27,7 @@ class Mixin: "Provide functionality common to Phonon classes." - @_base.data_access + @base.data_access def selections(self): "Return a dictionary specifying which atoms and directions can be used as selection." atoms = self._init_atom_dict().keys() @@ -36,13 +36,13 @@ def selections(self): "direction": ["x", "y", "z"], } - def _topology(self): - return calculation.topology.from_data(self._raw_data.topology) + def _stoichiometry(self): + return calculation._stoichiometry.from_data(self._raw_data.stoichiometry) def _init_atom_dict(self): return { key: value.indices - for key, value in self._topology().read().items() + for key, value in self._stoichiometry().read().items() if key != select.all } diff --git a/src/py4vasp/calculation/_phonon_band.py b/src/py4vasp/_calculation/phonon_band.py similarity index 90% rename from src/py4vasp/calculation/_phonon_band.py rename to src/py4vasp/_calculation/phonon_band.py index 0f02ce8d..284d3980 100644 --- a/src/py4vasp/calculation/_phonon_band.py +++ b/src/py4vasp/_calculation/phonon_band.py @@ -3,12 +3,12 @@ import numpy as np from py4vasp import calculation +from py4vasp._calculation import base, phonon from py4vasp._third_party import graph from py4vasp._util import convert, documentation, index, select -from py4vasp.calculation import _base, _phonon -class PhononBand(_phonon.Mixin, _base.Refinery, graph.Mixin): +class PhononBand(phonon.Mixin, base.Refinery, graph.Mixin): """The phonon band structure contains the **q**-resolved phonon eigenvalues. The phonon band structure is a graphical representation of the phonons. It @@ -26,14 +26,14 @@ class PhononBand(_phonon.Mixin, _base.Refinery, graph.Mixin): of a structural instability. """ - @_base.data_access + @base.data_access def __str__(self): return f"""phonon band data: {self._raw_data.dispersion.eigenvalues.shape[0]} q-points {self._raw_data.dispersion.eigenvalues.shape[1]} modes - {self._topology()}""" + {self._stoichiometry()}""" - @_base.data_access + @base.data_access def to_dict(self): """Read the phonon band structure into a dictionary. @@ -51,8 +51,8 @@ def to_dict(self): "modes": self._modes(), } - @_base.data_access - @documentation.format(selection=_phonon.selection_doc) + @base.data_access + @documentation.format(selection=phonon.selection_doc) def to_graph(self, selection=None, width=1.0): """Generate a graph of the phonon bands. @@ -75,7 +75,7 @@ def to_graph(self, selection=None, width=1.0): return graph def _dispersion(self): - return calculation.dispersion.from_data(self._raw_data.dispersion) + return calculation._dispersion.from_data(self._raw_data.dispersion) def _modes(self): return convert.to_complex(self._raw_data.eigenvectors[:]) diff --git a/src/py4vasp/calculation/_phonon_dos.py b/src/py4vasp/_calculation/phonon_dos.py similarity index 88% rename from src/py4vasp/calculation/_phonon_dos.py rename to src/py4vasp/_calculation/phonon_dos.py index 27f08a0a..1ddb6cca 100644 --- a/src/py4vasp/calculation/_phonon_dos.py +++ b/src/py4vasp/_calculation/phonon_dos.py @@ -1,11 +1,11 @@ # Copyright © VASP Software GmbH, # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +from py4vasp._calculation import base, phonon from py4vasp._third_party import graph from py4vasp._util import documentation, index, select -from py4vasp.calculation import _base, _phonon -class PhononDos(_phonon.Mixin, _base.Refinery, graph.Mixin): +class PhononDos(phonon.Mixin, base.Refinery, graph.Mixin): """The phonon density of states (DOS) describes the number of modes per energy. The phonon density of states (DOS) is a representation of the distribution of @@ -23,17 +23,17 @@ class PhononDos(_phonon.Mixin, _base.Refinery, graph.Mixin): with specific atomic species. """ - @_base.data_access + @base.data_access def __str__(self): energies = self._raw_data.energies - topology = self._topology() + stoichiometry = self._stoichiometry() return f"""phonon DOS: [{energies[0]:0.2f}, {energies[-1]:0.2f}] mesh with {len(energies)} points - {3 * topology.number_atoms()} modes - {topology}""" + {3 * stoichiometry.number_atoms()} modes + {stoichiometry}""" - @_base.data_access - @documentation.format(selection=_phonon.selection_doc) + @base.data_access + @documentation.format(selection=phonon.selection_doc) def to_dict(self, selection=None): """Read the phonon DOS into a dictionary. @@ -54,8 +54,8 @@ def to_dict(self, selection=None): **self._read_data(selection), } - @_base.data_access - @documentation.format(selection=_phonon.selection_doc) + @base.data_access + @documentation.format(selection=phonon.selection_doc) def to_graph(self, selection=None): """Generate a graph of the selected DOS. diff --git a/src/py4vasp/calculation/_piezoelectric_tensor.py b/src/py4vasp/_calculation/piezoelectric_tensor.py similarity index 95% rename from src/py4vasp/calculation/_piezoelectric_tensor.py rename to src/py4vasp/_calculation/piezoelectric_tensor.py index b108c8e4..1513d954 100644 --- a/src/py4vasp/calculation/_piezoelectric_tensor.py +++ b/src/py4vasp/_calculation/piezoelectric_tensor.py @@ -2,10 +2,10 @@ # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) import numpy as np -from py4vasp.calculation import _base +from py4vasp._calculation import base -class PiezoelectricTensor(_base.Refinery): +class PiezoelectricTensor(base.Refinery): """The piezoelectric tensor is the derivative of the energy with respect to strain and field. The piezoelectric tensor represents the coupling between mechanical stress and @@ -24,7 +24,7 @@ class PiezoelectricTensor(_base.Refinery): the crystal structure. """ - @_base.data_access + @base.data_access def __str__(self): data = self.to_dict() return f"""Piezoelectric tensor (C/m²) @@ -33,7 +33,7 @@ def __str__(self): {_tensor_to_string(data["clamped_ion"], "clamped-ion")} {_tensor_to_string(data["relaxed_ion"], "relaxed-ion")}""" - @_base.data_access + @base.data_access def to_dict(self): """Read the ionic and electronic contribution to the piezoelectric tensor into a dictionary. diff --git a/src/py4vasp/calculation/_polarization.py b/src/py4vasp/_calculation/polarization.py similarity index 93% rename from src/py4vasp/calculation/_polarization.py rename to src/py4vasp/_calculation/polarization.py index 57dfabfc..cd344392 100644 --- a/src/py4vasp/calculation/_polarization.py +++ b/src/py4vasp/_calculation/polarization.py @@ -1,9 +1,9 @@ # Copyright © VASP Software GmbH, # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) -from py4vasp.calculation import _base +from py4vasp._calculation import base -class Polarization(_base.Refinery): +class Polarization(base.Refinery): """The static polarization describes the electric dipole moment per unit volume. Static polarization arises in a material in response to a constant external electric @@ -18,7 +18,7 @@ class Polarization(_base.Refinery): side. Therefore you always need to compare changes of polarization. """ - @_base.data_access + @base.data_access def __str__(self): vec_to_string = lambda vec: " ".join(f"{x:11.5f}" for x in vec) return f""" @@ -28,7 +28,7 @@ def __str__(self): electronic dipole moment: {vec_to_string(self._raw_data.electron[:])} """.strip() - @_base.data_access + @base.data_access def to_dict(self): """Read electronic and ionic polarization into a dictionary diff --git a/src/py4vasp/calculation/_potential.py b/src/py4vasp/_calculation/potential.py similarity index 94% rename from src/py4vasp/calculation/_potential.py rename to src/py4vasp/_calculation/potential.py index d86e737f..e1af553c 100644 --- a/src/py4vasp/calculation/_potential.py +++ b/src/py4vasp/_calculation/potential.py @@ -5,14 +5,14 @@ import numpy as np from py4vasp import _config, calculation, exception +from py4vasp._calculation import base, structure from py4vasp._third_party import view from py4vasp._util import select -from py4vasp.calculation import _base, _structure VALID_KINDS = ("total", "ionic", "xc", "hartree") -class Potential(_base.Refinery, _structure.Mixin, view.Mixin): +class Potential(base.Refinery, structure.Mixin, view.Mixin): """The local potential describes the interactions between electrons and ions. In DFT calculations, the local potential consists of various contributions, each @@ -30,7 +30,7 @@ class Potential(_base.Refinery, _structure.Mixin, view.Mixin): average potential, you may also look at the :data:`~py4vasp.calculation.workfunction`. """ - @_base.data_access + @base.data_access def __str__(self): potential = self._raw_data.total_potential if _is_collinear(potential): @@ -39,15 +39,17 @@ def __str__(self): description = "noncollinear potential:" else: description = "nonpolarized potential:" - topology = calculation.topology.from_data(self._raw_data.structure.topology) - structure = f"structure: {topology}" + stoichiometry = calculation._stoichiometry.from_data( + self._raw_data.structure.stoichiometry + ) + structure = f"structure: {stoichiometry}" grid = f"grid: {potential.shape[3]}, {potential.shape[2]}, {potential.shape[1]}" available = "available: " + ", ".join( kind for kind in VALID_KINDS if not self._get_potential(kind).is_none() ) return "\n ".join([description, structure, grid, available]) - @_base.data_access + @base.data_access def to_dict(self): """Store all available contributions to the potential in a dictionary. @@ -78,7 +80,7 @@ def _generate_items(self, kind): elif _is_noncollinear(potential): yield f"{kind}_magnetization", potential[1:] - @_base.data_access + @base.data_access def to_view(self, selection="total", supercell=None, **user_options): """Plot an isosurface of a selected potential. diff --git a/src/py4vasp/calculation/_projector.py b/src/py4vasp/_calculation/projector.py similarity index 96% rename from src/py4vasp/calculation/_projector.py rename to src/py4vasp/_calculation/projector.py index 48f782f3..9ac8f40e 100644 --- a/src/py4vasp/calculation/_projector.py +++ b/src/py4vasp/_calculation/projector.py @@ -5,9 +5,9 @@ from typing import NamedTuple, Union from py4vasp import calculation, exception +from py4vasp._calculation import base +from py4vasp._calculation.selection import Selection from py4vasp._util import convert, documentation, index, select -from py4vasp.calculation import _base -from py4vasp.calculation._selection import Selection selection_doc = """\ selection : str @@ -66,7 +66,7 @@ def selection_examples(instance_name, function_name): _select_all = select.all -class Projector(_base.Refinery): +class Projector(base.Refinery): """The projectors used for atom and orbital resolved quantities. This is a utility class that facilitates projecting quantities such as the @@ -79,15 +79,15 @@ class Projector(_base.Refinery): _missing_data_message = "No projectors found, please verify the LORBIT tag is set." - @_base.data_access + @base.data_access def __str__(self): if self._raw_data.orbital_types.is_none(): return "no projectors" return f"""projectors: - atoms: {", ".join(self._topology().ion_types())} + atoms: {", ".join(self._stoichiometry().ion_types())} orbitals: {", ".join(self._orbital_types())}""" - @_base.data_access + @base.data_access def to_dict(self, selection=None, projections=None): """Return a map from labels to indices in the arrays produced by VASP. @@ -115,7 +115,7 @@ def to_dict(self, selection=None, projections=None): warnings.warn(message, DeprecationWarning, stacklevel=2) return self.project(selection, projections) - @_base.data_access + @base.data_access @documentation.format(selection_doc=selection_doc) def project(self, selection, projections): """Select a certain subset of the given projections and return them with a @@ -146,7 +146,7 @@ def project(self, selection, projections): for selection in self._parse_selection(selection) } - @_base.data_access + @base.data_access def selections(self): """Return a dictionary describing what options are available to specify the atom, orbital, and spin.""" @@ -190,8 +190,8 @@ def _raise_error_if_orbitals_missing(self): message = "Projectors are not available, rerun Vasp setting LORBIT >= 10." raise exception.IncorrectUsage(message) - def _topology(self): - return calculation.topology.from_data(self._raw_data.topology) + def _stoichiometry(self): + return calculation._stoichiometry.from_data(self._raw_data.stoichiometry) def _init_dicts(self): if self._raw_data.orbital_types.is_none(): @@ -204,7 +204,7 @@ def _init_dicts(self): def _init_atom_dict(self): return { key: value.indices - for key, value in self._topology().read().items() + for key, value in self._stoichiometry().read().items() if key != _select_all } @@ -261,7 +261,7 @@ class Index(NamedTuple): spin: Union[str, Selection] "Label of the spin component or a Selection object to read the corresponding data." - @_base.data_access + @base.data_access @documentation.format(separator=select.range_separator) def select( self, @@ -307,7 +307,7 @@ def select( spin=dicts["spin"][spin], ) - @_base.data_access + @base.data_access @documentation.format(selection_doc=selection_doc) def parse_selection(self, selection=_select_all): """Generate all possible indices where the projected information is stored. @@ -347,7 +347,7 @@ def _init_dicts_old(self): } def _init_atom_dict_old(self): - return self._topology().read() + return self._stoichiometry().read() def _init_orbital_dict_old(self): self._raise_error_if_orbitals_missing() diff --git a/src/py4vasp/calculation/_selection.py b/src/py4vasp/_calculation/selection.py similarity index 100% rename from src/py4vasp/calculation/_selection.py rename to src/py4vasp/_calculation/selection.py diff --git a/src/py4vasp/calculation/_slice.py b/src/py4vasp/_calculation/slice_.py similarity index 100% rename from src/py4vasp/calculation/_slice.py rename to src/py4vasp/_calculation/slice_.py diff --git a/src/py4vasp/calculation/_stress.py b/src/py4vasp/_calculation/stress.py similarity index 91% rename from src/py4vasp/calculation/_stress.py rename to src/py4vasp/_calculation/stress.py index d2071009..449cae38 100644 --- a/src/py4vasp/calculation/_stress.py +++ b/src/py4vasp/_calculation/stress.py @@ -2,12 +2,12 @@ # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) import numpy as np +from py4vasp._calculation import base, slice_, structure from py4vasp._util import documentation, reader -from py4vasp.calculation import _base, _slice, _structure -@documentation.format(examples=_slice.examples("stress")) -class Stress(_slice.Mixin, _base.Refinery, _structure.Mixin): +@documentation.format(examples=slice_.examples("stress")) +class Stress(slice_.Mixin, base.Refinery, structure.Mixin): """The stress describes the force acting on the shape of the unit cell. The stress refers to the force applied to the cell per unit area. Specifically, @@ -23,7 +23,7 @@ class Stress(_slice.Mixin, _base.Refinery, _structure.Mixin): {examples} """ - @_base.data_access + @base.data_access def __str__(self): "Convert the stress to a format similar to the OUTCAR file." step = self._last_step_in_slice @@ -38,8 +38,8 @@ def __str__(self): in kB {stress_to_string(stress)} """.strip() - @_base.data_access - @documentation.format(examples=_slice.examples("stress", "to_dict")) + @base.data_access + @documentation.format(examples=slice_.examples("stress", "to_dict")) def to_dict(self): """Read the stress and associated structural information for one or more selected steps of the trajectory. diff --git a/src/py4vasp/calculation/_structure.py b/src/py4vasp/_calculation/structure.py similarity index 88% rename from src/py4vasp/calculation/_structure.py rename to src/py4vasp/_calculation/structure.py index bb57ca36..2b3d101d 100644 --- a/src/py4vasp/calculation/_structure.py +++ b/src/py4vasp/_calculation/structure.py @@ -6,9 +6,9 @@ import numpy as np from py4vasp import calculation, exception, raw +from py4vasp._calculation import _stoichiometry, base, slice_ from py4vasp._third_party import view from py4vasp._util import documentation, import_, reader -from py4vasp.calculation import _base, _slice, _topology ase = import_.optional("ase") ase_io = import_.optional("ase.io") @@ -23,14 +23,14 @@ class _Format: end_table: str = "" newline: str = "" - def comment_line(self, topology, step_string): - return f"{topology}{step_string}{self.newline}" + def comment_line(self, stoichiometry, step_string): + return f"{stoichiometry}{step_string}{self.newline}" def scaling_factor(self, scale): return f"{self._element_to_string(scale)}{self.newline}".lstrip() - def ion_list(self, topology): - return f"{topology.to_POSCAR(self.newline)}{self.newline}" + def ion_list(self, stoichiometry): + return f"{stoichiometry.to_POSCAR(self.newline)}{self.newline}" def coordinate_system(self): return f"Direct{self.newline}" @@ -47,8 +47,8 @@ def _element_to_string(self, element): return f"{element:21.16f}" -@documentation.format(examples=_slice.examples("structure")) -class Structure(_slice.Mixin, _base.Refinery, view.Mixin): +@documentation.format(examples=slice_.examples("structure")) +class Structure(slice_.Mixin, base.Refinery, view.Mixin): """The structure contains the unit cell and the position of all ions within. The crystal structure is the specific arrangement of ions in a three-dimensional @@ -95,18 +95,18 @@ def from_POSCAR(cls, poscar, *, elements=None): def from_ase(cls, structure): """Generate a structure from the ase Atoms class.""" structure = raw.Structure( - topology=_topology.raw_topology_from_ase(structure), + stoichiometry=_stoichiometry.raw_stoichiometry_from_ase(structure), cell=_cell_from_ase(structure), positions=structure.get_scaled_positions()[np.newaxis], ) return cls.from_data(structure) - @_base.data_access + @base.data_access def __str__(self): "Generate a string representing the final structure usable as a POSCAR file." return self._create_repr() - @_base.data_access + @base.data_access def _repr_html_(self): format_ = _Format( begin_table="\n
", @@ -120,17 +120,17 @@ def _repr_html_(self): def _create_repr(self, format_=_Format()): step = self._get_last_step() lines = ( - format_.comment_line(self._topology(), self._step_string()), + format_.comment_line(self._stoichiometry(), self._step_string()), format_.scaling_factor(self._scale()), format_.vectors_to_table(self._raw_data.cell.lattice_vectors[step]), - format_.ion_list(self._topology()), + format_.ion_list(self._stoichiometry()), format_.coordinate_system(), format_.vectors_to_table(self._raw_data.positions[step]), ) return "\n".join(lines) - @_base.data_access - @documentation.format(examples=_slice.examples("structure", "to_dict")) + @base.data_access + @documentation.format(examples=slice_.examples("structure", "to_dict")) def to_dict(self): """Read the structural information into a dictionary. @@ -146,12 +146,12 @@ def to_dict(self): return { "lattice_vectors": self.lattice_vectors(), "positions": self.positions(), - "elements": self._topology().elements(), - "names": self._topology().names(), + "elements": self._stoichiometry().elements(), + "names": self._stoichiometry().names(), } - @_base.data_access - @documentation.format(examples=_slice.examples("structure", "to_view")) + @base.data_access + @documentation.format(examples=slice_.examples("structure", "to_view")) def to_view(self, supercell=None): """Generate a 3d representation of the structure(s). @@ -170,7 +170,7 @@ def to_view(self, supercell=None): """ make_3d = lambda array: array if array.ndim == 3 else array[np.newaxis] positions = make_3d(self.positions()) - elements = np.tile(self._topology().elements(), (len(positions), 1)) + elements = np.tile(self._stoichiometry().elements(), (len(positions), 1)) return view.View( elements=elements, lattice_vectors=make_3d(self.lattice_vectors()), @@ -178,8 +178,8 @@ def to_view(self, supercell=None): supercell=self._parse_supercell(supercell), ) - @_base.data_access - @documentation.format(examples=_slice.examples("structure", "to_ase")) + @base.data_access + @documentation.format(examples=slice_.examples("structure", "to_ase")) def to_ase(self, supercell=None): """Convert the structure to an ase Atoms object. @@ -222,8 +222,8 @@ def to_ase(self, supercell=None): order = sorted(range(num_atoms_super), key=lambda n: n % num_atoms_prim) return structure[order] - @_base.data_access - @documentation.format(examples=_slice.examples("structure", "to_mdtraj")) + @base.data_access + @documentation.format(examples=slice_.examples("structure", "to_mdtraj")) def to_mdtraj(self): """Convert the trajectory to mdtraj.Trajectory @@ -241,12 +241,12 @@ def to_mdtraj(self): raise exception.NotImplemented(message) data = self.to_dict() xyz = data["positions"] @ data["lattice_vectors"] * self.A_to_nm - trajectory = mdtraj.Trajectory(xyz, self._topology().to_mdtraj()) + trajectory = mdtraj.Trajectory(xyz, self._stoichiometry().to_mdtraj()) trajectory.unitcell_vectors = data["lattice_vectors"] * Structure.A_to_nm return trajectory - @_base.data_access - @documentation.format(examples=_slice.examples("structure", "to_POSCAR")) + @base.data_access + @documentation.format(examples=slice_.examples("structure", "to_POSCAR")) def to_POSCAR(self): """Convert the structure(s) to a POSCAR format @@ -263,7 +263,7 @@ def to_POSCAR(self): message = "Converting multiple structures to a POSCAR is currently not implemented." raise exception.NotImplemented(message) - @_base.data_access + @base.data_access def lattice_vectors(self): """Return the lattice vectors spanning the unit cell @@ -275,7 +275,7 @@ def lattice_vectors(self): lattice_vectors = _LatticeVectors(self._raw_data.cell.lattice_vectors) return self._scale() * lattice_vectors[self._get_steps()] - @_base.data_access + @base.data_access def positions(self): """Return the direct coordinates of all ions in the unit cell. @@ -289,8 +289,8 @@ def positions(self): """ return self._raw_data.positions[self._get_steps()] - @_base.data_access - @documentation.format(examples=_slice.examples("structure", "cartesian_positions")) + @base.data_access + @documentation.format(examples=slice_.examples("structure", "cartesian_positions")) def cartesian_positions(self): """Convert the positions from direct coordinates to cartesian ones. @@ -303,8 +303,8 @@ def cartesian_positions(self): """ return self.positions() @ self.lattice_vectors() - @_base.data_access - @documentation.format(examples=_slice.examples("structure", "volume")) + @base.data_access + @documentation.format(examples=slice_.examples("structure", "volume")) def volume(self): """Return the volume of the unit cell for the selected steps. @@ -317,7 +317,7 @@ def volume(self): """ return np.abs(np.linalg.det(self.lattice_vectors())) - @_base.data_access + @base.data_access def number_atoms(self): """Return the total number of atoms in the structure.""" if self._is_trajectory: @@ -325,7 +325,7 @@ def number_atoms(self): else: return self._raw_data.positions.shape[0] - @_base.data_access + @base.data_access def number_steps(self): """Return the number of structures in the trajectory.""" if self._is_trajectory: @@ -356,8 +356,8 @@ def _parse_supercell(self, supercell): ) raise exception.IncorrectUsage(message) - def _topology(self): - return calculation.topology.from_data(self._raw_data.topology) + def _stoichiometry(self): + return calculation._stoichiometry.from_data(self._raw_data.stoichiometry) def _scale(self): if isinstance(self._raw_data.cell.scale, np.float64): @@ -382,7 +382,7 @@ def _step_string(self): else: return f" (step {self._steps + 1})" - @_base.data_access + @base.data_access def __getitem__(self, steps): if not self._is_trajectory: message = "The structure is not a Trajectory so accessing individual elements is not allowed." diff --git a/src/py4vasp/calculation/_system.py b/src/py4vasp/_calculation/system.py similarity index 79% rename from src/py4vasp/calculation/_system.py rename to src/py4vasp/_calculation/system.py index 367ee8e6..70e4af04 100644 --- a/src/py4vasp/calculation/_system.py +++ b/src/py4vasp/_calculation/system.py @@ -1,17 +1,17 @@ # Copyright © VASP Software GmbH, # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +from py4vasp._calculation import base from py4vasp._util import convert -from py4vasp.calculation import _base -class System(_base.Refinery): +class System(base.Refinery): "The :tag:`SYSTEM` tag in the INCAR file is a title you choose for a VASP calculation." - @_base.data_access + @base.data_access def __str__(self): return convert.text_to_string(self._raw_data.system) - @_base.data_access + @base.data_access def to_dict(self): "Returns a dictionary containing the system tag." return {"system": str(self)} diff --git a/src/py4vasp/calculation/_velocity.py b/src/py4vasp/_calculation/velocity.py similarity index 89% rename from src/py4vasp/calculation/_velocity.py rename to src/py4vasp/_calculation/velocity.py index 11945d98..78b9f992 100644 --- a/src/py4vasp/calculation/_velocity.py +++ b/src/py4vasp/_calculation/velocity.py @@ -3,13 +3,13 @@ import numpy as np from py4vasp import _config, exception +from py4vasp._calculation import base, slice_, structure from py4vasp._third_party import view from py4vasp._util import documentation, reader -from py4vasp.calculation import _base, _slice, _structure -@documentation.format(examples=_slice.examples("velocity")) -class Velocity(_slice.Mixin, _base.Refinery, _structure.Mixin, view.Mixin): +@documentation.format(examples=slice_.examples("velocity")) +class Velocity(slice_.Mixin, base.Refinery, structure.Mixin, view.Mixin): """The velocities describe the ionic motion during an MD simulation. The velocities of the ions are a metric for the temperature of the system. Most @@ -27,7 +27,7 @@ class Velocity(_slice.Mixin, _base.Refinery, _structure.Mixin, view.Mixin): velocity_rescale = 200 - @_base.data_access + @base.data_access def __str__(self): step = self._last_step_in_slice velocities = self._vectors_to_string(self._velocity[step]) @@ -42,8 +42,8 @@ def _vector_to_string(self, vector): def _element_to_string(self, element): return f"{element:21.16f}" - @_base.data_access - @documentation.format(examples=_slice.examples("velocity", "to_dict")) + @base.data_access + @documentation.format(examples=slice_.examples("velocity", "to_dict")) def to_dict(self): """Return the structure and ion velocities in a dictionary @@ -60,8 +60,8 @@ def to_dict(self): "velocities": self._velocity[self._steps], } - @_base.data_access - @documentation.format(examples=_slice.examples("velocity", "to_view")) + @base.data_access + @documentation.format(examples=slice_.examples("velocity", "to_view")) def to_view(self, supercell=None): """Plot the velocities as vectors in the structure. diff --git a/src/py4vasp/calculation/_workfunction.py b/src/py4vasp/_calculation/workfunction.py similarity index 82% rename from src/py4vasp/calculation/_workfunction.py rename to src/py4vasp/_calculation/workfunction.py index d6387919..3124845a 100644 --- a/src/py4vasp/calculation/_workfunction.py +++ b/src/py4vasp/_calculation/workfunction.py @@ -1,11 +1,11 @@ # Copyright © VASP Software GmbH, # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) from py4vasp import calculation +from py4vasp._calculation import base from py4vasp._third_party import graph -from py4vasp.calculation import _base -class Workfunction(_base.Refinery, graph.Mixin): +class Workfunction(base.Refinery, graph.Mixin): """The workfunction describes the energy required to remove an electron to the vacuum. The workfunction of a material is the minimum energy required to remove an @@ -18,17 +18,16 @@ class Workfunction(_base.Refinery, graph.Mixin): resulting potential. """ - @_base.data_access + @base.data_access def __str__(self): data = self.to_dict() return f"""workfunction along {data["direction"]}: vacuum potential: {data["vacuum_potential"][0]:.3f} {data["vacuum_potential"][1]:.3f} - Fermi energy: {data["fermi_energy"]:.3f}""" + Fermi energy: {data["fermi_energy"]:.3f} + valence band maximum: {data["valence_band_maximum"]:.3f} + conduction band minimum: {data["conduction_band_minimum"]:.3f}""" - # valence band maximum: {data["valence_band_maximum"]:.3f} - # conduction band minimum: {data["conduction_band_minimum"]:.3f} - - @_base.data_access + @base.data_access def to_dict(self): """Reports useful information about the workfunction as a dictionary. @@ -44,19 +43,17 @@ def to_dict(self): within the surface. """ bandgap = calculation.bandgap.from_data(self._raw_data.reference_potential) - # vbm and cbm will be uncommented out when the relevant parts of the - # code are added to VASP 6.5 return { "direction": f"lattice vector {self._raw_data.idipol}", "distance": self._raw_data.distance[:], "average_potential": self._raw_data.average_potential[:], "vacuum_potential": self._raw_data.vacuum_potential[:], - # "valence_band_maximum": bandgap.valence_band_maximum(), - # "conduction_band_minimum": bandgap.conduction_band_minimum(), + "valence_band_maximum": bandgap.valence_band_maximum(), + "conduction_band_minimum": bandgap.conduction_band_minimum(), "fermi_energy": self._raw_data.fermi_energy, } - @_base.data_access + @base.data_access def to_graph(self): """Plot the average potential along the lattice vector selected by IDIPOL. diff --git a/src/py4vasp/_combine/base.py b/src/py4vasp/_combine/base.py index 3672a4b5..cabba39a 100644 --- a/src/py4vasp/_combine/base.py +++ b/src/py4vasp/_combine/base.py @@ -4,7 +4,8 @@ import pathlib from typing import Dict, List -from py4vasp import calculation, exception +import py4vasp +from py4vasp import exception def _match_combine_with_refinement(combine_name: str): @@ -13,7 +14,7 @@ def _match_combine_with_refinement(combine_name: str): "Forces": "force", "Stresses": "stress", } - return getattr(calculation, combine_to_refinement_name[combine_name]) + return getattr(py4vasp.calculation, combine_to_refinement_name[combine_name]) # for _, class_ in inspect.getmembers(data_depr, inspect.isclass): # if class_.__name__ == combine_to_refinement_name[combine_name]: # return class_ diff --git a/src/py4vasp/_control/poscar.py b/src/py4vasp/_control/poscar.py index 3816925b..c0ccfdab 100644 --- a/src/py4vasp/_control/poscar.py +++ b/src/py4vasp/_control/poscar.py @@ -1,6 +1,6 @@ # Copyright © VASP Software GmbH, # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) -from py4vasp import calculation +import py4vasp from py4vasp._control import base from py4vasp._third_party import view @@ -36,5 +36,5 @@ def to_view(self, supercell=None, *, elements=None): View Visualize the structure as a 3d figure. """ - structure = calculation.structure.from_POSCAR(self, elements=elements) + structure = py4vasp.calculation.structure.from_POSCAR(self, elements=elements) return structure.plot(supercell) diff --git a/src/py4vasp/_raw/data.py b/src/py4vasp/_raw/data.py index eeee17c5..51ab8fa9 100644 --- a/src/py4vasp/_raw/data.py +++ b/src/py4vasp/_raw/data.py @@ -90,7 +90,7 @@ class Cell: class CONTCAR: """The data corresponding to the CONTCAR file. - The CONTCAR file contains structural information (lattice, positions, topology), + The CONTCAR file contains structural information (lattice, positions, stoichiometry), relaxation constraints, and data relevant for continuation calculations. """ @@ -200,6 +200,22 @@ class ElasticModulus: "Elastic modulus when the position of the ions is relaxed." +@dataclasses.dataclass +class ElectronicMinimization: + """The OSZICAR data as generated by VASP. + + All data generated by VASP and traditionally stored in the OSZICAR file will be + stored here. See https://www.vasp.at/wiki/index.php/OSZICAR for more details about + what quantities to expect.""" + + convergence_data: VaspData + "All columns of the OSZICAR file stored for all ionic steps." + label: VaspData + "Label of all the data from the OSZICAR file." + is_elmin_converged: VaspData + "Is the electronic minimization step converged?" + + @dataclasses.dataclass class Energy: """Various energies during ionic relaxation or MD simulation. @@ -312,22 +328,6 @@ class Magnetism: "Contains the orbital magnetization for all atoms" -@dataclasses.dataclass -class ElectronicMinimization: - """The OSZICAR data as generated by VASP. - - All data generated by VASP and traditionally stored in the OSZICAR file will be - stored here. See https://www.vasp.at/wiki/index.php/OSZICAR for more details about - what quantities to expect.""" - - convergence_data: VaspData - "All columns of the OSZICAR file stored for all ionic steps." - label: VaspData - "Label of all the data from the OSZICAR file." - is_elmin_converged: VaspData - "Is the electronic minimization step converged?" - - @dataclasses.dataclass class PairCorrelation: """The pair-correlation function calculated during a MD simulation. @@ -344,7 +344,7 @@ class PairCorrelation: @dataclasses.dataclass -class PartialCharge: +class PartialDensity: """Electronic partial charge and magnetization density on the fine Fourier grid Possibly not only split by spin, but also by band and kpoint.""" @@ -366,11 +366,11 @@ class PhononBand: """The band structure of the phonons. Contains the eigenvalues and eigenvectors at specifics **q** points in the Brillouin - zone. Includes the topology to map atoms onto specific modes.""" + zone. Includes the stoichiometry to map atoms onto specific modes.""" dispersion: Dispersion "The **q** points and the eigenvalues." - topology: Topology + stoichiometry: Stoichiometry "The atom types in the crystal." eigenvectors: VaspData "The eigenvectors of the phonon modes." @@ -389,7 +389,7 @@ class PhononDos: "Dos at the energies D(E)." projections: VaspData "Projection of the DOS onto contribution of specific atoms." - topology: Topology + stoichiometry: Stoichiometry "The atom types in the crystal." @@ -451,14 +451,23 @@ class Projector: and orbitals. This class reports the atoms and orbitals included in the projection. """ - topology: Topology - "The topology of the system used, i.e., which elements are contained." + stoichiometry: Stoichiometry + "The stoichiometry of the system used, i.e., which elements are contained." orbital_types: VaspData "Character indicating the orbital angular momentum." number_spins: int "Indicates whether the calculation is spin polarized or not." +@dataclasses.dataclass +class Stoichiometry: + "Contains the type of ions in the system and how many of each type exist." + number_ion_types: VaspData + "Amount of ions of a particular type." + ion_types: VaspData + "Element of a particular type." + + @dataclasses.dataclass class Stress: "The stress acting on the unit cell at all steps." @@ -475,8 +484,8 @@ class Structure: Reports what ions are in the system and the positions of all ions as well as the unit cell for all steps in a relaxation in a MD run.""" - topology: Topology - "The topology of the system used, i.e., which elements are contained." + stoichiometry: Stoichiometry + "The stoichiometry of the system used, i.e., which elements are contained." cell: Cell "Unit cell of the crystal or simulation cell for molecules." positions: VaspData @@ -489,15 +498,6 @@ class System: system: str -@dataclasses.dataclass -class Topology: - "Contains the type of ions in the system and how many of each type exist." - number_ion_types: VaspData - "Amount of ions of a particular type." - ion_types: VaspData - "Element of a particular type." - - @dataclasses.dataclass class Velocity: "Contains the ion velocities along the trajectory." diff --git a/src/py4vasp/_raw/definition.py b/src/py4vasp/_raw/definition.py index 1e82e326..69858485 100644 --- a/src/py4vasp/_raw/definition.py +++ b/src/py4vasp/_raw/definition.py @@ -285,6 +285,14 @@ def selections(quantity): relaxed_ion=f"{group}/relaxed_ion_elastic_modulus", ) # +schema.add( + raw.ElectronicMinimization, + required=raw.Version(6, 5), + label="intermediate/ion_dynamics/oszicar_label", + convergence_data="intermediate/ion_dynamics/oszicar", + is_elmin_converged="/intermediate/ion_dynamics/electronic_step_converged", +) +# group = "results/linear_response" schema.add( raw.Fatband, @@ -378,14 +386,6 @@ def selections(quantity): orbital_moments="intermediate/ion_dynamics/magnetism/orbital_moments/values", ) # -schema.add( - raw.ElectronicMinimization, - required=raw.Version(6, 5), - label="intermediate/ion_dynamics/oszicar_label", - convergence_data="intermediate/ion_dynamics/oszicar", - is_elmin_converged="/intermediate/ion_dynamics/electronic_step_converged", -) -# group = "intermediate/pair_correlation" schema.add( raw.PairCorrelation, @@ -396,7 +396,7 @@ def selections(quantity): ) # schema.add( - raw.PartialCharge, + raw.PartialDensity, required=raw.Version(6, 5), structure=Link("structure", DEFAULT_SOURCE), partial_charge="results/partial_charges/parchg", @@ -410,7 +410,7 @@ def selections(quantity): raw.PhononBand, required=raw.Version(6, 4), dispersion=Link("dispersion", "phonon"), - topology=Link("topology", "phonon"), + stoichiometry=Link("stoichiometry", "phonon"), eigenvectors=f"{group}/eigenvectors", ) schema.add( @@ -418,7 +418,7 @@ def selections(quantity): required=raw.Version(6, 4), energies=f"{group}/dos_mesh", dos=f"{group}/dos", - topology=Link("topology", "phonon"), + stoichiometry=Link("stoichiometry", "phonon"), projections=f"{group}/dospar", ) # @@ -450,25 +450,38 @@ def selections(quantity): # schema.add( raw.Projector, - topology=Link("topology", DEFAULT_SOURCE), + stoichiometry=Link("stoichiometry", DEFAULT_SOURCE), orbital_types="results/projectors/lchar", number_spins=Length("results/electron_eigenvalues/eigenvalues"), ) schema.add( raw.Projector, name="kpoints_opt", - topology=Link("topology", DEFAULT_SOURCE), + stoichiometry=Link("stoichiometry", DEFAULT_SOURCE), orbital_types="results/projectors_kpoints_opt/lchar", number_spins=Length("results/electron_eigenvalues/eigenvalues"), ) schema.add( raw.Projector, name="kpoints_wan", - topology=Link("topology", DEFAULT_SOURCE), + stoichiometry=Link("stoichiometry", DEFAULT_SOURCE), orbital_types="results/projectors_kpoints_wan/lchar", number_spins=Length("results/electron_eigenvalues/eigenvalues"), ) # +schema.add( + raw.Stoichiometry, + ion_types="results/positions/ion_types", + number_ion_types="results/positions/number_ion_types", +) +schema.add( + raw.Stoichiometry, + name="phonon", + required=raw.Version(6, 4), + ion_types="results/phonons/primitive/ion_types", + number_ion_types="results/phonons/primitive/number_ion_types", +) +# schema.add( raw.Stress, structure=Link("structure", DEFAULT_SOURCE), @@ -477,7 +490,7 @@ def selections(quantity): # schema.add( raw.Structure, - topology=Link("topology", DEFAULT_SOURCE), + stoichiometry=Link("stoichiometry", DEFAULT_SOURCE), cell=Link("cell", DEFAULT_SOURCE), positions="intermediate/ion_dynamics/position_ions", ) @@ -485,26 +498,13 @@ def selections(quantity): raw.Structure, name="final", required=raw.Version(6, 5), - topology=Link("topology", DEFAULT_SOURCE), + stoichiometry=Link("stoichiometry", DEFAULT_SOURCE), cell=Link("cell", "final"), positions="results/positions/position_ions", ) # schema.add(raw.System, system="input/incar/SYSTEM") # -schema.add( - raw.Topology, - ion_types="results/positions/ion_types", - number_ion_types="results/positions/number_ion_types", -) -schema.add( - raw.Topology, - name="phonon", - required=raw.Version(6, 4), - ion_types="results/phonons/primitive/ion_types", - number_ion_types="results/phonons/primitive/number_ion_types", -) -# schema.add( raw.Velocity, required=raw.Version(6, 4), diff --git a/src/py4vasp/_util/convert.py b/src/py4vasp/_util/convert.py index f54bb95a..e1253cd0 100644 --- a/src/py4vasp/_util/convert.py +++ b/src/py4vasp/_util/convert.py @@ -46,11 +46,10 @@ def _to_snakecase(word: str) -> str: return word.lower() -# NOTE: to_camelcase is the function camelize from the inflection package +# NOTE: to_camelcase is based on the function camelize from the inflection package # (Copyright (C) 2012-2020 Janne Vanhala) def to_camelcase(string: str, uppercase_first_letter: bool = True) -> str: - """ - Convert strings to CamelCase. + """Convert strings to CamelCase. Examples:: @@ -70,7 +69,7 @@ def to_camelcase(string: str, uppercase_first_letter: bool = True) -> str: lowerCamelCase. Defaults to `True`. """ if uppercase_first_letter: - return re.sub(r"(?:^|_)(.)", lambda m: m.group(1).upper(), string) + return re.sub(r"(?:_|^)(.)", lambda m: m.group(1).upper(), string) else: return string[0].lower() + camelize(string)[1:] diff --git a/src/py4vasp/_util/parser.py b/src/py4vasp/_util/parser.py index 586e9ce9..76503ce5 100644 --- a/src/py4vasp/_util/parser.py +++ b/src/py4vasp/_util/parser.py @@ -3,7 +3,7 @@ import numpy as np -from py4vasp._raw.data import CONTCAR, Cell, Structure, Topology +from py4vasp._raw.data import CONTCAR, Cell, Stoichiometry, Structure from py4vasp._raw.data_wrapper import VaspData from py4vasp.exception import ParserError @@ -114,10 +114,10 @@ def has_selective_dynamics(self): return False @property - def topology(self): - """The topology from the POSCAR file. + def stoichiometry(self): + """The stoichiometry from the POSCAR file. - Parses the topology from the POSCAR file. The topology is parsed as is + Parses the stoichiometry from the POSCAR file. The stoichiometry is parsed as is and the species names are reported in the Topology object. If the species names are not specified in the POSCAR file, then the species names must be supplied as an argument. @@ -134,8 +134,7 @@ def topology(self): number_of_species = self.split_poscar[5].split() number_of_species = VaspData(np.array(number_of_species, dtype=int)) species_name = VaspData(np.array(species_name)) - topology = Topology(number_ion_types=number_of_species, ion_types=species_name) - return topology + return Stoichiometry(number_ion_types=number_of_species, ion_types=species_name) @property def ion_positions_and_selective_dynamics(self): @@ -147,7 +146,7 @@ def ion_positions_and_selective_dynamics(self): specified in Cartesian coordinates, then the positions are converted to direct coordinates. """ - number_of_species = self.topology.number_ion_types.data.sum() + number_of_species = self.stoichiometry.number_ion_types.data.sum() idx_start = 6 if self.has_selective_dynamics: idx_start += 1 @@ -196,7 +195,7 @@ def has_lattice_velocities(self): is 'Lattice velocities and vectors', then it is assumed that the POSCAR file has lattice velocities. """ - num_species = self.topology.number_ion_types.data.sum() + num_species = self.stoichiometry.number_ion_types.data.sum() idx_start = 7 + num_species if self.has_selective_dynamics: idx_start += 1 @@ -219,7 +218,7 @@ def lattice_velocities(self): If the velocities are specified in Direct coordinates, then the velocities are converted to Cartesian coordinates. """ - num_species = self.topology.number_ion_types.data.sum() + num_species = self.stoichiometry.number_ion_types.data.sum() idx_start = 7 + num_species if not self.has_lattice_velocities: raise ParserError("No lattice velocities found in POSCAR.") @@ -251,7 +250,7 @@ def has_ion_velocities(self): (assumed to be Cartesian). If the header is not one of these, then it is assumed that the POSCAR file does not have ion velocities. """ - num_species = self.topology.number_ion_types.data.sum() + num_species = self.stoichiometry.number_ion_types.data.sum() idx_start = 7 + num_species if self.has_selective_dynamics: idx_start += 1 @@ -276,7 +275,7 @@ def ion_velocities(self): If the velocities are specified in Direct coordinates, then the velocities are converted to Cartesian coordinates. """ - num_species = self.topology.number_ion_types.data.sum() + num_species = self.stoichiometry.number_ion_types.data.sum() if not self.has_ion_velocities: raise ParserError("No ion velocities found in POSCAR.") idx_start = 7 + num_species @@ -307,7 +306,7 @@ def to_contcar(self): """ ion_positions, selective_dynamics = self.ion_positions_and_selective_dynamics structure = Structure( - topology=self.topology, + stoichiometry=self.stoichiometry, cell=self.cell, positions=ion_positions, ) diff --git a/src/py4vasp/calculation/__init__.py b/src/py4vasp/calculation/__init__.py deleted file mode 100644 index 17a4ae34..00000000 --- a/src/py4vasp/calculation/__init__.py +++ /dev/null @@ -1,98 +0,0 @@ -# Copyright © VASP Software GmbH, -# Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) -"""Provide refinement functions for a the raw data of a VASP calculation run in the -current directory. - -Usually one is not directly interested in the raw data that is produced but -wants to produce either a figure for a publication or some post-processing of -the data. This package contains multiple modules that enable these kinds of -workflows by extracting the relevant data from the HDF5 file and transforming -them into an accessible format. The modules also provide plotting functionality -to get a quick insight about the data, which can then be refined either within -python or a different tool to obtain publication-quality figures. - -Generally, all modules provide a `read` function that extracts the data from the -HDF5 file and puts it into a Python dictionary. Where it makes sense in addition -a `plot` function is available that converts the data into a figure for Jupyter -notebooks. In addition, data conversion routines `to_X` may be available -transforming the data into another format or file, which may be useful to -generate plots with tools other than Python. For the specifics, please refer to -the documentation of the individual modules. - -The raw data is read from the current directory. The :class:`~py4vasp.Calculation` -class provides a more flexible interface with which you can determine the source -directory or file for the VASP calculation manually. That class exposes functions -of the modules as methods of attributes, i.e., the two following examples are -equivalent: - -.. rubric:: using :mod:`~py4vasp.calculation` module - ->>> from py4vasp import calculation ->>> calculation.dos.read() - -.. rubric:: using :class:`~py4vasp.Calculation` class - ->>> from py4vasp import Calculation ->>> calc = Calculation.from_path(".") ->>> calc.dos.read() - -In the latter example, you can change the path from which the data is extracted. -""" -import importlib -import pathlib - -from py4vasp import control, exception -from py4vasp._util import convert - -_input_files = ("INCAR", "KPOINTS", "POSCAR") -_quantities = ( - "band", - "bandgap", - "born_effective_charge", - "CONTCAR", - "density", - "dielectric_function", - "dielectric_tensor", - "dos", - "elastic_modulus", - "energy", - "fatband", - "force", - "force_constant", - "internal_strain", - "kpoint", - "magnetism", - "electronic_minimization", - "pair_correlation", - "partial_charge", - "phonon_band", - "phonon_dos", - "piezoelectric_tensor", - "polarization", - "potential", - "projector", - "stress", - "structure", - "system", - "topology", - "velocity", - "workfunction", -) -_private = ("dispersion",) -__all__ = _quantities # + _input_files - - -path = pathlib.Path(".") - - -def __getattr__(attr): - if attr in (_quantities + _private): - module = importlib.import_module(f"py4vasp.calculation._{attr}") - class_ = getattr(module, convert.to_camelcase(attr)) - return class_.from_path(".") - # elif attr in (_input_files): - # class_ = getattr(control, attr) - # return class_(".") - else: - message = f"Could not find {attr} in the possible attributes, please check the spelling" - raise exception.MissingAttribute(message) diff --git a/src/py4vasp/exception.py b/src/py4vasp/exception.py index d0800864..437d5ae6 100644 --- a/src/py4vasp/exception.py +++ b/src/py4vasp/exception.py @@ -36,11 +36,6 @@ class OutdatedVaspVersion(Py4VaspError): used version of Vasp.""" -class MissingAttribute(Py4VaspError, AttributeError): - """Exception raised when py4vasp attribute of Calculation, Batch, ... is used - that does not exist""" - - class ModuleNotInstalled(Py4VaspError): """Exception raised when a functionality is used that relies on an optional dependency of py4vasp but that dependency is not installed.""" diff --git a/tests/analysis/test_mlff.py b/tests/analysis/test_mlff.py index 6439de0f..9b5d9aec 100644 --- a/tests/analysis/test_mlff.py +++ b/tests/analysis/test_mlff.py @@ -127,7 +127,7 @@ def mock_calculations_incorrect(raw_data): return _mock_calculations -@patch("py4vasp.calculation._base.Refinery.from_path", autospec=True) +@patch("py4vasp._calculation.base.Refinery.from_path", autospec=True) @patch("py4vasp.raw.access", autospec=True) def test_read_inputs_from_path(mock_access, mock_from_path): absolute_path_dft = Path(__file__) / "dft" @@ -151,7 +151,7 @@ def test_read_inputs_from_path(mock_access, mock_from_path): assert isinstance(error_analysis.dft.stresses, np.ndarray) -@patch("py4vasp.calculation._base.Refinery.from_path", autospec=True) +@patch("py4vasp._calculation.base.Refinery.from_path", autospec=True) @patch("py4vasp.raw.access", autospec=True) def test_read_inputs_from_files(mock_analysis, mock_from_path): absolute_files_dft = Path(__file__) / "dft*.h5" @@ -180,9 +180,9 @@ def test_read_from_data(mock_calculations): expected_forces = mock_calculations.forces.read() expected_stresses = mock_calculations.stresses.read() mlff_error_analysis = MLFFErrorAnalysis._from_data(mock_calculations) - output_energies = mlff_error_analysis._calculations.energies.read() - output_forces = mlff_error_analysis._calculations.forces.read() - output_stresses = mlff_error_analysis._calculations.stresses.read() + output_energies = mlff_error_analysis._batch.energies.read() + output_forces = mlff_error_analysis._batch.forces.read() + output_stresses = mlff_error_analysis._batch.stresses.read() assert output_energies == expected_energies assert output_forces == expected_forces assert output_stresses == expected_stresses diff --git a/tests/test_calculations.py b/tests/batch/test_batch.py similarity index 71% rename from tests/test_calculations.py rename to tests/batch/test_batch.py index 6bfd5af0..aac2a39b 100644 --- a/tests/test_calculations.py +++ b/tests/batch/test_batch.py @@ -6,54 +6,50 @@ import pytest -from py4vasp import Calculations +from py4vasp import Batch def test_error_when_using_constructor(): with pytest.raises(Exception): - Calculations() + Batch() def test_creation_from_paths(): # Test creation from absolute paths absolute_path_1 = Path(__file__) / "path_1" absolute_path_2 = Path(__file__) / "path_2" - calculations = Calculations.from_paths( - path_name_1=absolute_path_1, path_name_2=absolute_path_2 - ) - output_paths = calculations.paths() + batch = Batch.from_paths(path_name_1=absolute_path_1, path_name_2=absolute_path_2) + output_paths = batch.paths() assert output_paths["path_name_1"] == [absolute_path_1] assert output_paths["path_name_2"] == [absolute_path_2] - output_number_of_calculations = calculations.number_of_calculations() + output_number_of_calculations = batch.number_of_calculations() assert output_number_of_calculations["path_name_1"] == 1 assert output_number_of_calculations["path_name_2"] == 1 # Test creation from relative paths relative_path_1 = os.path.relpath(absolute_path_1, Path.cwd()) relative_path_2 = os.path.relpath(absolute_path_2, Path.cwd()) - calculations = Calculations.from_paths( - path_name_1=relative_path_1, path_name_2=relative_path_2 - ) - output_paths = calculations.paths() + batch = Batch.from_paths(path_name_1=relative_path_1, path_name_2=relative_path_2) + output_paths = batch.paths() assert output_paths["path_name_1"] == [absolute_path_1] assert output_paths["path_name_2"] == [absolute_path_2] - output_number_of_calculations = calculations.number_of_calculations() + output_number_of_calculations = batch.number_of_calculations() assert output_number_of_calculations["path_name_1"] == 1 assert output_number_of_calculations["path_name_2"] == 1 # Test creation with string paths - calculations = Calculations.from_paths( + batch = Batch.from_paths( path_name_1=absolute_path_1.as_posix(), path_name_2=absolute_path_2.as_posix() ) - output_paths = calculations.paths() + output_paths = batch.paths() assert output_paths["path_name_1"] == [absolute_path_1] assert output_paths["path_name_2"] == [absolute_path_2] - output_number_of_calculations = calculations.number_of_calculations() + output_number_of_calculations = batch.number_of_calculations() assert output_number_of_calculations["path_name_1"] == 1 assert output_number_of_calculations["path_name_2"] == 1 def test_creation_from_paths_with_incorrect_input(): with pytest.raises(Exception): - Calculations.from_paths(path_name_1=1, path_name_2=2) + Batch.from_paths(path_name_1=1, path_name_2=2) def test_creation_from_paths_with_wildcards(tmp_path): @@ -64,10 +60,10 @@ def test_creation_from_paths_with_wildcards(tmp_path): create_paths = lambda paths: [path.mkdir() for path in paths] create_paths(paths_1) create_paths(paths_2) - calculations = Calculations.from_paths( + batch = Batch.from_paths( path_name_1=tmp_path / "path1_*", path_name_2=tmp_path / "path2_*" ) - output_paths = calculations.paths() + output_paths = batch.paths() assert all( [ output_paths["path_name_1"][i] == absolute_paths_1[i] @@ -80,7 +76,7 @@ def test_creation_from_paths_with_wildcards(tmp_path): for i in range(len(absolute_paths_2)) ] ) - output_number_of_calculations = calculations.number_of_calculations() + output_number_of_calculations = batch.number_of_calculations() assert output_number_of_calculations["path_name_1"] == 2 assert output_number_of_calculations["path_name_2"] == 2 @@ -88,13 +84,11 @@ def test_creation_from_paths_with_wildcards(tmp_path): def test_creation_from_file(): absolute_path_1 = Path(__file__) / "example_1.h5" absolute_path_2 = Path(__file__) / "example_2.h5" - calculations = Calculations.from_files( - path_name_1=absolute_path_1, path_name_2=absolute_path_2 - ) - output_paths = calculations.paths() + batch = Batch.from_files(path_name_1=absolute_path_1, path_name_2=absolute_path_2) + output_paths = batch.paths() assert output_paths["path_name_1"] == [absolute_path_1.parent] assert output_paths["path_name_2"] == [absolute_path_2.parent] - output_number_of_calculations = calculations.number_of_calculations() + output_number_of_calculations = batch.number_of_calculations() assert output_number_of_calculations["path_name_1"] == 1 assert output_number_of_calculations["path_name_2"] == 1 @@ -107,11 +101,11 @@ def test_create_from_files_with_wildcards(tmp_path): create_files = lambda paths: [path.touch() for path in paths] create_files(paths_1) create_files(paths_2) - calculations = Calculations.from_files( + batch = Batch.from_files( file_1=tmp_path / "example1_*.h5", file_2=tmp_path / "example2_*.h5", ) - output_paths = calculations.paths() + output_paths = batch.paths() assert all( [ output_paths["file_1"][i] == absolute_paths_1[i].parent @@ -124,32 +118,32 @@ def test_create_from_files_with_wildcards(tmp_path): for i in range(len(absolute_paths_2)) ] ) - output_number_of_calculations = calculations.number_of_calculations() + output_number_of_calculations = batch.number_of_calculations() assert output_number_of_calculations["file_1"] == 2 assert output_number_of_calculations["file_2"] == 2 -@patch("py4vasp.calculation._base.Refinery.from_path", autospec=True) +@patch("py4vasp._calculation.base.Refinery.from_path", autospec=True) @patch("py4vasp.raw.access", autospec=True) def test_has_attributes(mock_access, mock_from_path): - calculations = Calculations.from_paths(path_name_1="path_1", path_name_2="path_2") - assert hasattr(calculations, "energies") - assert hasattr(calculations.energies, "read") - output_read = calculations.energies.read() + batch = Batch.from_paths(path_name_1="path_1", path_name_2="path_2") + assert hasattr(batch, "energies") + assert hasattr(batch.energies, "read") + output_read = batch.energies.read() assert isinstance(output_read, dict) assert output_read.keys() == {"path_name_1", "path_name_2"} assert isinstance(output_read["path_name_1"], list) assert isinstance(output_read["path_name_2"], list) - assert hasattr(calculations, "forces") - assert hasattr(calculations.forces, "read") - output_read = calculations.forces.read() + assert hasattr(batch, "forces") + assert hasattr(batch.forces, "read") + output_read = batch.forces.read() assert isinstance(output_read, dict) assert output_read.keys() == {"path_name_1", "path_name_2"} assert isinstance(output_read["path_name_1"], list) assert isinstance(output_read["path_name_2"], list) - assert hasattr(calculations, "stresses") - assert hasattr(calculations.stresses, "read") - output_read = calculations.stresses.read() + assert hasattr(batch, "stresses") + assert hasattr(batch.stresses, "read") + output_read = batch.stresses.read() assert isinstance(output_read, dict) assert output_read.keys() == {"path_name_1", "path_name_2"} assert isinstance(output_read["path_name_1"], list) diff --git a/tests/calculation/test_band.py b/tests/calculation/test_band.py index 490656bb..14ecfe9e 100644 --- a/tests/calculation/test_band.py +++ b/tests/calculation/test_band.py @@ -293,7 +293,7 @@ def test_plot_incorrect_width(with_projectors): with_projectors.plot("Sr", width="not a number") -@patch("py4vasp.calculation._band.Band.to_graph") +@patch("py4vasp._calculation.band.Band.to_graph") def test_to_plotly(mock_plot, single_band): fig = single_band.to_plotly("selection", width=0.2) mock_plot.assert_called_once_with("selection", width=0.2) @@ -309,7 +309,7 @@ def test_to_image(single_band): def check_to_image(single_band, filename_argument, expected_filename): - with patch("py4vasp.calculation._band.Band.to_plotly") as plot: + with patch("py4vasp._calculation.band.Band.to_plotly") as plot: single_band.to_image("args", filename=filename_argument, key="word") plot.assert_called_once_with("args", key="word") fig = plot.return_value diff --git a/tests/calculation/test_bandgap.py b/tests/calculation/test_bandgap.py index 91b27968..9ca30436 100644 --- a/tests/calculation/test_bandgap.py +++ b/tests/calculation/test_bandgap.py @@ -164,8 +164,8 @@ def test_plot_incorrect_selection(bandgap, selection): bandgap.plot(selection) -@patch("py4vasp.calculation._bandgap.Bandgap.to_graph") -def test_energy_to_plotly(mock_plot, bandgap): +@patch("py4vasp._calculation.bandgap.Bandgap.to_graph") +def test_bandgap_to_plotly(mock_plot, bandgap): fig = bandgap.to_plotly() mock_plot.assert_called_once_with() graph = mock_plot.return_value @@ -180,7 +180,7 @@ def test_to_image(bandgap): def check_to_image(bandgap, filename_argument, expected_filename): - with patch("py4vasp.calculation._bandgap.Bandgap.to_plotly") as plot: + with patch("py4vasp._calculation.bandgap.Bandgap.to_plotly") as plot: bandgap.to_image("args", filename=filename_argument, key="word") plot.assert_called_once_with("args", key="word") fig = plot.return_value diff --git a/tests/calculation/test_base.py b/tests/calculation/test_base.py index 543cf3af..66a6946e 100644 --- a/tests/calculation/test_base.py +++ b/tests/calculation/test_base.py @@ -11,8 +11,8 @@ import pytest from py4vasp import exception, raw +from py4vasp._calculation import base from py4vasp._util import select -from py4vasp.calculation import _base from .conftest import SELECTION @@ -39,40 +39,40 @@ def mock_behavior(quantity, *, selection=None, path=None, file=None): yield access -class Example(_base.Refinery): +class Example(base.Refinery): def __post_init__(self): self.post_init_called = True - @_base.data_access + @base.data_access def to_dict(self): "to_dict documentation." return self._raw_data.content - @_base.data_access + @base.data_access def wrapper(self): return self.read() - @_base.data_access + @base.data_access def with_arguments(self, mandatory, optional=None): return mandatory, optional - @_base.data_access + @base.data_access def with_variadic_arguments(self, *args, **kwargs): return args, kwargs - @_base.data_access + @base.data_access def with_selection_argument(self, selection=DEFAULT_SELECTION): return self._raw_data.selection, selection - @_base.data_access + @base.data_access def selection_without_default(self, selection): return selection - @_base.data_access + @base.data_access def selection_from_property(self): return self._selection - @_base.data_access + @base.data_access def __str__(self): return self._raw_data.content @@ -255,8 +255,8 @@ def check_mock(example, mock, *args, **kwargs): mock.reset_mock() -class CamelCase(_base.Refinery): - @_base.data_access +class CamelCase(base.Refinery): + @base.data_access def to_dict(self): return "convert CamelCase to snake_case" diff --git a/tests/calculation/test_class.py b/tests/calculation/test_class.py index 441ffbce..aa8935ac 100644 --- a/tests/calculation/test_class.py +++ b/tests/calculation/test_class.py @@ -6,10 +6,10 @@ import pytest -from py4vasp import Calculation, calculation, control, exception +from py4vasp import Calculation, _calculation, control, exception -@patch("py4vasp.calculation._base.Refinery.from_path", autospec=True) +@patch("py4vasp._calculation.base.Refinery.from_path", autospec=True) @patch("py4vasp.raw.access", autospec=True) def test_creation_from_path(mock_access, mock_from_path): # note: in pytest __file__ defaults to absolute path @@ -25,7 +25,7 @@ def test_creation_from_path(mock_access, mock_from_path): mock_from_path.assert_called() -@patch("py4vasp.calculation._base.Refinery.from_file", autospec=True) +@patch("py4vasp._calculation.base.Refinery.from_file", autospec=True) @patch("py4vasp.raw.access", autospec=True) def test_creation_from_file(mock_access, mock_from_file): # note: in pytest __file__ defaults to absolute path @@ -45,7 +45,7 @@ def test_creation_from_file(mock_access, mock_from_file): @patch("py4vasp.raw.access", autospec=True) def test_all_attributes(mock_access): calc = Calculation.from_path("test_path") - for name in calculation.__all__: + for name in _calculation.QUANTITIES: # + _calculation.INPUT_FILES: assert hasattr(calc, name) mock_access.assert_not_called() mock_access.return_value.__enter__.assert_not_called() diff --git a/tests/calculation/test_contcar.py b/tests/calculation/test_contcar.py index d3006112..66962d97 100644 --- a/tests/calculation/test_contcar.py +++ b/tests/calculation/test_contcar.py @@ -61,7 +61,7 @@ def CONTCAR(raw_data, request): selection = request.param raw_contcar = raw_data.CONTCAR(selection) - contcar = calculation.CONTCAR.from_data(raw_contcar) + contcar = calculation._CONTCAR.from_data(raw_contcar) contcar.ref = types.SimpleNamespace() structure = calculation.structure.from_data(raw_data.structure(selection))[-1] contcar.ref.structure = structure @@ -116,4 +116,4 @@ def test_print(CONTCAR, format_): def test_factory_methods(raw_data, check_factory_methods): raw_contcar = raw_data.CONTCAR("Sr2TiO4") - check_factory_methods(calculation.CONTCAR, raw_contcar) + check_factory_methods(calculation._CONTCAR, raw_contcar) diff --git a/tests/calculation/test_module.py b/tests/calculation/test_default_calculation.py similarity index 63% rename from tests/calculation/test_module.py rename to tests/calculation/test_default_calculation.py index 52065114..07435d3d 100644 --- a/tests/calculation/test_module.py +++ b/tests/calculation/test_default_calculation.py @@ -19,6 +19,11 @@ def attribute_included(attr): return True -def test_nonexisting_attribute(): - with pytest.raises(exception.MissingAttribute): - calculation.does_not_exist +@pytest.mark.skip("Input files are not included in current release.") +def test_assigning_to_input_file(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + expected = "SYSTEM = demo INCAR file" + calculation.INCAR = expected + with open("INCAR", "r") as file: + actual = file.read() + assert actual == expected diff --git a/tests/calculation/test_dielectric_function.py b/tests/calculation/test_dielectric_function.py index 69cd110e..d9dd4a7d 100644 --- a/tests/calculation/test_dielectric_function.py +++ b/tests/calculation/test_dielectric_function.py @@ -299,7 +299,7 @@ def check_figure_contains_plots(fig, references, Assert): assert data.name == ref.name -@patch("py4vasp.calculation._dielectric_function.DielectricFunction.to_graph") +@patch("py4vasp._calculation.dielectric_function.DielectricFunction.to_graph") def test_electronic_to_plotly(mock_plot, electronic): fig = electronic.to_plotly("selection") mock_plot.assert_called_once_with("selection") @@ -322,7 +322,7 @@ def test_ionic_to_image(ionic): def check_to_image(dielectric_function, filename_argument, expected_filename): plot_function = ( - "py4vasp.calculation._dielectric_function.DielectricFunction.to_plotly" + "py4vasp._calculation.dielectric_function.DielectricFunction.to_plotly" ) with patch(plot_function) as plot: dielectric_function.to_image("args", filename=filename_argument, key="word") diff --git a/tests/calculation/test_dispersion.py b/tests/calculation/test_dispersion.py index 1f22ad85..d4ca7300 100644 --- a/tests/calculation/test_dispersion.py +++ b/tests/calculation/test_dispersion.py @@ -11,7 +11,7 @@ @pytest.fixture(params=["single_band", "spin_polarized", "line", "phonon"]) def dispersion(raw_data, request): raw_dispersion = raw_data.dispersion(request.param) - dispersion = calculation.dispersion.from_data(raw_dispersion) + dispersion = calculation._dispersion.from_data(raw_dispersion) dispersion.ref = types.SimpleNamespace() dispersion.ref.kpoints = calculation.kpoint.from_data(raw_dispersion.kpoints) dispersion.ref.eigenvalues = raw_dispersion.eigenvalues @@ -98,4 +98,4 @@ def test_print(dispersion, format_): def test_factory_methods(raw_data, check_factory_methods): data = raw_data.dispersion("single_band") - check_factory_methods(calculation.dispersion, data) + check_factory_methods(calculation._dispersion, data) diff --git a/tests/calculation/test_dos.py b/tests/calculation/test_dos.py index b6eb69a9..9f49fb04 100644 --- a/tests/calculation/test_dos.py +++ b/tests/calculation/test_dos.py @@ -206,7 +206,7 @@ def test_plot_combine_projectors(Fe3O4_projectors, Assert): Assert.allclose(data[names.index("Fe_down - p_down")].y, -subtraction_down) -@patch("py4vasp.calculation._dos.Dos.to_graph") +@patch("py4vasp._calculation.dos.Dos.to_graph") def test_Sr2TiO4_to_plotly(mock_plot, Sr2TiO4): fig = Sr2TiO4.to_plotly("selection") mock_plot.assert_called_once_with("selection") @@ -222,7 +222,7 @@ def test_Sr2TiO4_to_image(Sr2TiO4): def check_to_image(Sr2TiO4, filename_argument, expected_filename): - with patch("py4vasp.calculation._dos.Dos.to_plotly") as plot: + with patch("py4vasp._calculation.dos.Dos.to_plotly") as plot: Sr2TiO4.to_image("args", filename=filename_argument, key="word") plot.assert_called_once_with("args", key="word") fig = plot.return_value diff --git a/tests/calculation/test_electronic_minimization.py b/tests/calculation/test_electronic_minimization.py index 88350470..60579698 100644 --- a/tests/calculation/test_electronic_minimization.py +++ b/tests/calculation/test_electronic_minimization.py @@ -10,30 +10,32 @@ @pytest.fixture -def ElectronicMinimization(raw_data): - raw_oszicar = raw_data.electronic_minimization() - oszicar = calculation.electronic_minimization.from_data(raw_oszicar) - oszicar.ref = types.SimpleNamespace() - convergence_data = raw_oszicar.convergence_data - oszicar.ref.N = np.int64(convergence_data[:, 0]) - oszicar.ref.E = convergence_data[:, 1] - oszicar.ref.dE = convergence_data[:, 2] - oszicar.ref.deps = convergence_data[:, 3] - oszicar.ref.ncg = convergence_data[:, 4] - oszicar.ref.rms = convergence_data[:, 5] - oszicar.ref.rmsc = convergence_data[:, 6] - oszicar.ref.is_elmin_converged = [raw_oszicar.is_elmin_converged == [0.0]] +def electronic_minimization(raw_data): + raw_electronic_minimization = raw_data.electronic_minimization() + constructor = calculation.electronic_minimization.from_data + electronic_minimization = constructor(raw_electronic_minimization) + electronic_minimization.ref = types.SimpleNamespace() + convergence_data = raw_electronic_minimization.convergence_data + electronic_minimization.ref.N = np.int64(convergence_data[:, 0]) + electronic_minimization.ref.E = convergence_data[:, 1] + electronic_minimization.ref.dE = convergence_data[:, 2] + electronic_minimization.ref.deps = convergence_data[:, 3] + electronic_minimization.ref.ncg = convergence_data[:, 4] + electronic_minimization.ref.rms = convergence_data[:, 5] + electronic_minimization.ref.rmsc = convergence_data[:, 6] + is_elmin_converged = [raw_electronic_minimization.is_elmin_converged == [0.0]] + electronic_minimization.ref.is_elmin_converged = is_elmin_converged string_rep = "N\t\tE\t\tdE\t\tdeps\t\tncg\trms\t\trms(c)\n" format_rep = "{0:g}\t{1:0.12E}\t{2:0.6E}\t{3:0.6E}\t{4:g}\t{5:0.3E}\t{6:0.3E}\n" for idx in range(len(convergence_data)): string_rep += format_rep.format(*convergence_data[idx]) - oszicar.ref.string_rep = str(string_rep) - return oszicar + electronic_minimization.ref.string_rep = str(string_rep) + return electronic_minimization -def test_read(ElectronicMinimization, Assert): - actual = ElectronicMinimization.read() - expected = ElectronicMinimization.ref +def test_read(electronic_minimization, Assert): + actual = electronic_minimization.read() + expected = electronic_minimization.ref Assert.allclose(actual["N"], expected.N) Assert.allclose(actual["E"], expected.E) Assert.allclose(actual["dE"], expected.dE) @@ -46,22 +48,21 @@ def test_read(ElectronicMinimization, Assert): @pytest.mark.parametrize( "quantity_name", ["N", "E", "dE", "deps", "ncg", "rms", "rms(c)"] ) -def test_read_selection(quantity_name, ElectronicMinimization, Assert): - actual = ElectronicMinimization.read(quantity_name) - expected = getattr( - ElectronicMinimization.ref, quantity_name.replace("(", "").replace(")", "") - ) +def test_read_selection(quantity_name, electronic_minimization, Assert): + actual = electronic_minimization.read(quantity_name) + name_without_parenthesis = quantity_name.replace("(", "").replace(")", "") + expected = getattr(electronic_minimization.ref, name_without_parenthesis) Assert.allclose(actual[quantity_name], expected) -def test_read_incorrect_selection(ElectronicMinimization): +def test_read_incorrect_selection(electronic_minimization): with pytest.raises(exception.RefinementError): - ElectronicMinimization.read("forces") + electronic_minimization.read("forces") -def test_slice(ElectronicMinimization, Assert): - actual = ElectronicMinimization[0:1].read() - expected = ElectronicMinimization.ref +def test_slice(electronic_minimization, Assert): + actual = electronic_minimization[0:1].read() + expected = electronic_minimization.ref Assert.allclose(actual["N"], expected.N) Assert.allclose(actual["E"], expected.E) Assert.allclose(actual["dE"], expected.dE) @@ -71,21 +72,21 @@ def test_slice(ElectronicMinimization, Assert): Assert.allclose(actual["rms(c)"], expected.rmsc) -def test_plot(ElectronicMinimization, Assert): - graph = ElectronicMinimization.plot() +def test_plot(electronic_minimization, Assert): + graph = electronic_minimization.plot() assert graph.xlabel == "Iteration number" assert graph.ylabel == "E" assert len(graph.series) == 1 - Assert.allclose(graph.series[0].x, ElectronicMinimization.ref.N) - Assert.allclose(graph.series[0].y, ElectronicMinimization.ref.E) + Assert.allclose(graph.series[0].x, electronic_minimization.ref.N) + Assert.allclose(graph.series[0].y, electronic_minimization.ref.E) -def test_print(ElectronicMinimization, format_): - actual, _ = format_(ElectronicMinimization) - assert actual["text/plain"] == ElectronicMinimization.ref.string_rep +def test_print(electronic_minimization, format_): + actual, _ = format_(electronic_minimization) + assert actual["text/plain"] == electronic_minimization.ref.string_rep -def test_is_converged(ElectronicMinimization): - actual = ElectronicMinimization.is_converged() - expected = ElectronicMinimization.ref.is_elmin_converged +def test_is_converged(electronic_minimization): + actual = electronic_minimization.is_converged() + expected = electronic_minimization.ref.is_elmin_converged assert actual == expected diff --git a/tests/calculation/test_energy.py b/tests/calculation/test_energy.py index b301ec6f..2df79aa3 100644 --- a/tests/calculation/test_energy.py +++ b/tests/calculation/test_energy.py @@ -96,7 +96,7 @@ def test_incorrect_label(MD_energy): MD_energy.plot(number_instead_of_string) -@patch("py4vasp.calculation._energy.Energy.to_graph") +@patch("py4vasp._calculation.energy.Energy.to_graph") def test_energy_to_plotly(mock_plot, MD_energy): fig = MD_energy.to_plotly("selection") mock_plot.assert_called_once_with("selection") @@ -112,7 +112,7 @@ def test_to_image(MD_energy): def check_to_image(MD_energy, filename_argument, expected_filename): - with patch("py4vasp.calculation._energy.Energy.to_plotly") as plot: + with patch("py4vasp._calculation.energy.Energy.to_plotly") as plot: MD_energy.to_image("args", filename=filename_argument, key="word") plot.assert_called_once_with("args", key="word") fig = plot.return_value diff --git a/tests/calculation/test_fatband.py b/tests/calculation/test_fatband.py index f2a07936..d0f1891c 100644 --- a/tests/calculation/test_fatband.py +++ b/tests/calculation/test_fatband.py @@ -13,7 +13,7 @@ def fatband(raw_data): raw_fatband = raw_data.fatband("default") fatband = calculation.fatband.from_data(raw_fatband) fatband.ref = types.SimpleNamespace() - fatband.ref.dispersion = calculation.dispersion.from_data(raw_fatband.dispersion) + fatband.ref.dispersion = calculation._dispersion.from_data(raw_fatband.dispersion) fatbands = raw_fatband.fatbands fatband.ref.fatbands = fatbands[:, :, 0] + fatbands[:, :, 1] * 1j fatband.ref.fermi_energy = raw_fatband.fermi_energy diff --git a/tests/calculation/test_magnetism.py b/tests/calculation/test_magnetism.py index 406deed6..a43b954f 100644 --- a/tests/calculation/test_magnetism.py +++ b/tests/calculation/test_magnetism.py @@ -63,7 +63,8 @@ def __getitem__(self, step): reference.moments = np.moveaxis(raw_magnetism.spin_moments[:, 1:4], 1, 3) else: spin_moments = np.moveaxis(raw_magnetism.spin_moments[:, 1:4], 1, 3) - orbital_moments = np.moveaxis(raw_magnetism.orbital_moments[:, 1:4], 1, 3) + orbital_moments = np.zeros_like(spin_moments).astype(np.float64) + orbital_moments[:, :, 1:] += np.moveaxis(raw_magnetism.orbital_moments, 1, 3) reference.moments = spin_moments + orbital_moments reference.spin_moments = spin_moments reference.orbital_moments = orbital_moments diff --git a/tests/calculation/test_pair_correlation.py b/tests/calculation/test_pair_correlation.py index c3063919..17c3be39 100644 --- a/tests/calculation/test_pair_correlation.py +++ b/tests/calculation/test_pair_correlation.py @@ -71,7 +71,7 @@ def test_plot_nonexisting_label(pair_correlation): pair_correlation.plot("label does exist") -@patch("py4vasp.calculation._pair_correlation.PairCorrelation.to_graph") +@patch("py4vasp._calculation.pair_correlation.PairCorrelation.to_graph") def test_pair_correlation_to_plotly(mock_plot, pair_correlation): fig = pair_correlation.to_plotly("selection") mock_plot.assert_called_once_with("selection") @@ -87,7 +87,7 @@ def test_to_image(pair_correlation): def check_to_image(pair_correlation, filename_argument, expected_filename): - function = "py4vasp.calculation._pair_correlation.PairCorrelation.to_plotly" + function = "py4vasp._calculation.pair_correlation.PairCorrelation.to_plotly" with patch(function) as plot: pair_correlation.to_image("args", filename=filename_argument, key="word") plot.assert_called_once_with("args", key="word") diff --git a/tests/calculation/test_partial_charge.py b/tests/calculation/test_partial_density.py similarity index 58% rename from tests/calculation/test_partial_charge.py rename to tests/calculation/test_partial_density.py index 53009a3f..1dfce5b0 100644 --- a/tests/calculation/test_partial_charge.py +++ b/tests/calculation/test_partial_density.py @@ -7,8 +7,8 @@ import pytest from py4vasp import calculation +from py4vasp._calculation.partial_density import STM_settings from py4vasp._util.slicing import plane -from py4vasp.calculation._partial_charge import STM_settings from py4vasp.exception import IncorrectUsage, NoData, NotImplemented @@ -35,48 +35,48 @@ "split_bands and split_kpoints and spin_polarized Sr2TiO4", ] ) -def PartialCharge(raw_data, request): - return make_reference_partial_charge(raw_data, request.param) +def PartialDensity(raw_data, request): + return make_reference_partial_density(raw_data, request.param) @pytest.fixture -def NonSplitPartialCharge(raw_data): - return make_reference_partial_charge(raw_data, "no splitting no spin") +def NonSplitPartialDensity(raw_data): + return make_reference_partial_density(raw_data, "no splitting no spin") @pytest.fixture -def PolarizedNonSplitPartialCharge(raw_data): - return make_reference_partial_charge(raw_data, "spin_polarized") +def PolarizedNonSplitPartialDensity(raw_data): + return make_reference_partial_density(raw_data, "spin_polarized") @pytest.fixture -def PolarizedNonSplitPartialChargeCa3AsBr3(raw_data): - return make_reference_partial_charge(raw_data, "spin_polarized Ca3AsBr3") +def PolarizedNonSplitPartialDensityCa3AsBr3(raw_data): + return make_reference_partial_density(raw_data, "spin_polarized Ca3AsBr3") @pytest.fixture -def NonSplitPartialChargeCaAs3_110(raw_data): - return make_reference_partial_charge(raw_data, "CaAs3_110") +def NonSplitPartialDensityCaAs3_110(raw_data): + return make_reference_partial_density(raw_data, "CaAs3_110") @pytest.fixture -def NonSplitPartialChargeNi_100(raw_data): - return make_reference_partial_charge(raw_data, "Ni100") +def NonSplitPartialDensityNi_100(raw_data): + return make_reference_partial_density(raw_data, "Ni100") @pytest.fixture -def PolarizedNonSplitPartialChargeSr2TiO4(raw_data): - return make_reference_partial_charge(raw_data, "spin_polarized Sr2TiO4") +def PolarizedNonSplitPartialDensitySr2TiO4(raw_data): + return make_reference_partial_density(raw_data, "spin_polarized Sr2TiO4") @pytest.fixture -def NonPolarizedBandSplitPartialCharge(raw_data): - return make_reference_partial_charge(raw_data, "split_bands") +def NonPolarizedBandSplitPartialDensity(raw_data): + return make_reference_partial_density(raw_data, "split_bands") @pytest.fixture -def PolarizedAllSplitPartialCharge(raw_data): - return make_reference_partial_charge( +def PolarizedAllSplitPartialDensity(raw_data): + return make_reference_partial_density( raw_data, "split_bands and split_kpoints and spin_polarized" ) @@ -86,121 +86,123 @@ def spin(request): return request.param -def make_reference_partial_charge(raw_data, selection): - raw_partial_charge = raw_data.partial_charge(selection=selection) - parchg = calculation.partial_charge.from_data(raw_partial_charge) +def make_reference_partial_density(raw_data, selection): + raw_partial_density = raw_data.partial_density(selection=selection) + parchg = calculation.partial_density.from_data(raw_partial_density) parchg.ref = types.SimpleNamespace() - parchg.ref.structure = calculation.structure.from_data(raw_partial_charge.structure) + parchg.ref.structure = calculation.structure.from_data( + raw_partial_density.structure + ) parchg.ref.plane_vectors = plane( cell=parchg.ref.structure.lattice_vectors(), cut="c", normal="z", ) - parchg.ref.partial_charge = raw_partial_charge.partial_charge - parchg.ref.bands = raw_partial_charge.bands - parchg.ref.kpoints = raw_partial_charge.kpoints - parchg.ref.grid = raw_partial_charge.grid + parchg.ref.partial_density = raw_partial_density.partial_charge + parchg.ref.bands = raw_partial_density.bands + parchg.ref.kpoints = raw_partial_density.kpoints + parchg.ref.grid = raw_partial_density.grid return parchg -def test_read(PartialCharge, Assert, not_core): - actual = PartialCharge.read() - expected = PartialCharge.ref +def test_read(PartialDensity, Assert, not_core): + actual = PartialDensity.read() + expected = PartialDensity.ref Assert.allclose(actual["bands"], expected.bands) Assert.allclose(actual["kpoints"], expected.kpoints) Assert.allclose(actual["grid"], expected.grid) - expected_charge = np.squeeze(np.asarray(expected.partial_charge).T) - Assert.allclose(actual["partial_charge"], expected_charge) + expected_density = np.squeeze(np.asarray(expected.partial_density).T) + Assert.allclose(actual["partial_density"], expected_density) Assert.same_structure(actual["structure"], expected.structure.read()) -def test_topology(PartialCharge, not_core): - actual = PartialCharge._topology() - expected = str(PartialCharge.ref.structure._topology()) +def test_stoichiometry(PartialDensity, not_core): + actual = PartialDensity._stoichiometry() + expected = str(PartialDensity.ref.structure._stoichiometry()) assert actual == expected -def test_bands(PartialCharge, Assert, not_core): - actual = PartialCharge.bands() - expected = PartialCharge.ref.bands +def test_bands(PartialDensity, Assert, not_core): + actual = PartialDensity.bands() + expected = PartialDensity.ref.bands Assert.allclose(actual, expected) -def test_kpoints(PartialCharge, Assert, not_core): - actual = PartialCharge.kpoints() - expected = PartialCharge.ref.kpoints +def test_kpoints(PartialDensity, Assert, not_core): + actual = PartialDensity.kpoints() + expected = PartialDensity.ref.kpoints Assert.allclose(actual, expected) -def test_grid(PartialCharge, Assert, not_core): - actual = PartialCharge.grid() - expected = PartialCharge.ref.grid +def test_grid(PartialDensity, Assert, not_core): + actual = PartialDensity.grid() + expected = PartialDensity.ref.grid Assert.allclose(actual, expected) -def test_non_split_to_numpy(PolarizedNonSplitPartialCharge, Assert, not_core): - actual = PolarizedNonSplitPartialCharge.to_numpy("total") - expected = PolarizedNonSplitPartialCharge.ref.partial_charge +def test_non_split_to_numpy(PolarizedNonSplitPartialDensity, Assert, not_core): + actual = PolarizedNonSplitPartialDensity.to_numpy("total") + expected = PolarizedNonSplitPartialDensity.ref.partial_density Assert.allclose(actual, expected[0, 0, 0].T) - actual = PolarizedNonSplitPartialCharge.to_numpy("up") + actual = PolarizedNonSplitPartialDensity.to_numpy("up") Assert.allclose(actual, 0.5 * (expected[0, 0, 0].T + expected[0, 0, 1].T)) - actual = PolarizedNonSplitPartialCharge.to_numpy("down") + actual = PolarizedNonSplitPartialDensity.to_numpy("down") Assert.allclose(actual, 0.5 * (expected[0, 0, 0].T - expected[0, 0, 1].T)) -def test_split_to_numpy(PolarizedAllSplitPartialCharge, Assert, not_core): - bands = PolarizedAllSplitPartialCharge.ref.bands - kpoints = PolarizedAllSplitPartialCharge.ref.kpoints +def test_split_to_numpy(PolarizedAllSplitPartialDensity, Assert, not_core): + bands = PolarizedAllSplitPartialDensity.ref.bands + kpoints = PolarizedAllSplitPartialDensity.ref.kpoints for band_index, band in enumerate(bands): for kpoint_index, kpoint in enumerate(kpoints): - actual = PolarizedAllSplitPartialCharge.to_numpy( + actual = PolarizedAllSplitPartialDensity.to_numpy( band=band, kpoint=kpoint, selection="total" ) - expected = PolarizedAllSplitPartialCharge.ref.partial_charge + expected = PolarizedAllSplitPartialDensity.ref.partial_density Assert.allclose(actual, np.asarray(expected)[kpoint_index, band_index, 0].T) msg = f"Band {max(bands) + 1} not found in the bands array." with pytest.raises(NoData) as excinfo: - PolarizedAllSplitPartialCharge.to_numpy( + PolarizedAllSplitPartialDensity.to_numpy( band=max(bands) + 1, kpoint=max(kpoints), selection="up" ) assert msg in str(excinfo.value) msg = f"K-point {min(kpoints) - 1} not found in the kpoints array." with pytest.raises(NoData) as excinfo: - PolarizedAllSplitPartialCharge.to_numpy( + PolarizedAllSplitPartialDensity.to_numpy( band=min(bands), kpoint=min(kpoints) - 1, selection="down" ) assert msg in str(excinfo.value) -def test_non_polarized_to_numpy(NonSplitPartialCharge, spin, Assert, not_core): - actual = NonSplitPartialCharge.to_numpy(selection=spin) - expected = NonSplitPartialCharge.ref.partial_charge +def test_non_polarized_to_numpy(NonSplitPartialDensity, spin, Assert, not_core): + actual = NonSplitPartialDensity.to_numpy(selection=spin) + expected = NonSplitPartialDensity.ref.partial_density Assert.allclose(actual, np.asarray(expected).T[:, :, :, 0, 0, 0]) def test_split_bands_to_numpy( - NonPolarizedBandSplitPartialCharge, spin, Assert, not_core + NonPolarizedBandSplitPartialDensity, spin, Assert, not_core ): - bands = NonPolarizedBandSplitPartialCharge.ref.bands + bands = NonPolarizedBandSplitPartialDensity.ref.bands for band_index, band in enumerate(bands): - actual = NonPolarizedBandSplitPartialCharge.to_numpy(spin, band=band) - expected = NonPolarizedBandSplitPartialCharge.ref.partial_charge + actual = NonPolarizedBandSplitPartialDensity.to_numpy(spin, band=band) + expected = NonPolarizedBandSplitPartialDensity.ref.partial_density Assert.allclose(actual, np.asarray(expected).T[:, :, :, 0, band_index, 0]) -def test_to_stm_split(PolarizedAllSplitPartialCharge, not_core): +def test_to_stm_split(PolarizedAllSplitPartialDensity, not_core): msg = "set LSEPK and LSEPB to .FALSE. in the INCAR file." with pytest.raises(NotImplemented) as excinfo: - PolarizedAllSplitPartialCharge.to_stm(selection="constant_current") + PolarizedAllSplitPartialDensity.to_stm(selection="constant_current") assert msg in str(excinfo.value) -def test_to_stm_nonsplit_tip_to_high(NonSplitPartialCharge, not_core): - actual = NonSplitPartialCharge +def test_to_stm_nonsplit_tip_to_high(NonSplitPartialDensity, not_core): + actual = NonSplitPartialDensity tip_height = 8.4 error = f"""The tip position at {tip_height:.2f} is above half of the estimated vacuum thickness {actual._estimate_vacuum():.2f} Angstrom. @@ -210,45 +212,45 @@ def test_to_stm_nonsplit_tip_to_high(NonSplitPartialCharge, not_core): def test_to_stm_nonsplit_not_orthogonal_no_vacuum( - PolarizedNonSplitPartialChargeSr2TiO4, + PolarizedNonSplitPartialDensitySr2TiO4, not_core, ): msg = "The vacuum region in your cell is too small for STM simulations." with pytest.raises(IncorrectUsage) as excinfo: - PolarizedNonSplitPartialChargeSr2TiO4.to_stm() + PolarizedNonSplitPartialDensitySr2TiO4.to_stm() assert msg in str(excinfo.value) -def test_to_stm_wrong_spin_nonsplit(PolarizedNonSplitPartialCharge, not_core): +def test_to_stm_wrong_spin_nonsplit(PolarizedNonSplitPartialDensity, not_core): msg = "'up', 'down', or 'total'" with pytest.raises(IncorrectUsage) as excinfo: - PolarizedNonSplitPartialCharge.to_stm(selection="all") + PolarizedNonSplitPartialDensity.to_stm(selection="all") assert msg in str(excinfo.value) -def test_to_stm_wrong_mode(PolarizedNonSplitPartialCharge, not_core): +def test_to_stm_wrong_mode(PolarizedNonSplitPartialDensity, not_core): with pytest.raises(IncorrectUsage) as excinfo: - PolarizedNonSplitPartialCharge.to_stm(selection="stm") + PolarizedNonSplitPartialDensity.to_stm(selection="stm") assert "STM mode" in str(excinfo.value) -def test_wrong_vacuum_direction(NonSplitPartialChargeNi_100, not_core): +def test_wrong_vacuum_direction(NonSplitPartialDensityNi_100, not_core): msg = """The vacuum region in your cell is not located along the third lattice vector.""" with pytest.raises(NotImplemented) as excinfo: - NonSplitPartialChargeNi_100.to_stm() + NonSplitPartialDensityNi_100.to_stm() assert msg in str(excinfo.value) @pytest.mark.parametrize("alias", ("constant_height", "ch", "height")) def test_to_stm_nonsplit_constant_height( - PolarizedNonSplitPartialCharge, alias, spin, Assert, not_core + PolarizedNonSplitPartialDensity, alias, spin, Assert, not_core ): supercell = 3 - actual = PolarizedNonSplitPartialCharge.to_stm( + actual = PolarizedNonSplitPartialDensity.to_stm( selection=f"{alias}({spin})", tip_height=2.0, supercell=supercell ) - expected = PolarizedNonSplitPartialCharge.ref + expected = PolarizedNonSplitPartialDensity.ref assert type(actual.series.data) == np.ndarray assert actual.series.data.shape == (expected.grid[0], expected.grid[1]) Assert.allclose(actual.series.lattice.vectors, expected.plane_vectors.vectors) @@ -265,16 +267,16 @@ def test_to_stm_nonsplit_constant_height( @pytest.mark.parametrize("alias", ("constant_current", "cc", "current")) def test_to_stm_nonsplit_constant_current( - PolarizedNonSplitPartialCharge, alias, spin, Assert, not_core + PolarizedNonSplitPartialDensity, alias, spin, Assert, not_core ): current = 5 supercell = np.asarray([2, 4]) - actual = PolarizedNonSplitPartialCharge.to_stm( + actual = PolarizedNonSplitPartialDensity.to_stm( selection=f"{spin}({alias})", current=current, supercell=supercell, ) - expected = PolarizedNonSplitPartialCharge.ref + expected = PolarizedNonSplitPartialDensity.ref assert type(actual.series.data) == np.ndarray assert actual.series.data.shape == (expected.grid[0], expected.grid[1]) Assert.allclose(actual.series.lattice.vectors, expected.plane_vectors.vectors) @@ -291,16 +293,16 @@ def test_to_stm_nonsplit_constant_current( @pytest.mark.parametrize("alias", ("constant_current", "cc", "current")) def test_to_stm_nonsplit_constant_current_non_ortho( - NonSplitPartialChargeCaAs3_110, alias, spin, Assert, not_core + NonSplitPartialDensityCaAs3_110, alias, spin, Assert, not_core ): current = 5 supercell = np.asarray([2, 4]) - actual = NonSplitPartialChargeCaAs3_110.to_stm( + actual = NonSplitPartialDensityCaAs3_110.to_stm( selection=f"{spin}({alias})", current=current, supercell=supercell, ) - expected = NonSplitPartialChargeCaAs3_110.ref + expected = NonSplitPartialDensityCaAs3_110.ref assert type(actual.series.data) == np.ndarray assert actual.series.data.shape == (expected.grid[0], expected.grid[1]) Assert.allclose(actual.series.lattice.vectors, expected.plane_vectors.vectors) @@ -315,8 +317,8 @@ def test_to_stm_nonsplit_constant_current_non_ortho( assert f"{current:.2f}" in actual.title -def test_stm_settings(PolarizedNonSplitPartialCharge, not_core): - actual = dataclasses.asdict(PolarizedNonSplitPartialCharge.stm_settings) +def test_stm_default_settings(PolarizedNonSplitPartialDensity, not_core): + actual = dataclasses.asdict(PolarizedNonSplitPartialDensity.stm_settings) defaults = { "sigma_xy": 4.0, "sigma_z": 4.0, @@ -332,44 +334,44 @@ def test_stm_settings(PolarizedNonSplitPartialCharge, not_core): enhancement_factor=500, interpolation_factor=5, ) - graph = PolarizedNonSplitPartialCharge.to_stm(stm_settings=modified) + graph = PolarizedNonSplitPartialDensity.to_stm(stm_settings=modified) assert graph.series.settings == modified -def test_smoothening_change(PolarizedNonSplitPartialCharge, not_core): +def test_smoothening_change(PolarizedNonSplitPartialDensity, not_core): mod_settings = STM_settings(sigma_xy=2.0, sigma_z=2.0, truncate=1.0) - data = PolarizedNonSplitPartialCharge.to_numpy("total", band=0, kpoint=0) - default_smoothed_density = PolarizedNonSplitPartialCharge._smooth_stm_data( + data = PolarizedNonSplitPartialDensity.to_numpy("total", band=0, kpoint=0) + default_smoothed_density = PolarizedNonSplitPartialDensity._smooth_stm_data( data=data, stm_settings=STM_settings() ) - new_smoothed_density = PolarizedNonSplitPartialCharge._smooth_stm_data( + new_smoothed_density = PolarizedNonSplitPartialDensity._smooth_stm_data( data=data, stm_settings=mod_settings ) assert not np.allclose(default_smoothed_density, new_smoothed_density) -def test_enhancement_setting_change(PolarizedNonSplitPartialCharge, Assert, not_core): +def test_enhancement_setting_change(PolarizedNonSplitPartialDensity, Assert, not_core): enhance_settings = STM_settings( enhancement_factor=STM_settings().enhancement_factor / 2.0 ) - graph_def = PolarizedNonSplitPartialCharge.to_stm("constant_height") - graph_less_enhanced = PolarizedNonSplitPartialCharge.to_stm( + graph_def = PolarizedNonSplitPartialDensity.to_stm("constant_height") + graph_less_enhanced = PolarizedNonSplitPartialDensity.to_stm( "constant_height", stm_settings=enhance_settings ) Assert.allclose(graph_def.series.data, graph_less_enhanced.series.data * 2) -def test_interpolation_setting_change(PolarizedNonSplitPartialCharge, not_core): +def test_interpolation_setting_change(PolarizedNonSplitPartialDensity, not_core): interp_settings = STM_settings( interpolation_factor=STM_settings().interpolation_factor / 4.0 ) - graph_def = PolarizedNonSplitPartialCharge.to_stm("constant_current", current=1) - graph_less_interp_points = PolarizedNonSplitPartialCharge.to_stm( + graph_def = PolarizedNonSplitPartialDensity.to_stm("constant_current", current=1) + graph_less_interp_points = PolarizedNonSplitPartialDensity.to_stm( "constant_current", current=1, stm_settings=interp_settings ) assert not np.allclose(graph_def.series.data, graph_less_interp_points.series.data) def test_factory_methods(raw_data, check_factory_methods): - data = raw_data.partial_charge("spin_polarized") - check_factory_methods(calculation.partial_charge, data) + data = raw_data.partial_density("spin_polarized") + check_factory_methods(calculation.partial_density, data) diff --git a/tests/calculation/test_phonon_band.py b/tests/calculation/test_phonon_band.py index fbc13002..3f9f0179 100644 --- a/tests/calculation/test_phonon_band.py +++ b/tests/calculation/test_phonon_band.py @@ -19,7 +19,8 @@ def phonon_band(raw_data): band.ref.modes = convert.to_complex(raw_band.eigenvectors) raw_qpoints = raw_band.dispersion.kpoints band.ref.qpoints = calculation.kpoint.from_data(raw_qpoints) - band.ref.topology = calculation.topology.from_data(raw_band.topology) + raw_stoichiometry = raw_band.stoichiometry + band.ref.stoichiometry = calculation._stoichiometry.from_data(raw_stoichiometry) Sr = slice(0, 2) band.ref.Sr = np.sum(np.abs(band.ref.modes[:, :, Sr, :]), axis=(2, 3)) Ti = 2 @@ -88,7 +89,7 @@ def check_series(self, series, projection, label, width): self.Assert.allclose(series.width, width * projection.T) -@patch("py4vasp.calculation._phonon_band.PhononBand.to_graph") +@patch("py4vasp._calculation.phonon_band.PhononBand.to_graph") def test_to_plotly(mock_plot, phonon_band): fig = phonon_band.to_plotly("selection", width=0.2) mock_plot.assert_called_once_with("selection", width=0.2) @@ -104,7 +105,7 @@ def test_to_image(phonon_band): def check_to_image(phonon_band, filename_argument, expected_filename): - with patch("py4vasp.calculation._phonon_band.PhononBand.to_plotly") as plot: + with patch("py4vasp._calculation.phonon_band.PhononBand.to_plotly") as plot: phonon_band.to_image("args", filename=filename_argument, key="word") plot.assert_called_once_with("args", key="word") fig = plot.return_value diff --git a/tests/calculation/test_phonon_dos.py b/tests/calculation/test_phonon_dos.py index 3cefed00..fd6bac2c 100644 --- a/tests/calculation/test_phonon_dos.py +++ b/tests/calculation/test_phonon_dos.py @@ -65,7 +65,7 @@ def check_series(series, reference, label, Assert): Assert.allclose(series.y, reference) -@patch("py4vasp.calculation._phonon_dos.PhononDos.to_graph") +@patch("py4vasp._calculation.phonon_dos.PhononDos.to_graph") def test_phonon_dos_to_plotly(mock_plot, phonon_dos): fig = phonon_dos.to_plotly("selection") mock_plot.assert_called_once_with("selection") @@ -81,7 +81,7 @@ def test_phonon_dos_to_image(phonon_dos): def check_to_image(phonon_dos, filename_argument, expected_filename): - with patch("py4vasp.calculation._phonon_dos.PhononDos.to_plotly") as plot: + with patch("py4vasp._calculation.phonon_dos.PhononDos.to_plotly") as plot: phonon_dos.to_image("args", filename=filename_argument, key="word") plot.assert_called_once_with("args", key="word") fig = plot.return_value diff --git a/tests/calculation/test_projector.py b/tests/calculation/test_projector.py index 4e9baee9..2d501369 100644 --- a/tests/calculation/test_projector.py +++ b/tests/calculation/test_projector.py @@ -6,8 +6,8 @@ import pytest from py4vasp import calculation, exception +from py4vasp._calculation.selection import Selection from py4vasp._util import select -from py4vasp.calculation._selection import Selection @pytest.fixture diff --git a/tests/calculation/test_repr.py b/tests/calculation/test_repr.py index 205ec3d0..d3db9344 100644 --- a/tests/calculation/test_repr.py +++ b/tests/calculation/test_repr.py @@ -1,16 +1,18 @@ # Copyright © VASP Software GmbH, # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) import importlib +from pathlib import PosixPath, WindowsPath -from py4vasp import calculation +from py4vasp import _calculation, calculation from py4vasp._util import convert def test_repr(): - for name in calculation._quantities: + for name in _calculation.QUANTITIES: instance = getattr(calculation, name) class_name = convert.to_camelcase(name) - module = importlib.import_module(f"py4vasp.calculation._{name}") + module = importlib.import_module(f"py4vasp._calculation.{name}") locals()[class_name] = getattr(module, class_name) + print(repr(instance)) copy = eval(repr(instance)) assert copy.__class__ == instance.__class__ diff --git a/tests/calculation/test_slice_mixin.py b/tests/calculation/test_slice_mixin.py index 81402d21..83f24e1e 100644 --- a/tests/calculation/test_slice_mixin.py +++ b/tests/calculation/test_slice_mixin.py @@ -5,8 +5,8 @@ import pytest from py4vasp import exception +from py4vasp._calculation import slice_ from py4vasp._util import documentation -from py4vasp.calculation import _slice class Other: @@ -16,8 +16,8 @@ def __init__(self, *args, **kwargs): self._kwargs = kwargs -@documentation.format(examples=_slice.examples("example")) -class ExampleSlice(_slice.Mixin, Other): +@documentation.format(examples=slice_.examples("example")) +class ExampleSlice(slice_.Mixin, Other): "{examples}" def steps(self): @@ -175,7 +175,7 @@ def test_incorrect_argument(): def test_documentation(single_step, last_step): - reference = _slice.examples("example") + reference = slice_.examples("example") assert inspect.getdoc(single_step) == reference assert inspect.getdoc(last_step) == reference diff --git a/tests/calculation/test_topology.py b/tests/calculation/test_stoichiometry.py similarity index 54% rename from tests/calculation/test_topology.py rename to tests/calculation/test_stoichiometry.py index 9019fa92..6dc438e8 100644 --- a/tests/calculation/test_topology.py +++ b/tests/calculation/test_stoichiometry.py @@ -3,8 +3,8 @@ import pytest from py4vasp import calculation, exception +from py4vasp._calculation.selection import Selection from py4vasp._util import import_, select -from py4vasp.calculation._selection import Selection ase = import_.optional("ase") pd = import_.optional("pandas") @@ -12,22 +12,22 @@ class Base: def test_read(self): - topology = self.topology.read() - assert topology[select.all] == Selection(indices=slice(0, 7)) + stoichiometry = self.stoichiometry.read() + assert stoichiometry[select.all] == Selection(indices=slice(0, 7)) for i, name in enumerate(self.names): expected = Selection(indices=slice(i, i + 1), label=name) - assert topology[str(i + 1)] == expected - self.check_ion_indices(topology) + assert stoichiometry[str(i + 1)] == expected + self.check_ion_indices(stoichiometry) def test_to_frame(self, not_core): - actual = self.topology.to_frame() + actual = self.stoichiometry.to_frame() ref_data = {"name": self.names, "element": self.elements} reference = pd.DataFrame(ref_data) assert reference.equals(actual) def test_to_mdtraj(self, not_core): - actual, _ = self.topology.to_mdtraj().to_dataframe() - num_atoms = self.topology.number_atoms() + actual, _ = self.stoichiometry.to_mdtraj().to_dataframe() + num_atoms = self.stoichiometry.number_atoms() ref_data = { "serial": num_atoms * (None,), "name": self.names, @@ -41,23 +41,23 @@ def test_to_mdtraj(self, not_core): assert reference.equals(actual) def test_elements(self): - assert self.topology.elements() == self.elements + assert self.stoichiometry.elements() == self.elements def test_ion_types(self): - assert self.topology.ion_types() == self.unique_elements + assert self.stoichiometry.ion_types() == self.unique_elements def test_names(self): - actual = self.topology.names() + actual = self.stoichiometry.names() assert actual == self.names def test_number_atoms(self): - assert self.topology.number_atoms() == 7 + assert self.stoichiometry.number_atoms() == 7 def test_from_ase(self, not_core): structure = ase.Atoms("".join(self.elements)) - topology = calculation.topology.from_ase(structure) - assert topology.elements() == self.elements - assert str(topology) == str(self.topology) + stoichiometry = calculation._stoichiometry.from_ase(structure) + assert stoichiometry.elements() == self.elements + assert str(stoichiometry) == str(self.stoichiometry) @property def unique_elements(self): @@ -71,23 +71,25 @@ def unique_elements(self): class TestSr2TiO4(Base): @pytest.fixture(autouse=True) def _setup(self, raw_data): - self.topology = calculation.topology.from_data(raw_data.topology("Sr2TiO4")) + self.stoichiometry = calculation._stoichiometry.from_data( + raw_data.stoichiometry("Sr2TiO4") + ) self.names = ["Sr_1", "Sr_2", "Ti_1", "O_1", "O_2", "O_3", "O_4"] self.elements = 2 * ["Sr"] + ["Ti"] + 4 * ["O"] - def check_ion_indices(self, topology): - assert topology["Sr"] == Selection(indices=slice(0, 2), label="Sr") - assert topology["Ti"] == Selection(indices=slice(2, 3), label="Ti") - assert topology["O"] == Selection(indices=slice(3, 7), label="O") + def check_ion_indices(self, stoichiometry): + assert stoichiometry["Sr"] == Selection(indices=slice(0, 2), label="Sr") + assert stoichiometry["Ti"] == Selection(indices=slice(2, 3), label="Ti") + assert stoichiometry["O"] == Selection(indices=slice(3, 7), label="O") def test_to_poscar(self): - assert self.topology.to_POSCAR() == "Sr Ti O\n2 1 4" - assert self.topology.to_POSCAR(".format.") == "Sr Ti O.format.\n2 1 4" + assert self.stoichiometry.to_POSCAR() == "Sr Ti O\n2 1 4" + assert self.stoichiometry.to_POSCAR(".format.") == "Sr Ti O.format.\n2 1 4" with pytest.raises(exception.IncorrectUsage): - self.topology.to_POSCAR(None) + self.stoichiometry.to_POSCAR(None) def test_print(self, format_): - actual, _ = format_(self.topology) + actual, _ = format_(self.stoichiometry) reference = { "text/plain": "Sr2TiO4", "text/html": "Sr2TiO4", @@ -100,21 +102,21 @@ class TestCa3AsBr3(Base): @pytest.fixture(autouse=True) def _setup(self, raw_data): - raw_topology = raw_data.topology("Ca2AsBr-CaBr2") - self.topology = calculation.topology.from_data(raw_topology) + raw_stoichiometry = raw_data.stoichiometry("Ca2AsBr-CaBr2") + self.stoichiometry = calculation._stoichiometry.from_data(raw_stoichiometry) self.names = ["Ca_1", "Ca_2", "As_1", "Br_1", "Ca_3", "Br_2", "Br_3"] self.elements = ["Ca", "Ca", "As", "Br", "Ca", "Br", "Br"] - def check_ion_indices(self, topology): - assert topology["Ca"] == Selection(indices=[0, 1, 4], label="Ca") - assert topology["As"] == Selection(indices=slice(2, 3), label="As") - assert topology["Br"] == Selection(indices=[3, 5, 6], label="Br") + def check_ion_indices(self, stoichiometry): + assert stoichiometry["Ca"] == Selection(indices=[0, 1, 4], label="Ca") + assert stoichiometry["As"] == Selection(indices=slice(2, 3), label="As") + assert stoichiometry["Br"] == Selection(indices=[3, 5, 6], label="Br") def test_to_poscar(self): - assert self.topology.to_POSCAR() == "Ca As Br Ca Br\n2 1 1 1 2" + assert self.stoichiometry.to_POSCAR() == "Ca As Br Ca Br\n2 1 1 1 2" def test_print(self, format_): - actual, _ = format_(self.topology) + actual, _ = format_(self.stoichiometry) reference = { "text/plain": "Ca3AsBr3", "text/html": "Ca3AsBr3", @@ -123,5 +125,5 @@ def test_print(self, format_): def test_factory_methods(raw_data, check_factory_methods): - data = raw_data.topology("Sr2TiO4") - check_factory_methods(calculation.topology, data) + data = raw_data.stoichiometry("Sr2TiO4") + check_factory_methods(calculation._stoichiometry, data) diff --git a/tests/calculation/test_structure.py b/tests/calculation/test_structure.py index 2ee5bbe7..f80a1ca8 100644 --- a/tests/calculation/test_structure.py +++ b/tests/calculation/test_structure.py @@ -97,8 +97,8 @@ def make_structure(raw_structure): scale = 1.0 structure.ref.lattice_vectors = scale * raw_structure.cell.lattice_vectors structure.ref.positions = raw_structure.positions - topology = calculation.topology.from_data(raw_structure.topology) - structure.ref.elements = topology.elements() + stoichiometry = calculation._stoichiometry.from_data(raw_structure.stoichiometry) + structure.ref.elements = stoichiometry.elements() return structure diff --git a/tests/calculation/test_workfunction.py b/tests/calculation/test_workfunction.py index ceacf08f..4944c17a 100644 --- a/tests/calculation/test_workfunction.py +++ b/tests/calculation/test_workfunction.py @@ -30,8 +30,8 @@ def test_read(workfunction, Assert): Assert.allclose(actual["average_potential"], workfunction.ref.average_potential) Assert.allclose(actual["vacuum_potential"], workfunction.ref.vacuum_potential) # Uncomment out these lines when vbm and cbm are added to VASP 6.5 - # Assert.allclose(actual["valence_band_maximum"], workfunction.ref.vbm) - # Assert.allclose(actual["conduction_band_minimum"], workfunction.ref.cbm) + Assert.allclose(actual["valence_band_maximum"], workfunction.ref.vbm) + Assert.allclose(actual["conduction_band_minimum"], workfunction.ref.cbm) Assert.allclose(actual["fermi_energy"], workfunction.ref.fermi_energy) @@ -44,7 +44,7 @@ def test_plot(workfunction, Assert): assert graph.series.name == "potential" -@patch("py4vasp.calculation._workfunction.Workfunction.to_graph") +@patch("py4vasp._calculation.workfunction.Workfunction.to_graph") def test_to_plotly(mock_plot, workfunction): fig = workfunction.to_plotly() mock_plot.assert_called_once_with() @@ -60,7 +60,7 @@ def test_to_image(workfunction): def check_to_image(workfunction, filename_argument, expected_filename): - with patch("py4vasp.calculation._workfunction.Workfunction.to_plotly") as plot: + with patch("py4vasp._calculation.workfunction.Workfunction.to_plotly") as plot: workfunction.to_image("args", filename=filename_argument, key="word") plot.assert_called_once_with("args", key="word") fig = plot.return_value @@ -72,18 +72,18 @@ def test_print(workfunction, format_): reference = """\ workfunction along {lattice_vector}: vacuum potential: {vacuum1:.3f} {vacuum2:.3f} - Fermi energy: {fermi_energy:.3f}""" + Fermi energy: {fermi_energy:.3f} + valence band maximum: {vbm:.3f} + conduction band minimum: {cbm:.3f}""" reference = reference.format( lattice_vector=workfunction.ref.lattice_vector, vacuum1=workfunction.ref.vacuum_potential[0], vacuum2=workfunction.ref.vacuum_potential[1], fermi_energy=workfunction.ref.fermi_energy, + vbm=workfunction.ref.vbm, + cbm=workfunction.ref.cbm, ) assert actual == {"text/plain": reference} - # valence band maximum: {vbm:.3f} - # conduction band minimum: {cbm:.3f} - # vbm=workfunction.ref.vbm, - # cbm=workfunction.ref.cbm, def test_factory_methods(raw_data, check_factory_methods): diff --git a/tests/conftest.py b/tests/conftest.py index da06f97f..5defef01 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -133,10 +133,6 @@ def CONTCAR(selection): else: raise exception.NotImplemented() - @staticmethod - def electronic_minimization(selection=None): - return _example_electronic_minimization() - @staticmethod def density(selection): parts = selection.split() @@ -192,6 +188,10 @@ def dos(selection): def elastic_modulus(selection): return _elastic_modulus() + @staticmethod + def electronic_minimization(selection=None): + return _electronic_minimization() + @staticmethod def energy(selection, randomize: bool = False): if selection == "MD": @@ -305,13 +305,13 @@ def structure(selection): raise exception.NotImplemented() @staticmethod - def topology(selection): + def stoichiometry(selection): if selection == "Sr2TiO4": - return _Sr2TiO4_topology() + return _Sr2TiO4_stoichiometry() elif selection == "Fe3O4": - return _Fe3O4_topology() + return _Fe3O4_stoichiometry() elif selection == "Ca2AsBr-CaBr2": # test duplicate entries - return _Ca3AsBr3_topology() + return _Ca3AsBr3_stoichiometry() else: raise exception.NotImplemented() @@ -329,8 +329,8 @@ def workfunction(selection): return _workfunction(selection) @staticmethod - def partial_charge(selection): - return _partial_charge(selection) + def partial_density(selection): + return _partial_density(selection) @pytest.fixture @@ -420,7 +420,7 @@ def _phonon_band(): shape = (*dispersion.eigenvalues.shape, number_atoms, axes, complex_) return raw.PhononBand( dispersion=dispersion, - topology=_Sr2TiO4_topology(), + stoichiometry=_Sr2TiO4_stoichiometry(), eigenvectors=np.linspace(0, 1, np.prod(shape)).reshape(shape), ) @@ -440,7 +440,7 @@ def _phonon_dos(): upper_ratio = np.array(list(reversed(lower_ratio))) ratio = np.linspace(lower_ratio, upper_ratio, number_points).T projections = np.multiply(ratio, dos) - return raw.PhononDos(energies, dos, projections, _Sr2TiO4_topology()) + return raw.PhononDos(energies, dos, projections, _Sr2TiO4_stoichiometry()) def _piezoelectric_tensor(): @@ -545,7 +545,8 @@ def _magnetism(selection): spin_moments=_make_data(np.arange(np.prod(shape)).reshape(shape)), ) if selection == "orbital_moments": - magnetism.orbital_moments = _make_data(np.sqrt(magnetism.spin_moments)) + remove_charge_and_s_component = magnetism.spin_moments[:, 1:, :, 1:] + magnetism.orbital_moments = _make_data(np.sqrt(remove_charge_and_s_component)) return magnetism @@ -667,7 +668,7 @@ def _Sr2TiO4_cell(): ) -def _example_electronic_minimization(): +def _electronic_minimization(): random_convergence_data = np.random.rand(9, 3) iteration_number = np.arange(1, 10)[:, np.newaxis] ncg = np.random.randint(4, 10, (9, 1)) @@ -685,7 +686,7 @@ def _example_electronic_minimization(): ) -def _partial_charge(selection): +def _partial_density(selection): grid_dim = grid_dimensions if "CaAs3_110" in selection: structure = _CaAs3_110_structure() @@ -724,7 +725,7 @@ def _partial_charge(selection): # Fill the gaussian_charge array gaussian_charge[:, :, :, :, gy, gx] = val gaussian_charge = raw.VaspData(gaussian_charge) - return raw.PartialCharge( + return raw.PartialDensity( structure=structure, bands=bands, kpoints=kpoints, @@ -835,7 +836,7 @@ def _Sr2TiO4_potential(included_potential): def _Sr2TiO4_projectors(use_orbitals): orbital_types = "s py pz px dxy dyz dz2 dxz x2-y2 fy3x2 fxyz fyz2 fz3 fxz2 fzx2 fx3" return raw.Projector( - topology=_Sr2TiO4_topology(), + stoichiometry=_Sr2TiO4_stoichiometry(), orbital_types=_make_orbital_types(use_orbitals, orbital_types), number_spins=1, ) @@ -865,7 +866,7 @@ def _Graphite_structure(): [0.33333333, 0.66666667, 0.60127716], ] return raw.Structure( - topology=_Graphite_topology(), + stoichiometry=_Graphite_stoichiometry(), cell=_Graphite_cell(), positions=raw.VaspData(positions), ) @@ -880,8 +881,8 @@ def _Graphite_cell(): return raw.Cell(np.asarray(lattice_vectors), scale=raw.VaspData(1.0)) -def _Graphite_topology(): - return raw.Topology( +def _Graphite_stoichiometry(): + return raw.Stoichiometry( number_ion_types=np.array((10,)), ion_types=np.array(("C",), dtype="S"), ) @@ -897,7 +898,7 @@ def _Ni100_structure(): [0.00000000, 0.40000000, 0.00000000], ] return raw.Structure( - topology=_Ni100_topology(), + stoichiometry=_Ni100_stoichiometry(), cell=_Ni100_cell(), positions=raw.VaspData(positions), ) @@ -912,8 +913,8 @@ def _Ni100_cell(): return raw.Cell(np.asarray(lattice_vectors), scale=raw.VaspData(1.0)) -def _Ni100_topology(): - return raw.Topology( +def _Ni100_stoichiometry(): + return raw.Stoichiometry( number_ion_types=np.array((5,)), ion_types=np.array(("Ni",), dtype="S"), ) @@ -948,7 +949,7 @@ def _CaAs3_110_structure(): [0.77964386, 0.09593968, 0.76122779], ] return raw.Structure( - topology=_CaAs3_110_topology(), + stoichiometry=_CaAs3_110_stoichiometry(), cell=_CaAs3_110_cell(), positions=raw.VaspData(positions), ) @@ -963,8 +964,8 @@ def _CaAs3_110_cell(): return raw.Cell(np.asarray(lattice_vectors), scale=raw.VaspData(1.0)) -def _CaAs3_110_topology(): - return raw.Topology( +def _CaAs3_110_stoichiometry(): + return raw.Stoichiometry( number_ion_types=np.array((6, 18)), ion_types=np.array(("Ca", "As"), dtype="S"), ) @@ -982,14 +983,14 @@ def _Sr2TiO4_structure(): [0.00000, 0.50000, 0.5], ] return raw.Structure( - topology=_Sr2TiO4_topology(), + stoichiometry=_Sr2TiO4_stoichiometry(), cell=_Sr2TiO4_cell(), positions=np.tile(positions, repetitions), ) -def _Sr2TiO4_topology(): - return raw.Topology( +def _Sr2TiO4_stoichiometry(): + return raw.Stoichiometry( number_ion_types=np.array((2, 1, 4)), ion_types=np.array(("Sr", "Ti", "O "), dtype="S"), ) @@ -1082,7 +1083,7 @@ def _Fe3O4_potential(selection, included_potential): def _Fe3O4_projectors(use_orbitals): return raw.Projector( - topology=_Fe3O4_topology(), + stoichiometry=_Fe3O4_stoichiometry(), orbital_types=_make_orbital_types(use_orbitals, "s p d f"), number_spins=2, ) @@ -1112,14 +1113,14 @@ def _Fe3O4_structure(): ] shift = np.linspace(-0.02, 0.01, number_steps) return raw.Structure( - topology=_Fe3O4_topology(), + stoichiometry=_Fe3O4_stoichiometry(), cell=_Fe3O4_cell(), positions=np.add.outer(shift, positions), ) -def _Fe3O4_topology(): - return raw.Topology( +def _Fe3O4_stoichiometry(): + return raw.Stoichiometry( number_ion_types=np.array((3, 4)), ion_types=np.array(("Fe", "O "), dtype="S") ) @@ -1148,14 +1149,14 @@ def _Ca3AsBr3_structure(): [0.5, 0.5, 0.0], # Br_3 ] return raw.Structure( - topology=_Ca3AsBr3_topology(), + stoichiometry=_Ca3AsBr3_stoichiometry(), cell=_Ca3AsBr3_cell(), positions=_make_data(positions), ) -def _Ca3AsBr3_topology(): - return raw.Topology( +def _Ca3AsBr3_stoichiometry(): + return raw.Stoichiometry( number_ion_types=np.array((2, 1, 1, 1, 2)), ion_types=np.array(("Ca", "As", "Br", "Ca", "Br"), dtype="S"), ) diff --git a/tests/util/test_convert.py b/tests/util/test_convert.py index 4e31e835..f6ec203f 100644 --- a/tests/util/test_convert.py +++ b/tests/util/test_convert.py @@ -3,7 +3,7 @@ import numpy as np from py4vasp._config import VASP_COLORS -from py4vasp._util.convert import text_to_string, to_complex, to_rgb +from py4vasp._util.convert import text_to_string, to_camelcase, to_complex, to_rgb def test_text_to_string(): @@ -48,3 +48,11 @@ def test_hex_to_rgb(Assert): expected = np.array(converted) / 255 actual = np.array([to_rgb(color) for color in colors]) Assert.allclose(expected, actual) + + +def test_camelcase(): + assert to_camelcase("foo") == "Foo" + assert to_camelcase("foo_bar") == "FooBar" + assert to_camelcase("foo_bar_baz") == "FooBarBaz" + assert to_camelcase("_foo") == "Foo" + assert to_camelcase("_foo_bar") == "FooBar" diff --git a/tests/util/test_parser.py b/tests/util/test_parser.py index 60c47686..28ba75ae 100644 --- a/tests/util/test_parser.py +++ b/tests/util/test_parser.py @@ -6,7 +6,7 @@ import numpy as np import pytest -from py4vasp._raw.data import CONTCAR, Cell, Structure, Topology +from py4vasp._raw.data import CONTCAR, Cell, Stoichiometry, Structure from py4vasp._raw.data_wrapper import VaspData from py4vasp._util.parser import ParsePoscar from py4vasp.exception import ParserError @@ -401,7 +401,7 @@ def test_negative_scaling_factor(cubic_BN, poscar_creator, Assert): @pytest.mark.parametrize("has_species_name", [True, False]) -def test_topology(cubic_BN, has_species_name, Assert): +def test_stoichiometry(cubic_BN, has_species_name, Assert): poscar_string, componentwise_inputs, arguments = cubic_BN( has_species_name=has_species_name ) @@ -411,25 +411,25 @@ def test_topology(cubic_BN, has_species_name, Assert): VaspData(species_names) if species_names else arguments["species_name"].split() ) expected_ions_per_species = VaspData(ions_per_species) - expected_topology = Topology( + expected_stoichiometry = Stoichiometry( number_ion_types=expected_ions_per_species, ion_types=expected_species_names ) - output_topology = ParsePoscar(poscar_string, **arguments).topology + output_stoichiometry = ParsePoscar(poscar_string, **arguments).stoichiometry Assert.allclose( - expected_topology.number_ion_types, output_topology.number_ion_types + expected_stoichiometry.number_ion_types, output_stoichiometry.number_ion_types ) if has_species_name: - expected_ion_types = expected_topology.ion_types.__array__() + expected_ion_types = expected_stoichiometry.ion_types.__array__() else: - expected_ion_types = expected_topology.ion_types - output_ion_types = output_topology.ion_types.__array__() + expected_ion_types = expected_stoichiometry.ion_types + output_ion_types = output_stoichiometry.ion_types.__array__() assert all(expected_ion_types == output_ion_types) def test_error_no_species_provided(cubic_BN): poscar_string, *_ = cubic_BN(has_species_name=False) with pytest.raises(ParserError): - ParsePoscar(poscar_string).topology + ParsePoscar(poscar_string).stoichiometry @pytest.mark.parametrize("has_selective_dynamics", [True, False])