From 066043c735a9143aa54d1af667259a96c1a907af Mon Sep 17 00:00:00 2001 From: Martin Schlipf Date: Fri, 7 Jun 2024 15:00:54 +0200 Subject: [PATCH 01/15] WIP: hide input files --- src/py4vasp/__init__.py | 2 +- .../{calculation => _calculation}/_CONTCAR.py | 0 .../{calculation => _calculation}/_OSZICAR.py | 0 .../_class.py => _calculation/__init__.py} | 105 ++++++++++++++++-- .../{calculation => _calculation}/_density.py | 0 .../_dielectric_function.py | 0 .../_dielectric_tensor.py | 0 .../_dispersion.py | 10 +- .../{calculation => _calculation}/_dos.py | 0 .../_elastic_modulus.py | 0 .../{calculation => _calculation}/_energy.py | 0 .../{calculation => _calculation}/_fatband.py | 0 .../{calculation => _calculation}/_force.py | 0 .../_force_constant.py | 0 .../_internal_strain.py | 0 .../_magnetism.py | 0 .../_pair_correlation.py | 0 .../_partial_charge.py | 0 .../{calculation => _calculation}/_phonon.py | 0 .../_phonon_band.py | 0 .../_phonon_dos.py | 0 .../_piezoelectric_tensor.py | 0 .../_polarization.py | 0 .../_potential.py | 0 .../{calculation => _calculation}/_stress.py | 0 .../{calculation => _calculation}/_system.py | 0 .../_topology.py | 26 ++--- .../_velocity.py | 0 .../_workfunction.py | 0 .../_band.py => _calculation/band.py} | 36 +++--- .../_bandgap.py => _calculation/bandgap.py} | 32 +++--- .../_base.py => _calculation/base.py} | 0 .../born_effective_charge.py} | 8 +- .../_kpoint.py => _calculation/kpoint.py} | 22 ++-- .../projector.py} | 20 ++-- .../selection.py} | 0 .../_slice.py => _calculation/slice_.py} | 0 .../structure.py} | 50 ++++----- src/py4vasp/_combine/base.py | 5 +- src/py4vasp/_control/poscar.py | 4 +- src/py4vasp/_util/convert.py | 7 +- src/py4vasp/calculation/__init__.py | 98 ---------------- src/py4vasp/exception.py | 5 - tests/calculation/test_band.py | 4 +- tests/calculation/test_bandgap.py | 6 +- tests/calculation/test_base.py | 24 ++-- tests/calculation/test_class.py | 8 +- ..._module.py => test_default_calculation.py} | 10 +- tests/calculation/test_projector.py | 2 +- tests/calculation/test_slice_mixin.py | 8 +- tests/calculation/test_structure.py | 2 +- tests/calculation/test_topology.py | 10 +- tests/util/test_convert.py | 10 +- 53 files changed, 254 insertions(+), 260 deletions(-) rename src/py4vasp/{calculation => _calculation}/_CONTCAR.py (100%) rename src/py4vasp/{calculation => _calculation}/_OSZICAR.py (100%) rename src/py4vasp/{calculation/_class.py => _calculation/__init__.py} (59%) rename src/py4vasp/{calculation => _calculation}/_density.py (100%) rename src/py4vasp/{calculation => _calculation}/_dielectric_function.py (100%) rename src/py4vasp/{calculation => _calculation}/_dielectric_tensor.py (100%) rename src/py4vasp/{calculation => _calculation}/_dispersion.py (96%) rename src/py4vasp/{calculation => _calculation}/_dos.py (100%) rename src/py4vasp/{calculation => _calculation}/_elastic_modulus.py (100%) rename src/py4vasp/{calculation => _calculation}/_energy.py (100%) rename src/py4vasp/{calculation => _calculation}/_fatband.py (100%) rename src/py4vasp/{calculation => _calculation}/_force.py (100%) rename src/py4vasp/{calculation => _calculation}/_force_constant.py (100%) rename src/py4vasp/{calculation => _calculation}/_internal_strain.py (100%) rename src/py4vasp/{calculation => _calculation}/_magnetism.py (100%) rename src/py4vasp/{calculation => _calculation}/_pair_correlation.py (100%) rename src/py4vasp/{calculation => _calculation}/_partial_charge.py (100%) rename src/py4vasp/{calculation => _calculation}/_phonon.py (100%) rename src/py4vasp/{calculation => _calculation}/_phonon_band.py (100%) rename src/py4vasp/{calculation => _calculation}/_phonon_dos.py (100%) rename src/py4vasp/{calculation => _calculation}/_piezoelectric_tensor.py (100%) rename src/py4vasp/{calculation => _calculation}/_polarization.py (100%) rename src/py4vasp/{calculation => _calculation}/_potential.py (100%) rename src/py4vasp/{calculation => _calculation}/_stress.py (100%) rename src/py4vasp/{calculation => _calculation}/_system.py (100%) rename src/py4vasp/{calculation => _calculation}/_topology.py (94%) rename src/py4vasp/{calculation => _calculation}/_velocity.py (100%) rename src/py4vasp/{calculation => _calculation}/_workfunction.py (100%) rename src/py4vasp/{calculation/_band.py => _calculation/band.py} (87%) rename src/py4vasp/{calculation/_bandgap.py => _calculation/bandgap.py} (93%) rename src/py4vasp/{calculation/_base.py => _calculation/base.py} (100%) rename src/py4vasp/{calculation/_born_effective_charge.py => _calculation/born_effective_charge.py} (93%) rename src/py4vasp/{calculation/_kpoint.py => _calculation/kpoint.py} (97%) rename src/py4vasp/{calculation/_projector.py => _calculation/projector.py} (97%) rename src/py4vasp/{calculation/_selection.py => _calculation/selection.py} (100%) rename src/py4vasp/{calculation/_slice.py => _calculation/slice_.py} (100%) rename src/py4vasp/{calculation/_structure.py => _calculation/structure.py} (93%) delete mode 100644 src/py4vasp/calculation/__init__.py rename tests/calculation/{test_module.py => test_default_calculation.py} (68%) diff --git a/src/py4vasp/__init__.py b/src/py4vasp/__init__.py index ec74a95d..6bdbeb73 100644 --- a/src/py4vasp/__init__.py +++ b/src/py4vasp/__init__.py @@ -4,7 +4,7 @@ from py4vasp._calculations import Calculations from py4vasp._third_party.graph import plot from py4vasp._third_party.interactive import set_error_handling -from py4vasp.calculation._class import Calculation +from py4vasp._calculation import calculation, Calculation __version__ = "0.9.0" set_error_handling("Minimal") diff --git a/src/py4vasp/calculation/_CONTCAR.py b/src/py4vasp/_calculation/_CONTCAR.py similarity index 100% rename from src/py4vasp/calculation/_CONTCAR.py rename to src/py4vasp/_calculation/_CONTCAR.py diff --git a/src/py4vasp/calculation/_OSZICAR.py b/src/py4vasp/_calculation/_OSZICAR.py similarity index 100% rename from src/py4vasp/calculation/_OSZICAR.py rename to src/py4vasp/_calculation/_OSZICAR.py diff --git a/src/py4vasp/calculation/_class.py b/src/py4vasp/_calculation/__init__.py similarity index 59% rename from src/py4vasp/calculation/_class.py rename to src/py4vasp/_calculation/__init__.py index c0e4e3dc..c05c4cc8 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", + # "CONTCAR", + # "density", + # "dielectric_function", + # "dielectric_tensor", + # "dos", + # "elastic_modulus", + # "energy", + # "fatband", + # "force", + # "force_constant", + # "internal_strain", + "kpoint", + # "magnetism", + # "OSZICAR", + # "pair_correlation", + # "partial_charge", + # "phonon_band", + # "phonon_dos", + # "piezoelectric_tensor", + # "polarization", + # "potential", + "projector", + # "stress", + "structure", + # "system", + # "velocity", + # "workfunction", + "_dispersion", + "_topology", +) class Calculation: @@ -123,13 +199,15 @@ def POSCAR(self, poscar): 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 @@ -140,7 +218,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 @@ -150,12 +230,19 @@ def _add_to_documentation(calc, name): calc.__doc__ += f" ~py4vasp.calculation.{name}\n " return calc - -Calculation = _add_all_refinement_classes(Calculation, _add_to_documentation) - - def _add_input_files(calc): - 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/_density.py b/src/py4vasp/_calculation/_density.py similarity index 100% rename from src/py4vasp/calculation/_density.py rename to src/py4vasp/_calculation/_density.py diff --git a/src/py4vasp/calculation/_dielectric_function.py b/src/py4vasp/_calculation/_dielectric_function.py similarity index 100% rename from src/py4vasp/calculation/_dielectric_function.py rename to src/py4vasp/_calculation/_dielectric_function.py diff --git a/src/py4vasp/calculation/_dielectric_tensor.py b/src/py4vasp/_calculation/_dielectric_tensor.py similarity index 100% rename from src/py4vasp/calculation/_dielectric_tensor.py rename to src/py4vasp/_calculation/_dielectric_tensor.py 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/_dos.py b/src/py4vasp/_calculation/_dos.py similarity index 100% rename from src/py4vasp/calculation/_dos.py rename to src/py4vasp/_calculation/_dos.py diff --git a/src/py4vasp/calculation/_elastic_modulus.py b/src/py4vasp/_calculation/_elastic_modulus.py similarity index 100% rename from src/py4vasp/calculation/_elastic_modulus.py rename to src/py4vasp/_calculation/_elastic_modulus.py diff --git a/src/py4vasp/calculation/_energy.py b/src/py4vasp/_calculation/_energy.py similarity index 100% rename from src/py4vasp/calculation/_energy.py rename to src/py4vasp/_calculation/_energy.py diff --git a/src/py4vasp/calculation/_fatband.py b/src/py4vasp/_calculation/_fatband.py similarity index 100% rename from src/py4vasp/calculation/_fatband.py rename to src/py4vasp/_calculation/_fatband.py diff --git a/src/py4vasp/calculation/_force.py b/src/py4vasp/_calculation/_force.py similarity index 100% rename from src/py4vasp/calculation/_force.py rename to src/py4vasp/_calculation/_force.py diff --git a/src/py4vasp/calculation/_force_constant.py b/src/py4vasp/_calculation/_force_constant.py similarity index 100% rename from src/py4vasp/calculation/_force_constant.py rename to src/py4vasp/_calculation/_force_constant.py diff --git a/src/py4vasp/calculation/_internal_strain.py b/src/py4vasp/_calculation/_internal_strain.py similarity index 100% rename from src/py4vasp/calculation/_internal_strain.py rename to src/py4vasp/_calculation/_internal_strain.py diff --git a/src/py4vasp/calculation/_magnetism.py b/src/py4vasp/_calculation/_magnetism.py similarity index 100% rename from src/py4vasp/calculation/_magnetism.py rename to src/py4vasp/_calculation/_magnetism.py diff --git a/src/py4vasp/calculation/_pair_correlation.py b/src/py4vasp/_calculation/_pair_correlation.py similarity index 100% rename from src/py4vasp/calculation/_pair_correlation.py rename to src/py4vasp/_calculation/_pair_correlation.py diff --git a/src/py4vasp/calculation/_partial_charge.py b/src/py4vasp/_calculation/_partial_charge.py similarity index 100% rename from src/py4vasp/calculation/_partial_charge.py rename to src/py4vasp/_calculation/_partial_charge.py diff --git a/src/py4vasp/calculation/_phonon.py b/src/py4vasp/_calculation/_phonon.py similarity index 100% rename from src/py4vasp/calculation/_phonon.py rename to src/py4vasp/_calculation/_phonon.py diff --git a/src/py4vasp/calculation/_phonon_band.py b/src/py4vasp/_calculation/_phonon_band.py similarity index 100% rename from src/py4vasp/calculation/_phonon_band.py rename to src/py4vasp/_calculation/_phonon_band.py diff --git a/src/py4vasp/calculation/_phonon_dos.py b/src/py4vasp/_calculation/_phonon_dos.py similarity index 100% rename from src/py4vasp/calculation/_phonon_dos.py rename to src/py4vasp/_calculation/_phonon_dos.py diff --git a/src/py4vasp/calculation/_piezoelectric_tensor.py b/src/py4vasp/_calculation/_piezoelectric_tensor.py similarity index 100% rename from src/py4vasp/calculation/_piezoelectric_tensor.py rename to src/py4vasp/_calculation/_piezoelectric_tensor.py diff --git a/src/py4vasp/calculation/_polarization.py b/src/py4vasp/_calculation/_polarization.py similarity index 100% rename from src/py4vasp/calculation/_polarization.py rename to src/py4vasp/_calculation/_polarization.py diff --git a/src/py4vasp/calculation/_potential.py b/src/py4vasp/_calculation/_potential.py similarity index 100% rename from src/py4vasp/calculation/_potential.py rename to src/py4vasp/_calculation/_potential.py diff --git a/src/py4vasp/calculation/_stress.py b/src/py4vasp/_calculation/_stress.py similarity index 100% rename from src/py4vasp/calculation/_stress.py rename to src/py4vasp/_calculation/_stress.py diff --git a/src/py4vasp/calculation/_system.py b/src/py4vasp/_calculation/_system.py similarity index 100% rename from src/py4vasp/calculation/_system.py rename to src/py4vasp/_calculation/_system.py diff --git a/src/py4vasp/calculation/_topology.py b/src/py4vasp/_calculation/_topology.py similarity index 94% rename from src/py4vasp/calculation/_topology.py rename to src/py4vasp/_calculation/_topology.py index 35612096..0c37193e 100644 --- a/src/py4vasp/calculation/_topology.py +++ b/src/py4vasp/_calculation/_topology.py @@ -6,8 +6,8 @@ from py4vasp import raw from py4vasp._util import check, convert, import_, select -from py4vasp.calculation import _base -from py4vasp.calculation._selection import Selection +from py4vasp._calculation import base +from py4vasp._calculation.selection import Selection mdtraj = import_.optional("mdtraj") pd = import_.optional("pandas") @@ -15,7 +15,7 @@ _subscript = "_" -class Topology(_base.Refinery): +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 @@ -31,17 +31,17 @@ def from_ase(cls, structure): """Generate a Topology from the given ase Atoms object.""" return cls.from_data(raw_topology_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. @@ -57,7 +57,7 @@ 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 @@ -68,7 +68,7 @@ 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.""" df = self.to_frame() @@ -78,7 +78,7 @@ 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. @@ -100,24 +100,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) diff --git a/src/py4vasp/calculation/_velocity.py b/src/py4vasp/_calculation/_velocity.py similarity index 100% rename from src/py4vasp/calculation/_velocity.py rename to src/py4vasp/_calculation/_velocity.py diff --git a/src/py4vasp/calculation/_workfunction.py b/src/py4vasp/_calculation/_workfunction.py similarity index 100% rename from src/py4vasp/calculation/_workfunction.py rename to src/py4vasp/_calculation/_workfunction.py 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..a9e375ec 100644 --- a/src/py4vasp/calculation/_band.py +++ b/src/py4vasp/_calculation/band.py @@ -5,13 +5,13 @@ from py4vasp import calculation from py4vasp._third_party import graph from py4vasp._util import check, documentation, import_ -from py4vasp.calculation import _base, _projector +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..6b1bc72d 100644 --- a/src/py4vasp/calculation/_bandgap.py +++ b/src/py4vasp/_calculation/bandgap.py @@ -8,7 +8,7 @@ from py4vasp import exception from py4vasp._third_party import graph from py4vasp._util import convert, documentation, select -from py4vasp.calculation import _base, _slice +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/_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..f8eaae5c 100644 --- a/src/py4vasp/calculation/_kpoint.py +++ b/src/py4vasp/_calculation/kpoint.py @@ -7,7 +7,7 @@ from py4vasp import exception from py4vasp._util import convert, documentation -from py4vasp.calculation import _base +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/_projector.py b/src/py4vasp/_calculation/projector.py similarity index 97% rename from src/py4vasp/calculation/_projector.py rename to src/py4vasp/_calculation/projector.py index 48f782f3..2b5032a7 100644 --- a/src/py4vasp/calculation/_projector.py +++ b/src/py4vasp/_calculation/projector.py @@ -6,8 +6,8 @@ from py4vasp import calculation, exception from py4vasp._util import convert, documentation, index, select -from py4vasp.calculation import _base -from py4vasp.calculation._selection import Selection +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,7 +79,7 @@ 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" @@ -87,7 +87,7 @@ def __str__(self): atoms: {", ".join(self._topology().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.""" @@ -191,7 +191,7 @@ def _raise_error_if_orbitals_missing(self): raise exception.IncorrectUsage(message) def _topology(self): - return calculation.topology.from_data(self._raw_data.topology) + return calculation._topology.from_data(self._raw_data.topology) def _init_dicts(self): if self._raw_data.orbital_types.is_none(): @@ -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. 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/_structure.py b/src/py4vasp/_calculation/structure.py similarity index 93% rename from src/py4vasp/calculation/_structure.py rename to src/py4vasp/_calculation/structure.py index 81510c3d..6286fc7d 100644 --- a/src/py4vasp/calculation/_structure.py +++ b/src/py4vasp/_calculation/structure.py @@ -8,7 +8,7 @@ from py4vasp import calculation, exception, raw from py4vasp._third_party import view from py4vasp._util import documentation, import_, reader -from py4vasp.calculation import _base, _slice, _topology +from py4vasp._calculation import base, slice_, _topology ase = import_.optional("ase") ase_io = import_.optional("ase.io") @@ -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 @@ -101,12 +101,12 @@ def from_ase(cls, structure): ) 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
", @@ -129,8 +129,8 @@ def _create_repr(self, format_=_Format()): ) 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. @@ -150,8 +150,8 @@ def to_dict(self): "names": self._topology().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). @@ -176,8 +176,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. @@ -220,8 +220,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 @@ -243,8 +243,8 @@ def to_mdtraj(self): 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 @@ -261,7 +261,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 @@ -273,7 +273,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. @@ -287,8 +287,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. @@ -301,8 +301,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. @@ -315,7 +315,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: @@ -323,7 +323,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: @@ -355,7 +355,7 @@ def _parse_supercell(self, supercell): raise exception.IncorrectUsage(message) def _topology(self): - return calculation.topology.from_data(self._raw_data.topology) + return calculation._topology.from_data(self._raw_data.topology) def _scale(self): if isinstance(self._raw_data.cell.scale, np.float_): @@ -380,7 +380,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/_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/_util/convert.py b/src/py4vasp/_util/convert.py index c97f2944..72e37979 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/calculation/__init__.py b/src/py4vasp/calculation/__init__.py deleted file mode 100644 index 2e6857ce..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", - "OSZICAR", - "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/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..e0e19bbd 100644 --- a/tests/calculation/test_base.py +++ b/tests/calculation/test_base.py @@ -12,7 +12,7 @@ from py4vasp import exception, raw from py4vasp._util import select -from py4vasp.calculation import _base +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 501aa89f..2e790981 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_module.py b/tests/calculation/test_default_calculation.py similarity index 68% rename from tests/calculation/test_module.py rename to tests/calculation/test_default_calculation.py index 52065114..f575ab76 100644 --- a/tests/calculation/test_module.py +++ b/tests/calculation/test_default_calculation.py @@ -19,6 +19,10 @@ def attribute_included(attr): return True -def test_nonexisting_attribute(): - with pytest.raises(exception.MissingAttribute): - calculation.does_not_exist +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_projector.py b/tests/calculation/test_projector.py index 4e9baee9..3eac1e86 100644 --- a/tests/calculation/test_projector.py +++ b/tests/calculation/test_projector.py @@ -7,7 +7,7 @@ from py4vasp import calculation, exception from py4vasp._util import select -from py4vasp.calculation._selection import Selection +from py4vasp._calculation.selection import Selection @pytest.fixture diff --git a/tests/calculation/test_slice_mixin.py b/tests/calculation/test_slice_mixin.py index 81402d21..223c319c 100644 --- a/tests/calculation/test_slice_mixin.py +++ b/tests/calculation/test_slice_mixin.py @@ -6,7 +6,7 @@ from py4vasp import exception from py4vasp._util import documentation -from py4vasp.calculation import _slice +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_structure.py b/tests/calculation/test_structure.py index 2ee5bbe7..9b7761ea 100644 --- a/tests/calculation/test_structure.py +++ b/tests/calculation/test_structure.py @@ -97,7 +97,7 @@ 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) + topology = calculation._topology.from_data(raw_structure.topology) structure.ref.elements = topology.elements() return structure diff --git a/tests/calculation/test_topology.py b/tests/calculation/test_topology.py index 9019fa92..3552ec59 100644 --- a/tests/calculation/test_topology.py +++ b/tests/calculation/test_topology.py @@ -4,7 +4,7 @@ from py4vasp import calculation, exception from py4vasp._util import import_, select -from py4vasp.calculation._selection import Selection +from py4vasp._calculation.selection import Selection ase = import_.optional("ase") pd = import_.optional("pandas") @@ -55,7 +55,7 @@ def test_number_atoms(self): def test_from_ase(self, not_core): structure = ase.Atoms("".join(self.elements)) - topology = calculation.topology.from_ase(structure) + topology = calculation._topology.from_ase(structure) assert topology.elements() == self.elements assert str(topology) == str(self.topology) @@ -71,7 +71,7 @@ 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.topology = calculation._topology.from_data(raw_data.topology("Sr2TiO4")) self.names = ["Sr_1", "Sr_2", "Ti_1", "O_1", "O_2", "O_3", "O_4"] self.elements = 2 * ["Sr"] + ["Ti"] + 4 * ["O"] @@ -101,7 +101,7 @@ 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) + self.topology = calculation._topology.from_data(raw_topology) 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"] @@ -124,4 +124,4 @@ 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) + check_factory_methods(calculation._topology, data) diff --git a/tests/util/test_convert.py b/tests/util/test_convert.py index 390acee5..9a3dcb66 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_complex, to_rgb, to_camelcase def test_text_to_string(): @@ -47,3 +47,11 @@ def test_hex_to_rgb(Assert): expected = np.array(colors) / 255 actual = np.array([to_rgb(color) for color in VASP_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" From 3056a240482804249a2fd7b790acc2c5ea5b443f Mon Sep 17 00:00:00 2001 From: Martin Schlipf Date: Fri, 7 Jun 2024 16:22:01 +0200 Subject: [PATCH 02/15] WIP: remove input files --- src/py4vasp/_calculation/_CONTCAR.py | 12 +-- src/py4vasp/_calculation/__init__.py | 26 +++--- .../_calculation/{_density.py => density.py} | 26 +++--- ...ric_function.py => dielectric_function.py} | 12 +-- ...lectric_tensor.py => dielectric_tensor.py} | 8 +- src/py4vasp/_calculation/{_dos.py => dos.py} | 24 ++--- ..._elastic_modulus.py => elastic_modulus.py} | 8 +- ..._OSZICAR.py => electronic_minimization.py} | 12 +-- .../_calculation/{_energy.py => energy.py} | 22 ++--- .../_calculation/{_fatband.py => fatband.py} | 10 +- .../_calculation/{_force.py => force.py} | 16 ++-- .../{_force_constant.py => force_constant.py} | 8 +- ..._internal_strain.py => internal_strain.py} | 8 +- .../{_magnetism.py => magnetism.py} | 32 +++---- src/py4vasp/_raw/data.py | 31 +++---- src/py4vasp/_raw/definition.py | 16 ++-- tests/calculation/test_contcar.py | 4 +- tests/calculation/test_dielectric_function.py | 4 +- tests/calculation/test_dos.py | 4 +- .../test_electronic_minimization.py | 92 +++++++++++++++++++ tests/calculation/test_energy.py | 4 +- tests/calculation/test_fatband.py | 2 +- tests/calculation/test_oszicar.py | 89 ------------------ tests/conftest.py | 12 +-- 24 files changed, 242 insertions(+), 240 deletions(-) rename src/py4vasp/_calculation/{_density.py => density.py} (97%) rename src/py4vasp/_calculation/{_dielectric_function.py => dielectric_function.py} (97%) rename src/py4vasp/_calculation/{_dielectric_tensor.py => dielectric_tensor.py} (95%) rename src/py4vasp/_calculation/{_dos.py => dos.py} (90%) rename src/py4vasp/_calculation/{_elastic_modulus.py => elastic_modulus.py} (95%) rename src/py4vasp/_calculation/{_OSZICAR.py => electronic_minimization.py} (96%) rename src/py4vasp/_calculation/{_energy.py => energy.py} (94%) rename src/py4vasp/_calculation/{_fatband.py => fatband.py} (92%) rename src/py4vasp/_calculation/{_force.py => force.py} (90%) rename src/py4vasp/_calculation/{_force_constant.py => force_constant.py} (94%) rename src/py4vasp/_calculation/{_internal_strain.py => internal_strain.py} (93%) rename src/py4vasp/_calculation/{_magnetism.py => magnetism.py} (93%) create mode 100644 tests/calculation/test_electronic_minimization.py delete mode 100644 tests/calculation/test_oszicar.py diff --git a/src/py4vasp/_calculation/_CONTCAR.py b/src/py4vasp/_calculation/_CONTCAR.py index e4eaccc1..a7679e72 100644 --- a/src/py4vasp/_calculation/_CONTCAR.py +++ b/src/py4vasp/_calculation/_CONTCAR.py @@ -3,10 +3,10 @@ from py4vasp import calculation from py4vasp._third_party import view from py4vasp._util import convert -from py4vasp.calculation import _base, _structure +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()) @@ -71,7 +71,7 @@ def _line_generator(self): yield from _ion_velocity_lines(self._raw_data.ion_velocities) def _topology(self): - return calculation.topology.from_data(self._raw_data.structure.topology) + return calculation._topology.from_data(self._raw_data.structure.topology) def _cell_lines(cell): diff --git a/src/py4vasp/_calculation/__init__.py b/src/py4vasp/_calculation/__init__.py index c05c4cc8..16130cce 100644 --- a/src/py4vasp/_calculation/__init__.py +++ b/src/py4vasp/_calculation/__init__.py @@ -49,20 +49,19 @@ class provides a more flexible interface with which you can determine the source "band", "bandgap", "born_effective_charge", - # "CONTCAR", - # "density", - # "dielectric_function", - # "dielectric_tensor", - # "dos", - # "elastic_modulus", - # "energy", - # "fatband", - # "force", - # "force_constant", - # "internal_strain", + "density", + "dielectric_function", + "dielectric_tensor", + "dos", + "elastic_modulus", + "electronic_minimization", + "energy", + "fatband", + "force", + "force_constant", + "internal_strain", "kpoint", - # "magnetism", - # "OSZICAR", + "magnetism", # "pair_correlation", # "partial_charge", # "phonon_band", @@ -76,6 +75,7 @@ class provides a more flexible interface with which you can determine the source # "system", # "velocity", # "workfunction", + "_CONTCAR", "_dispersion", "_topology", ) 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 19fb5c46..c3993fd0 100644 --- a/src/py4vasp/_calculation/_density.py +++ b/src/py4vasp/_calculation/density.py @@ -5,7 +5,7 @@ from py4vasp import _config, calculation, exception from py4vasp._third_party import graph, view from py4vasp._util import documentation, import_, index, select, slicing -from py4vasp.calculation import _base, _structure +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,11 @@ 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) + topology = calculation._topology.from_data(self._raw_data.structure.topology) if self._selection == "tau": name = "Kinetic energy" elif self.is_nonpolarized(): @@ -102,7 +102,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 +165,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 +198,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 +213,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. @@ -328,7 +328,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 @@ -390,7 +390,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. @@ -450,17 +450,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..9f26db88 100644 --- a/src/py4vasp/_calculation/_dielectric_function.py +++ b/src/py4vasp/_calculation/dielectric_function.py @@ -4,10 +4,10 @@ from py4vasp._third_party import graph from py4vasp._util import convert, index, select -from py4vasp.calculation import _base +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..e019b6f7 100644 --- a/src/py4vasp/_calculation/_dielectric_tensor.py +++ b/src/py4vasp/_calculation/dielectric_tensor.py @@ -2,10 +2,10 @@ # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) from py4vasp import exception from py4vasp._util import convert -from py4vasp.calculation import _base +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..0762ae6d 100644 --- a/src/py4vasp/_calculation/_dos.py +++ b/src/py4vasp/_calculation/dos.py @@ -3,13 +3,13 @@ from py4vasp import calculation from py4vasp._third_party import graph from py4vasp._util import documentation, import_ -from py4vasp.calculation import _base, _projector +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/_OSZICAR.py b/src/py4vasp/_calculation/electronic_minimization.py similarity index 96% rename from src/py4vasp/_calculation/_OSZICAR.py rename to src/py4vasp/_calculation/electronic_minimization.py index 8aa12e91..5d5148f4 100644 --- a/src/py4vasp/_calculation/_OSZICAR.py +++ b/src/py4vasp/_calculation/electronic_minimization.py @@ -5,10 +5,10 @@ from py4vasp import exception, raw from py4vasp._third_party import graph -from py4vasp.calculation import _base, _slice +from py4vasp._calculation import base, slice_ -class OSZICAR(_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 OSZICAR(_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..ddf3300c 100644 --- a/src/py4vasp/_calculation/_energy.py +++ b/src/py4vasp/_calculation/energy.py @@ -4,7 +4,7 @@ from py4vasp._third_party import graph from py4vasp._util import convert, documentation, index, select -from py4vasp.calculation import _base, _slice +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..699c5d15 100644 --- a/src/py4vasp/_calculation/_fatband.py +++ b/src/py4vasp/_calculation/fatband.py @@ -2,10 +2,10 @@ # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) from py4vasp import calculation from py4vasp._util import convert -from py4vasp.calculation import _base +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 39248c1d..b0351474 100644 --- a/src/py4vasp/_calculation/_force.py +++ b/src/py4vasp/_calculation/force.py @@ -5,11 +5,11 @@ from py4vasp import _config from py4vasp._third_party import view from py4vasp._util import documentation, reader -from py4vasp.calculation import _base, _slice, _structure +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/_magnetism.py b/src/py4vasp/_calculation/magnetism.py similarity index 93% rename from src/py4vasp/_calculation/_magnetism.py rename to src/py4vasp/_calculation/magnetism.py index 2c2152cf..23007e17 100644 --- a/src/py4vasp/_calculation/_magnetism.py +++ b/src/py4vasp/_calculation/magnetism.py @@ -5,7 +5,7 @@ from py4vasp import _config, exception from py4vasp._third_party import view from py4vasp._util import documentation -from py4vasp.calculation import _base, _slice, _structure +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. diff --git a/src/py4vasp/_raw/data.py b/src/py4vasp/_raw/data.py index 77d7866c..ea91e751 100644 --- a/src/py4vasp/_raw/data.py +++ b/src/py4vasp/_raw/data.py @@ -200,6 +200,21 @@ 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 +327,6 @@ class Magnetism: "Contains the orbital magnetization for all atoms" -@dataclasses.dataclass -class OSZICAR: - """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. diff --git a/src/py4vasp/_raw/definition.py b/src/py4vasp/_raw/definition.py index 6119fabc..c1269296 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.OSZICAR, - 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, 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_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_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 new file mode 100644 index 00000000..60579698 --- /dev/null +++ b/tests/calculation/test_electronic_minimization.py @@ -0,0 +1,92 @@ +# Copyright © VASP Software GmbH, +# Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + +import types + +import numpy as np +import pytest + +from py4vasp import calculation, exception + + +@pytest.fixture +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]) + electronic_minimization.ref.string_rep = str(string_rep) + return electronic_minimization + + +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) + Assert.allclose(actual["deps"], expected.deps) + Assert.allclose(actual["ncg"], expected.ncg) + Assert.allclose(actual["rms"], expected.rms) + Assert.allclose(actual["rms(c)"], expected.rmsc) + + +@pytest.mark.parametrize( + "quantity_name", ["N", "E", "dE", "deps", "ncg", "rms", "rms(c)"] +) +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(electronic_minimization): + with pytest.raises(exception.RefinementError): + electronic_minimization.read("forces") + + +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) + Assert.allclose(actual["deps"], expected.deps) + Assert.allclose(actual["ncg"], expected.ncg) + Assert.allclose(actual["rms"], expected.rms) + Assert.allclose(actual["rms(c)"], expected.rmsc) + + +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, electronic_minimization.ref.N) + Assert.allclose(graph.series[0].y, electronic_minimization.ref.E) + + +def test_print(electronic_minimization, format_): + actual, _ = format_(electronic_minimization) + assert actual["text/plain"] == electronic_minimization.ref.string_rep + + +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_oszicar.py b/tests/calculation/test_oszicar.py deleted file mode 100644 index 4608b056..00000000 --- a/tests/calculation/test_oszicar.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright © VASP Software GmbH, -# Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - -import types - -import numpy as np -import pytest - -from py4vasp import calculation, exception - - -@pytest.fixture -def OSZICAR(raw_data): - raw_oszicar = raw_data.OSZICAR() - oszicar = calculation.OSZICAR.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]] - 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 - - -def test_read(OSZICAR, Assert): - actual = OSZICAR.read() - expected = OSZICAR.ref - Assert.allclose(actual["N"], expected.N) - Assert.allclose(actual["E"], expected.E) - Assert.allclose(actual["dE"], expected.dE) - Assert.allclose(actual["deps"], expected.deps) - Assert.allclose(actual["ncg"], expected.ncg) - Assert.allclose(actual["rms"], expected.rms) - Assert.allclose(actual["rms(c)"], expected.rmsc) - - -@pytest.mark.parametrize( - "quantity_name", ["N", "E", "dE", "deps", "ncg", "rms", "rms(c)"] -) -def test_read_selection(quantity_name, OSZICAR, Assert): - actual = OSZICAR.read(quantity_name) - expected = getattr(OSZICAR.ref, quantity_name.replace("(", "").replace(")", "")) - Assert.allclose(actual[quantity_name], expected) - - -def test_read_incorrect_selection(OSZICAR): - with pytest.raises(exception.RefinementError): - OSZICAR.read("forces") - - -def test_slice(OSZICAR, Assert): - actual = OSZICAR[0:1].read() - expected = OSZICAR.ref - Assert.allclose(actual["N"], expected.N) - Assert.allclose(actual["E"], expected.E) - Assert.allclose(actual["dE"], expected.dE) - Assert.allclose(actual["deps"], expected.deps) - Assert.allclose(actual["ncg"], expected.ncg) - Assert.allclose(actual["rms"], expected.rms) - Assert.allclose(actual["rms(c)"], expected.rmsc) - - -def test_plot(OSZICAR, Assert): - graph = OSZICAR.plot() - assert graph.xlabel == "Iteration number" - assert graph.ylabel == "E" - assert len(graph.series) == 1 - Assert.allclose(graph.series[0].x, OSZICAR.ref.N) - Assert.allclose(graph.series[0].y, OSZICAR.ref.E) - - -def test_print(OSZICAR, format_): - actual, _ = format_(OSZICAR) - assert actual["text/plain"] == OSZICAR.ref.string_rep - - -def test_is_converged(OSZICAR): - actual = OSZICAR.is_converged() - expected = OSZICAR.ref.is_elmin_converged - assert actual == expected diff --git a/tests/conftest.py b/tests/conftest.py index 1458b8bb..334c5af7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -131,10 +131,6 @@ def CONTCAR(selection): else: raise exception.NotImplemented() - @staticmethod - def OSZICAR(selection=None): - return _example_OSZICAR() - @staticmethod def density(selection): parts = selection.split() @@ -190,6 +186,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": @@ -665,7 +665,7 @@ def _Sr2TiO4_cell(): ) -def _example_OSZICAR(): +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)) @@ -676,7 +676,7 @@ def _example_OSZICAR(): convergence_data = raw.VaspData(convergence_data) label = raw.VaspData([b"N", b"E", b"dE", b"deps", b"ncg", b"rms", b"rms(c)"]) is_elmin_converged = [0] - return raw.OSZICAR( + return raw.ElectronicMinimization( convergence_data=convergence_data, label=label, is_elmin_converged=is_elmin_converged, From d737e52ca66fded7e6c3222e24da79e97c6e66be Mon Sep 17 00:00:00 2001 From: Martin Schlipf Date: Mon, 17 Jun 2024 14:32:16 +0200 Subject: [PATCH 03/15] Fix calculations tests --- src/py4vasp/__init__.py | 2 +- src/py4vasp/_calculation/_CONTCAR.py | 2 +- src/py4vasp/_calculation/__init__.py | 28 +-- src/py4vasp/_calculation/_topology.py | 2 +- src/py4vasp/_calculation/band.py | 2 +- src/py4vasp/_calculation/bandgap.py | 2 +- src/py4vasp/_calculation/density.py | 2 +- .../_calculation/dielectric_function.py | 2 +- src/py4vasp/_calculation/dielectric_tensor.py | 2 +- src/py4vasp/_calculation/dos.py | 2 +- .../_calculation/electronic_minimization.py | 2 +- src/py4vasp/_calculation/energy.py | 2 +- src/py4vasp/_calculation/fatband.py | 2 +- src/py4vasp/_calculation/force.py | 2 +- src/py4vasp/_calculation/kpoint.py | 2 +- src/py4vasp/_calculation/magnetism.py | 2 +- ...air_correlation.py => pair_correlation.py} | 16 +- ...{_partial_charge.py => partial_density.py} | 20 +- .../_calculation/{_phonon.py => phonon.py} | 6 +- .../{_phonon_band.py => phonon_band.py} | 14 +- .../{_phonon_dos.py => phonon_dos.py} | 14 +- ...tric_tensor.py => piezoelectric_tensor.py} | 8 +- .../{_polarization.py => polarization.py} | 8 +- .../{_potential.py => potential.py} | 12 +- src/py4vasp/_calculation/projector.py | 2 +- .../_calculation/{_stress.py => stress.py} | 12 +- src/py4vasp/_calculation/structure.py | 2 +- .../_calculation/{_system.py => system.py} | 8 +- .../{_velocity.py => velocity.py} | 16 +- .../{_workfunction.py => workfunction.py} | 23 +-- src/py4vasp/_raw/data.py | 3 +- src/py4vasp/_raw/definition.py | 2 +- tests/calculation/test_base.py | 2 +- tests/calculation/test_default_calculation.py | 2 +- tests/calculation/test_dispersion.py | 4 +- tests/calculation/test_pair_correlation.py | 4 +- ...tial_charge.py => test_partial_density.py} | 176 +++++++++--------- tests/calculation/test_phonon_band.py | 6 +- tests/calculation/test_phonon_dos.py | 4 +- tests/calculation/test_projector.py | 2 +- tests/calculation/test_repr.py | 8 +- tests/calculation/test_slice_mixin.py | 2 +- tests/calculation/test_topology.py | 2 +- tests/calculation/test_workfunction.py | 18 +- tests/conftest.py | 8 +- tests/util/test_convert.py | 2 +- 46 files changed, 236 insertions(+), 228 deletions(-) rename src/py4vasp/_calculation/{_pair_correlation.py => pair_correlation.py} (91%) rename src/py4vasp/_calculation/{_partial_charge.py => partial_density.py} (98%) rename src/py4vasp/_calculation/{_phonon.py => phonon.py} (93%) rename src/py4vasp/_calculation/{_phonon_band.py => phonon_band.py} (91%) rename src/py4vasp/_calculation/{_phonon_dos.py => phonon_dos.py} (91%) rename src/py4vasp/_calculation/{_piezoelectric_tensor.py => piezoelectric_tensor.py} (95%) rename src/py4vasp/_calculation/{_polarization.py => polarization.py} (93%) rename src/py4vasp/_calculation/{_potential.py => potential.py} (96%) rename src/py4vasp/_calculation/{_stress.py => stress.py} (91%) rename src/py4vasp/_calculation/{_system.py => system.py} (79%) rename src/py4vasp/_calculation/{_velocity.py => velocity.py} (89%) rename src/py4vasp/_calculation/{_workfunction.py => workfunction.py} (82%) rename tests/calculation/{test_partial_charge.py => test_partial_density.py} (59%) diff --git a/src/py4vasp/__init__.py b/src/py4vasp/__init__.py index 6bdbeb73..efe013c6 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._calculation import Calculation, calculation from py4vasp._calculations import Calculations from py4vasp._third_party.graph import plot from py4vasp._third_party.interactive import set_error_handling -from py4vasp._calculation import calculation, Calculation __version__ = "0.9.0" set_error_handling("Minimal") diff --git a/src/py4vasp/_calculation/_CONTCAR.py b/src/py4vasp/_calculation/_CONTCAR.py index a7679e72..0da5510a 100644 --- a/src/py4vasp/_calculation/_CONTCAR.py +++ b/src/py4vasp/_calculation/_CONTCAR.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 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): diff --git a/src/py4vasp/_calculation/__init__.py b/src/py4vasp/_calculation/__init__.py index 16130cce..10cc7ce1 100644 --- a/src/py4vasp/_calculation/__init__.py +++ b/src/py4vasp/_calculation/__init__.py @@ -62,19 +62,19 @@ class provides a more flexible interface with which you can determine the source "internal_strain", "kpoint", "magnetism", - # "pair_correlation", - # "partial_charge", - # "phonon_band", - # "phonon_dos", - # "piezoelectric_tensor", - # "polarization", - # "potential", + "pair_correlation", + "partial_density", + "phonon_band", + "phonon_dos", + "piezoelectric_tensor", + "polarization", + "potential", "projector", - # "stress", + "stress", "structure", - # "system", - # "velocity", - # "workfunction", + "system", + "velocity", + "workfunction", "_CONTCAR", "_dispersion", "_topology", @@ -230,19 +230,25 @@ def _add_to_documentation(calc, name): calc.__doc__ += f" ~py4vasp.calculation.{name}\n " return calc + def _add_input_files(calc): 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/_topology.py b/src/py4vasp/_calculation/_topology.py index 0c37193e..6ba66af8 100644 --- a/src/py4vasp/_calculation/_topology.py +++ b/src/py4vasp/_calculation/_topology.py @@ -5,9 +5,9 @@ import numpy as np from py4vasp import raw -from py4vasp._util import check, convert, import_, select from py4vasp._calculation import base from py4vasp._calculation.selection import Selection +from py4vasp._util import check, convert, import_, select mdtraj = import_.optional("mdtraj") pd = import_.optional("pandas") diff --git a/src/py4vasp/_calculation/band.py b/src/py4vasp/_calculation/band.py index a9e375ec..70284ce5 100644 --- a/src/py4vasp/_calculation/band.py +++ b/src/py4vasp/_calculation/band.py @@ -3,9 +3,9 @@ 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") diff --git a/src/py4vasp/_calculation/bandgap.py b/src/py4vasp/_calculation/bandgap.py index 6b1bc72d..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): diff --git a/src/py4vasp/_calculation/density.py b/src/py4vasp/_calculation/density.py index c3993fd0..d2eb5274 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") diff --git a/src/py4vasp/_calculation/dielectric_function.py b/src/py4vasp/_calculation/dielectric_function.py index 9f26db88..c232c178 100644 --- a/src/py4vasp/_calculation/dielectric_function.py +++ b/src/py4vasp/_calculation/dielectric_function.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 from py4vasp._third_party import graph from py4vasp._util import convert, index, select -from py4vasp._calculation import base class DielectricFunction(base.Refinery, graph.Mixin): diff --git a/src/py4vasp/_calculation/dielectric_tensor.py b/src/py4vasp/_calculation/dielectric_tensor.py index e019b6f7..e841a325 100644 --- a/src/py4vasp/_calculation/dielectric_tensor.py +++ b/src/py4vasp/_calculation/dielectric_tensor.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 exception -from py4vasp._util import convert from py4vasp._calculation import base +from py4vasp._util import convert class DielectricTensor(base.Refinery): diff --git a/src/py4vasp/_calculation/dos.py b/src/py4vasp/_calculation/dos.py index 0762ae6d..04a3e331 100644 --- a/src/py4vasp/_calculation/dos.py +++ b/src/py4vasp/_calculation/dos.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 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") diff --git a/src/py4vasp/_calculation/electronic_minimization.py b/src/py4vasp/_calculation/electronic_minimization.py index 5d5148f4..d5e2acc8 100644 --- a/src/py4vasp/_calculation/electronic_minimization.py +++ b/src/py4vasp/_calculation/electronic_minimization.py @@ -4,8 +4,8 @@ import numpy as np from py4vasp import exception, raw -from py4vasp._third_party import graph from py4vasp._calculation import base, slice_ +from py4vasp._third_party import graph class ElectronicMinimization(slice_.Mixin, base.Refinery, graph.Mixin): diff --git a/src/py4vasp/_calculation/energy.py b/src/py4vasp/_calculation/energy.py index ddf3300c..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): diff --git a/src/py4vasp/_calculation/fatband.py b/src/py4vasp/_calculation/fatband.py index 699c5d15..ef6dafd0 100644 --- a/src/py4vasp/_calculation/fatband.py +++ b/src/py4vasp/_calculation/fatband.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._util import convert from py4vasp._calculation import base +from py4vasp._util import convert class Fatband(base.Refinery): diff --git a/src/py4vasp/_calculation/force.py b/src/py4vasp/_calculation/force.py index b0351474..283dfd66 100644 --- a/src/py4vasp/_calculation/force.py +++ b/src/py4vasp/_calculation/force.py @@ -3,9 +3,9 @@ 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")) diff --git a/src/py4vasp/_calculation/kpoint.py b/src/py4vasp/_calculation/kpoint.py index f8eaae5c..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._util import convert, documentation from py4vasp._calculation import base +from py4vasp._util import convert, documentation _kpoints_selection = """\ selection : str, optional diff --git a/src/py4vasp/_calculation/magnetism.py b/src/py4vasp/_calculation/magnetism.py index 23007e17..09e5159c 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 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 98% rename from src/py4vasp/_calculation/_partial_charge.py rename to src/py4vasp/_calculation/partial_density.py index 6841a879..28d5872c 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,7 +70,7 @@ 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""" @@ -80,11 +80,11 @@ def __str__(self): {"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", @@ -290,7 +290,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. @@ -324,7 +324,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. @@ -348,7 +348,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 93% rename from src/py4vasp/_calculation/_phonon.py rename to src/py4vasp/_calculation/phonon.py index c65a13bc..522eb85d 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() @@ -37,7 +37,7 @@ def selections(self): } def _topology(self): - return calculation.topology.from_data(self._raw_data.topology) + return calculation._topology.from_data(self._raw_data.topology) def _init_atom_dict(self): return { diff --git a/src/py4vasp/_calculation/_phonon_band.py b/src/py4vasp/_calculation/phonon_band.py similarity index 91% rename from src/py4vasp/_calculation/_phonon_band.py rename to src/py4vasp/_calculation/phonon_band.py index 0f02ce8d..9a5d4017 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()}""" - @_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 91% rename from src/py4vasp/_calculation/_phonon_dos.py rename to src/py4vasp/_calculation/phonon_dos.py index 27f08a0a..289e2a63 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,7 +23,7 @@ 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() @@ -32,8 +32,8 @@ def __str__(self): {3 * topology.number_atoms()} modes {topology}""" - @_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 96% rename from src/py4vasp/_calculation/_potential.py rename to src/py4vasp/_calculation/potential.py index b1a50800..65cbd024 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,7 +39,7 @@ def __str__(self): description = "noncollinear potential:" else: description = "nonpolarized potential:" - topology = calculation.topology.from_data(self._raw_data.structure.topology) + topology = calculation._topology.from_data(self._raw_data.structure.topology) structure = f"structure: {topology}" grid = f"grid: {potential.shape[3]}, {potential.shape[2]}, {potential.shape[1]}" available = "available: " + ", ".join( @@ -47,7 +47,7 @@ def __str__(self): ) 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 +78,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 index 2b5032a7..01367e00 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._util import convert, documentation, index, select from py4vasp._calculation import base from py4vasp._calculation.selection import Selection +from py4vasp._util import convert, documentation, index, select selection_doc = """\ selection : str 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 index 6286fc7d..413deaa1 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 _topology, 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") 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 1426b9f7..7cb012af 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/_raw/data.py b/src/py4vasp/_raw/data.py index ea91e751..db992385 100644 --- a/src/py4vasp/_raw/data.py +++ b/src/py4vasp/_raw/data.py @@ -215,6 +215,7 @@ class ElectronicMinimization: is_elmin_converged: VaspData "Is the electronic minimization step converged?" + @dataclasses.dataclass class Energy: """Various energies during ionic relaxation or MD simulation. @@ -343,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.""" diff --git a/src/py4vasp/_raw/definition.py b/src/py4vasp/_raw/definition.py index c1269296..67f43878 100644 --- a/src/py4vasp/_raw/definition.py +++ b/src/py4vasp/_raw/definition.py @@ -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", diff --git a/tests/calculation/test_base.py b/tests/calculation/test_base.py index e0e19bbd..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._util import select from py4vasp._calculation import base +from py4vasp._util import select from .conftest import SELECTION diff --git a/tests/calculation/test_default_calculation.py b/tests/calculation/test_default_calculation.py index f575ab76..a69eee4c 100644 --- a/tests/calculation/test_default_calculation.py +++ b/tests/calculation/test_default_calculation.py @@ -21,7 +21,7 @@ def attribute_included(attr): def test_assigning_to_input_file(tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) - expected ="SYSTEM = demo INCAR file" + expected = "SYSTEM = demo INCAR file" calculation.INCAR = expected with open("INCAR", "r") as file: actual = file.read() 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_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 59% rename from tests/calculation/test_partial_charge.py rename to tests/calculation/test_partial_density.py index 4d8770a8..7759da76 100644 --- a/tests/calculation/test_partial_charge.py +++ b/tests/calculation/test_partial_density.py @@ -34,48 +34,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" ) @@ -85,119 +85,121 @@ 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): - actual = PartialCharge.read() - expected = PartialCharge.ref +def test_read(PartialDensity, Assert): + 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): - actual = PartialCharge._topology() - expected = str(PartialCharge.ref.structure._topology()) +def test_topology(PartialDensity): + actual = PartialDensity._topology() + expected = str(PartialDensity.ref.structure._topology()) assert actual == expected -def test_bands(PartialCharge, Assert): - actual = PartialCharge.bands() - expected = PartialCharge.ref.bands +def test_bands(PartialDensity, Assert): + actual = PartialDensity.bands() + expected = PartialDensity.ref.bands Assert.allclose(actual, expected) -def test_kpoints(PartialCharge, Assert): - actual = PartialCharge.kpoints() - expected = PartialCharge.ref.kpoints +def test_kpoints(PartialDensity, Assert): + actual = PartialDensity.kpoints() + expected = PartialDensity.ref.kpoints Assert.allclose(actual, expected) -def test_grid(PartialCharge, Assert): - actual = PartialCharge.grid() - expected = PartialCharge.ref.grid +def test_grid(PartialDensity, Assert): + actual = PartialDensity.grid() + expected = PartialDensity.ref.grid Assert.allclose(actual, expected) -def test_non_split_to_numpy(PolarizedNonSplitPartialCharge, Assert): - actual = PolarizedNonSplitPartialCharge.to_numpy("total") - expected = PolarizedNonSplitPartialCharge.ref.partial_charge +def test_non_split_to_numpy(PolarizedNonSplitPartialDensity, Assert): + 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): - bands = PolarizedAllSplitPartialCharge.ref.bands - kpoints = PolarizedAllSplitPartialCharge.ref.kpoints +def test_split_to_numpy(PolarizedAllSplitPartialDensity, Assert): + 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): - actual = NonSplitPartialCharge.to_numpy(selection=spin) - expected = NonSplitPartialCharge.ref.partial_charge +def test_non_polarized_to_numpy(NonSplitPartialDensity, spin, Assert): + 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): - bands = NonPolarizedBandSplitPartialCharge.ref.bands +def test_split_bands_to_numpy(NonPolarizedBandSplitPartialDensity, spin, Assert): + 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): +def test_to_stm_split(PolarizedAllSplitPartialDensity): 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): - actual = NonSplitPartialCharge +def test_to_stm_nonsplit_tip_to_high(NonSplitPartialDensity): + 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. @@ -207,44 +209,44 @@ def test_to_stm_nonsplit_tip_to_high(NonSplitPartialCharge): def test_to_stm_nonsplit_not_orthogonal_no_vacuum( - PolarizedNonSplitPartialChargeSr2TiO4, + PolarizedNonSplitPartialDensitySr2TiO4, ): 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): +def test_to_stm_wrong_spin_nonsplit(PolarizedNonSplitPartialDensity): 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): +def test_to_stm_wrong_mode(PolarizedNonSplitPartialDensity): 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): +def test_wrong_vacuum_direction(NonSplitPartialDensityNi_100): 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) @@ -261,16 +263,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) @@ -287,16 +289,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) @@ -311,8 +313,8 @@ def test_to_stm_nonsplit_constant_current_non_ortho( assert f"{current:.2f}" in actual.title -def test_stm_default_settings(PolarizedNonSplitPartialCharge): - actual = dataclasses.asdict(PolarizedNonSplitPartialCharge.stm_settings) +def test_stm_default_settings(PolarizedNonSplitPartialDensity): + actual = dataclasses.asdict(PolarizedNonSplitPartialDensity.stm_settings) defaults = { "sigma_xy": 4.0, "sigma_z": 4.0, @@ -324,5 +326,5 @@ def test_stm_default_settings(PolarizedNonSplitPartialCharge): 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..857c88ac 100644 --- a/tests/calculation/test_phonon_band.py +++ b/tests/calculation/test_phonon_band.py @@ -19,7 +19,7 @@ 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) + band.ref.topology = calculation._topology.from_data(raw_band.topology) Sr = slice(0, 2) band.ref.Sr = np.sum(np.abs(band.ref.modes[:, :, Sr, :]), axis=(2, 3)) Ti = 2 @@ -88,7 +88,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 +104,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 3eac1e86..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._util import select from py4vasp._calculation.selection import Selection +from py4vasp._util import select @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 223c319c..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._util import documentation from py4vasp._calculation import slice_ +from py4vasp._util import documentation class Other: diff --git a/tests/calculation/test_topology.py b/tests/calculation/test_topology.py index 3552ec59..be7995b7 100644 --- a/tests/calculation/test_topology.py +++ b/tests/calculation/test_topology.py @@ -3,8 +3,8 @@ import pytest from py4vasp import calculation, exception -from py4vasp._util import import_, select from py4vasp._calculation.selection import Selection +from py4vasp._util import import_, select ase = import_.optional("ase") pd = import_.optional("pandas") 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 334c5af7..121dcfcf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -327,8 +327,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 @@ -683,7 +683,7 @@ def _electronic_minimization(): ) -def _partial_charge(selection): +def _partial_density(selection): grid_dim = grid_dimensions if "CaAs3_110" in selection: structure = _CaAs3_110_structure() @@ -713,7 +713,7 @@ def _partial_charge(selection): random_charge = raw.VaspData( np.random.rand(len(kpoints), len(bands), spin_dimension, *grid_dim) ) - return raw.PartialCharge( + return raw.PartialDensity( structure=structure, bands=bands, kpoints=kpoints, diff --git a/tests/util/test_convert.py b/tests/util/test_convert.py index 9a3dcb66..44aab33c 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, to_camelcase +from py4vasp._util.convert import text_to_string, to_camelcase, to_complex, to_rgb def test_text_to_string(): From 1740d6c96e2efb184f48d5ef3e6f7c7176fbdec9 Mon Sep 17 00:00:00 2001 From: Martin Schlipf Date: Mon, 17 Jun 2024 15:39:17 +0200 Subject: [PATCH 04/15] Rename Calculations -> Batch --- src/py4vasp/__init__.py | 2 +- src/py4vasp/_analysis/mlff.py | 32 ++++----- src/py4vasp/{_calculations.py => _batch.py} | 16 ++--- tests/analysis/test_mlff.py | 10 +-- .../test_batch.py} | 70 +++++++++---------- 5 files changed, 60 insertions(+), 70 deletions(-) rename src/py4vasp/{_calculations.py => _batch.py} (89%) rename tests/{test_calculations.py => batch/test_batch.py} (71%) diff --git a/src/py4vasp/__init__.py b/src/py4vasp/__init__.py index efe013c6..aae105d1 100644 --- a/src/py4vasp/__init__.py +++ b/src/py4vasp/__init__.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._analysis.mlff import MLFFErrorAnalysis +from py4vasp._batch import Batch from py4vasp._calculation import Calculation, calculation -from py4vasp._calculations import Calculations from py4vasp._third_party.graph import plot from py4vasp._third_party.interactive import set_error_handling 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/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) From aee60164a298f113d26d632d3b28264e5b298e40 Mon Sep 17 00:00:00 2001 From: Martin Schlipf Date: Mon, 17 Jun 2024 17:53:04 +0200 Subject: [PATCH 05/15] Prevent numpy 2 installation --- core/pyproject.toml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/pyproject.toml b/core/pyproject.toml index ee78a837..15063423 100644 --- a/core/pyproject.toml +++ b/core/pyproject.toml @@ -25,7 +25,7 @@ repository = "https://github.com/vasp-dev/py4vasp" [tool.poetry.dependencies] python = ">=3.9" -numpy = ">=1.23" +numpy = "^1.23" h5py = ">=3.7.0" [tool.poetry.group.dev.dependencies] diff --git a/pyproject.toml b/pyproject.toml index f95412e6..7fd1d047 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ repository = "https://github.com/vasp-dev/py4vasp" [tool.poetry.dependencies] python = ">=3.9" -numpy = ">=1.23" +numpy = "^1.23" h5py = ">=3.7.0" pandas = ">=1.4.3" nglview = ">=3.0.5" From ac9f063ccd2823bb0018642a6dc4d3b9fc49dd8b Mon Sep 17 00:00:00 2001 From: Martin Schlipf Date: Mon, 17 Jun 2024 17:59:04 +0200 Subject: [PATCH 06/15] Update lock files --- core/poetry.lock | 32 ++++++++++++++++---------------- poetry.lock | 9 ++++----- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/core/poetry.lock b/core/poetry.lock index 1d94d801..a51f3034 100644 --- a/core/poetry.lock +++ b/core/poetry.lock @@ -1,14 +1,14 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. [[package]] name = "astroid" -version = "3.1.0" +version = "3.2.1" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.8.0" files = [ - {file = "astroid-3.1.0-py3-none-any.whl", hash = "sha256:951798f922990137ac090c53af473db7ab4e70c770e6d7fae0cec59f74411819"}, - {file = "astroid-3.1.0.tar.gz", hash = "sha256:ac248253bfa4bd924a0de213707e7ebeeb3138abeb48d798784ead1e56d419d4"}, + {file = "astroid-3.2.1-py3-none-any.whl", hash = "sha256:b452064132234819f023b94f4bd045b250ea0009f372b4377cfcd87f10806ca5"}, + {file = "astroid-3.2.1.tar.gz", hash = "sha256:902564b36796ba1eab3ad2c7a694861fbd926f574d5dbb5fa1d86778a2ba2d91"}, ] [package.dependencies] @@ -235,13 +235,13 @@ numpy = ">=1.17.3" [[package]] name = "hypothesis" -version = "6.101.0" +version = "6.102.4" description = "A library for property-based testing" optional = false python-versions = ">=3.8" files = [ - {file = "hypothesis-6.101.0-py3-none-any.whl", hash = "sha256:3aeeda59bc7878aa2ad066f71925ab9296a148f7fb7d5395a47795035283dec2"}, - {file = "hypothesis-6.101.0.tar.gz", hash = "sha256:b43242fc2b672b26c81688876976c1c2cc44116050ca12f60eb2671b75eacd45"}, + {file = "hypothesis-6.102.4-py3-none-any.whl", hash = "sha256:013df31b04a4daede13756f497e60e451963d86f426395a79f99c5d692919bbd"}, + {file = "hypothesis-6.102.4.tar.gz", hash = "sha256:59b4d144346d5cffb482cc1bafbd21b13ff31608e8c4b3e4630339aee3e87763"}, ] [package.dependencies] @@ -382,13 +382,13 @@ files = [ [[package]] name = "platformdirs" -version = "4.2.1" +version = "4.2.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.1-py3-none-any.whl", hash = "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"}, - {file = "platformdirs-4.2.1.tar.gz", hash = "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf"}, + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, ] [package.extras] @@ -413,22 +413,22 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pylint" -version = "3.1.0" +version = "3.2.0" description = "python code static checker" optional = false python-versions = ">=3.8.0" files = [ - {file = "pylint-3.1.0-py3-none-any.whl", hash = "sha256:507a5b60953874766d8a366e8e8c7af63e058b26345cfcb5f91f89d987fd6b74"}, - {file = "pylint-3.1.0.tar.gz", hash = "sha256:6a69beb4a6f63debebaab0a3477ecd0f559aa726af4954fc948c51f7a2549e23"}, + {file = "pylint-3.2.0-py3-none-any.whl", hash = "sha256:9f20c05398520474dac03d7abb21ab93181f91d4c110e1e0b32bc0d016c34fa4"}, + {file = "pylint-3.2.0.tar.gz", hash = "sha256:ad8baf17c8ea5502f23ae38d7c1b7ec78bd865ce34af9a0b986282e2611a8ff2"}, ] [package.dependencies] -astroid = ">=3.1.0,<=3.2.0-dev0" +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" @@ -528,4 +528,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.9" -content-hash = "a19e45d9772b6787259404e7542594e210af50e4ff329eafdc8a2363dfa71dbe" +content-hash = "0e9853afabcd6b1e258bb9c2b31aa7a469829940a2f195fefcd625029743416f" diff --git a/poetry.lock b/poetry.lock index 826aa2cf..eb5056a1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. [[package]] name = "alabaster" @@ -2124,8 +2124,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" @@ -2479,8 +2479,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" @@ -2646,7 +2646,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"}, @@ -3523,4 +3522,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more [metadata] lock-version = "2.0" python-versions = ">=3.9" -content-hash = "1dbe1ec8147ca66d47d5e20b43f33bd2a8941507b9aa3b2a03e6e305968a2dc8" +content-hash = "77a96b6fd125ce33f11c5361874255a95af49da196d543a3de0be1af5b179172" From f0c8a1031d022ae3a948878adbad8efc59c5b9a1 Mon Sep 17 00:00:00 2001 From: Martin Schlipf Date: Mon, 17 Jun 2024 23:17:26 +0200 Subject: [PATCH 07/15] VASP does not write charge component for orbital moments --- src/py4vasp/_calculation/magnetism.py | 4 ++-- tests/calculation/test_magnetism.py | 2 +- tests/conftest.py | 5 ++++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/py4vasp/_calculation/magnetism.py b/src/py4vasp/_calculation/magnetism.py index 09e5159c..bef02311 100644 --- a/src/py4vasp/_calculation/magnetism.py +++ b/src/py4vasp/_calculation/magnetism.py @@ -230,7 +230,7 @@ def _collinear_moments(self): 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:] + orbital_moments = self._raw_data.orbital_moments[self._steps] else: orbital_moments = np.zeros_like(spin_moments) if selection == "orbital": @@ -246,7 +246,7 @@ 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:] + orbital_moments = self._raw_data.orbital_moments[self._steps] direction_axis = 1 if spin_moments.ndim == 4 else 0 return { "spin_moments": np.moveaxis(spin_moments, direction_axis, -1), diff --git a/tests/calculation/test_magnetism.py b/tests/calculation/test_magnetism.py index 288e65c3..c0e8427e 100644 --- a/tests/calculation/test_magnetism.py +++ b/tests/calculation/test_magnetism.py @@ -63,7 +63,7 @@ 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.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/conftest.py b/tests/conftest.py index 121dcfcf..4ec23f79 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -543,7 +543,10 @@ 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_component = magnetism.spin_moments[:, 1:] + magnetism.orbital_moments = _make_data(np.sqrt(remove_charge_component)) + print("spin_moments", magnetism.spin_moments.shape) + print("orb_moments", magnetism.orbital_moments.shape) return magnetism From 047571c5d9443c8169d205c24d6e8c64ca8e40c6 Mon Sep 17 00:00:00 2001 From: Martin Schlipf Date: Mon, 17 Jun 2024 23:42:56 +0200 Subject: [PATCH 08/15] Fix s component not present in orbital moments --- src/py4vasp/_calculation/magnetism.py | 21 ++++++++++++++------- tests/calculation/test_magnetism.py | 3 ++- tests/conftest.py | 6 ++---- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/py4vasp/_calculation/magnetism.py b/src/py4vasp/_calculation/magnetism.py index bef02311..1964cea9 100644 --- a/src/py4vasp/_calculation/magnetism.py +++ b/src/py4vasp/_calculation/magnetism.py @@ -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] - 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] + 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/tests/calculation/test_magnetism.py b/tests/calculation/test_magnetism.py index c0e8427e..fa8fd477 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, 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/conftest.py b/tests/conftest.py index 4ec23f79..89148030 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -543,10 +543,8 @@ def _magnetism(selection): spin_moments=_make_data(np.arange(np.prod(shape)).reshape(shape)), ) if selection == "orbital_moments": - remove_charge_component = magnetism.spin_moments[:, 1:] - magnetism.orbital_moments = _make_data(np.sqrt(remove_charge_component)) - print("spin_moments", magnetism.spin_moments.shape) - print("orb_moments", magnetism.orbital_moments.shape) + remove_charge_and_s_component = magnetism.spin_moments[:, 1:, :, 1:] + magnetism.orbital_moments = _make_data(np.sqrt(remove_charge_and_s_component)) return magnetism From 4760c8d53d08c58ee6ce7c93fb01ccae34485a44 Mon Sep 17 00:00:00 2001 From: Martin Schlipf Date: Tue, 28 Jan 2025 09:20:39 +0100 Subject: [PATCH 09/15] Fix partial density tests --- tests/calculation/test_partial_density.py | 24 +++++++++++------------ tests/conftest.py | 15 +++++++++++--- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/tests/calculation/test_partial_density.py b/tests/calculation/test_partial_density.py index 3b3ccc0a..0db8cec5 100644 --- a/tests/calculation/test_partial_density.py +++ b/tests/calculation/test_partial_density.py @@ -8,7 +8,7 @@ from py4vasp import calculation from py4vasp._util.slicing import plane -from py4vasp.calculation._partial_charge import STM_settings +from py4vasp._calculation.partial_density import STM_settings from py4vasp.exception import IncorrectUsage, NoData, NotImplemented @@ -332,39 +332,39 @@ def test_stm_default_settings(PolarizedNonSplitPartialDensity, 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) diff --git a/tests/conftest.py b/tests/conftest.py index ab4803f2..97bdacbe 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -713,9 +713,18 @@ def _partial_density(selection): else: spin_dimension = 1 grid = raw.VaspData(tuple(reversed(grid_dim))) - random_charge = raw.VaspData( - np.random.rand(len(kpoints), len(bands), spin_dimension, *grid_dim) - ) + charge_shape = (len(kpoints), len(bands), spin_dimension, *grid_dim) + gaussian_charge = np.zeros(charge_shape) + if not _is_core(): + cov = grid_dim[0] / 10 # standard deviation + z = np.arange(grid_dim[0]) # z range + for gy in range(grid_dim[1]): + for gx in range(grid_dim[2]): + m = int(grid_dim[0] / 2) + gy / 10 + gx / 10 + val = stats.multivariate_normal(mean=m, cov=cov).pdf(z) + # Fill the gaussian_charge array + gaussian_charge[:, :, :, :, gy, gx] = val + gaussian_charge = raw.VaspData(gaussian_charge) return raw.PartialDensity( structure=structure, bands=bands, From 8d3275df2c9302b1b6d10cbfa9f51ab5f9662b55 Mon Sep 17 00:00:00 2001 From: Martin Schlipf Date: Tue, 28 Jan 2025 09:35:56 +0100 Subject: [PATCH 10/15] Fix density test --- src/py4vasp/_calculation/density.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py4vasp/_calculation/density.py b/src/py4vasp/_calculation/density.py index c1813de8..0bba38cd 100644 --- a/src/py4vasp/_calculation/density.py +++ b/src/py4vasp/_calculation/density.py @@ -83,7 +83,7 @@ class Density(base.Refinery, structure.Mixin, view.Mixin): 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) + topology = calculation._topology.from_data(self._raw_data.structure.topology) if self._selection == "kinetic_energy": name = "Kinetic energy" elif self.is_nonpolarized(): From 4df856ddb200388fc6613be1e5b90f693bf0e7bf Mon Sep 17 00:00:00 2001 From: Martin Schlipf Date: Tue, 28 Jan 2025 09:38:30 +0100 Subject: [PATCH 11/15] Remove test for input files --- tests/calculation/test_class.py | 2 +- tests/calculation/test_default_calculation.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/calculation/test_class.py b/tests/calculation/test_class.py index 3c54f30b..c8f185a0 100644 --- a/tests/calculation/test_class.py +++ b/tests/calculation/test_class.py @@ -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.QUANTITIES + _calculation.INPUT_FILES: + 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_default_calculation.py b/tests/calculation/test_default_calculation.py index a69eee4c..07435d3d 100644 --- a/tests/calculation/test_default_calculation.py +++ b/tests/calculation/test_default_calculation.py @@ -19,6 +19,7 @@ def attribute_included(attr): return True +@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" From dd86faa4f826cd16f699c0d88930fff22d256758 Mon Sep 17 00:00:00 2001 From: Martin Schlipf Date: Tue, 28 Jan 2025 09:40:57 +0100 Subject: [PATCH 12/15] Fix format --- tests/calculation/test_class.py | 2 +- tests/calculation/test_partial_density.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/calculation/test_class.py b/tests/calculation/test_class.py index c8f185a0..aa8935ac 100644 --- a/tests/calculation/test_class.py +++ b/tests/calculation/test_class.py @@ -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.QUANTITIES: # + _calculation.INPUT_FILES: + 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_partial_density.py b/tests/calculation/test_partial_density.py index 0db8cec5..4167a6ac 100644 --- a/tests/calculation/test_partial_density.py +++ b/tests/calculation/test_partial_density.py @@ -7,8 +7,8 @@ import pytest from py4vasp import calculation -from py4vasp._util.slicing import plane from py4vasp._calculation.partial_density import STM_settings +from py4vasp._util.slicing import plane from py4vasp.exception import IncorrectUsage, NoData, NotImplemented @@ -184,7 +184,9 @@ def test_non_polarized_to_numpy(NonSplitPartialDensity, spin, Assert, not_core): Assert.allclose(actual, np.asarray(expected).T[:, :, :, 0, 0, 0]) -def test_split_bands_to_numpy(NonPolarizedBandSplitPartialDensity, spin, Assert, not_core): +def test_split_bands_to_numpy( + NonPolarizedBandSplitPartialDensity, spin, Assert, not_core +): bands = NonPolarizedBandSplitPartialDensity.ref.bands for band_index, band in enumerate(bands): actual = NonPolarizedBandSplitPartialDensity.to_numpy(spin, band=band) From 68cea47acf7045f7bd0c9660a9d0aaa3fca53f3f Mon Sep 17 00:00:00 2001 From: Martin Schlipf Date: Tue, 28 Jan 2025 10:15:29 +0100 Subject: [PATCH 13/15] Rename topology -> stoichiometry --- src/py4vasp/_calculation/_CONTCAR.py | 8 +- src/py4vasp/_calculation/__init__.py | 2 +- .../{_topology.py => _stoichiometry.py} | 30 +++--- src/py4vasp/_calculation/density.py | 5 +- src/py4vasp/_calculation/partial_density.py | 14 +-- src/py4vasp/_calculation/phonon.py | 6 +- src/py4vasp/_calculation/phonon_band.py | 2 +- src/py4vasp/_calculation/phonon_dos.py | 6 +- src/py4vasp/_calculation/potential.py | 6 +- src/py4vasp/_calculation/projector.py | 10 +- src/py4vasp/_calculation/structure.py | 28 +++--- src/py4vasp/_raw/data.py | 34 +++---- src/py4vasp/_raw/definition.py | 40 ++++---- src/py4vasp/_util/parser.py | 23 +++-- src/py4vasp/calculation/__init__.py | 98 ------------------- tests/calculation/test_partial_density.py | 6 +- tests/calculation/test_phonon_band.py | 3 +- ...test_topology.py => test_stoichiometry.py} | 66 ++++++------- tests/calculation/test_structure.py | 4 +- tests/conftest.py | 52 +++++----- tests/util/test_parser.py | 18 ++-- 21 files changed, 180 insertions(+), 281 deletions(-) rename src/py4vasp/_calculation/{_topology.py => _stoichiometry.py} (84%) delete mode 100644 src/py4vasp/calculation/__init__.py rename tests/calculation/{test_topology.py => test_stoichiometry.py} (55%) diff --git a/src/py4vasp/_calculation/_CONTCAR.py b/src/py4vasp/_calculation/_CONTCAR.py index 0da5510a..0a631a9d 100644 --- a/src/py4vasp/_calculation/_CONTCAR.py +++ b/src/py4vasp/_calculation/_CONTCAR.py @@ -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/__init__.py b/src/py4vasp/_calculation/__init__.py index 9e0d94cd..9c1cc68d 100644 --- a/src/py4vasp/_calculation/__init__.py +++ b/src/py4vasp/_calculation/__init__.py @@ -77,7 +77,7 @@ class provides a more flexible interface with which you can determine the source "workfunction", "_CONTCAR", "_dispersion", - "_topology", + "_stoichiometry", ) diff --git a/src/py4vasp/_calculation/_topology.py b/src/py4vasp/_calculation/_stoichiometry.py similarity index 84% rename from src/py4vasp/_calculation/_topology.py rename to src/py4vasp/_calculation/_stoichiometry.py index 6ba66af8..7df83671 100644 --- a/src/py4vasp/_calculation/_topology.py +++ b/src/py4vasp/_calculation/_stoichiometry.py @@ -15,21 +15,13 @@ _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 def __str__(self): @@ -43,7 +35,7 @@ def _repr_html_(self): @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 ------- @@ -59,7 +51,7 @@ def to_dict(self): @base.data_access def to_frame(self): - """Convert the topology to a DataFrame + """Convert the stoichiometry to a DataFrame Returns ------- @@ -70,7 +62,7 @@ def to_frame(self): @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 @@ -80,7 +72,7 @@ def to_mdtraj(self): @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 ---------- @@ -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/density.py b/src/py4vasp/_calculation/density.py index 0bba38cd..f7dddfb1 100644 --- a/src/py4vasp/_calculation/density.py +++ b/src/py4vasp/_calculation/density.py @@ -83,7 +83,8 @@ class Density(base.Refinery, structure.Mixin, view.Mixin): 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( diff --git a/src/py4vasp/_calculation/partial_density.py b/src/py4vasp/_calculation/partial_density.py index 44a9ebc1..4867d7be 100644 --- a/src/py4vasp/_calculation/partial_density.py +++ b/src/py4vasp/_calculation/partial_density.py @@ -74,7 +74,7 @@ def stm_settings(self): 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()}"} @@ -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) diff --git a/src/py4vasp/_calculation/phonon.py b/src/py4vasp/_calculation/phonon.py index 522eb85d..c1936561 100644 --- a/src/py4vasp/_calculation/phonon.py +++ b/src/py4vasp/_calculation/phonon.py @@ -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 index 9a5d4017..284d3980 100644 --- a/src/py4vasp/_calculation/phonon_band.py +++ b/src/py4vasp/_calculation/phonon_band.py @@ -31,7 +31,7 @@ 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 def to_dict(self): diff --git a/src/py4vasp/_calculation/phonon_dos.py b/src/py4vasp/_calculation/phonon_dos.py index 289e2a63..1ddb6cca 100644 --- a/src/py4vasp/_calculation/phonon_dos.py +++ b/src/py4vasp/_calculation/phonon_dos.py @@ -26,11 +26,11 @@ class PhononDos(phonon.Mixin, base.Refinery, graph.Mixin): @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) diff --git a/src/py4vasp/_calculation/potential.py b/src/py4vasp/_calculation/potential.py index a6cdeb4e..e1af553c 100644 --- a/src/py4vasp/_calculation/potential.py +++ b/src/py4vasp/_calculation/potential.py @@ -39,8 +39,10 @@ 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() diff --git a/src/py4vasp/_calculation/projector.py b/src/py4vasp/_calculation/projector.py index 01367e00..9ac8f40e 100644 --- a/src/py4vasp/_calculation/projector.py +++ b/src/py4vasp/_calculation/projector.py @@ -84,7 +84,7 @@ 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 @@ -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 } @@ -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/structure.py b/src/py4vasp/_calculation/structure.py index 18aeed53..2b3d101d 100644 --- a/src/py4vasp/_calculation/structure.py +++ b/src/py4vasp/_calculation/structure.py @@ -6,7 +6,7 @@ import numpy as np from py4vasp import calculation, exception, raw -from py4vasp._calculation import _topology, base, slice_ +from py4vasp._calculation import _stoichiometry, base, slice_ from py4vasp._third_party import view from py4vasp._util import documentation, import_, reader @@ -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}" @@ -95,7 +95,7 @@ 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], ) @@ -120,10 +120,10 @@ 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]), ) @@ -146,8 +146,8 @@ 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 @@ -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()), @@ -241,7 +241,7 @@ 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 @@ -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): diff --git a/src/py4vasp/_raw/data.py b/src/py4vasp/_raw/data.py index db992385..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. """ @@ -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 67f43878..69858485 100644 --- a/src/py4vasp/_raw/definition.py +++ b/src/py4vasp/_raw/definition.py @@ -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/parser.py b/src/py4vasp/_util/parser.py index 586e9ce9..976f5579 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, Structure, Stoichiometry 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/tests/calculation/test_partial_density.py b/tests/calculation/test_partial_density.py index 4167a6ac..1dfce5b0 100644 --- a/tests/calculation/test_partial_density.py +++ b/tests/calculation/test_partial_density.py @@ -116,9 +116,9 @@ def test_read(PartialDensity, Assert, not_core): Assert.same_structure(actual["structure"], expected.structure.read()) -def test_topology(PartialDensity, not_core): - actual = PartialDensity._topology() - expected = str(PartialDensity.ref.structure._topology()) +def test_stoichiometry(PartialDensity, not_core): + actual = PartialDensity._stoichiometry() + expected = str(PartialDensity.ref.structure._stoichiometry()) assert actual == expected diff --git a/tests/calculation/test_phonon_band.py b/tests/calculation/test_phonon_band.py index 857c88ac..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 diff --git a/tests/calculation/test_topology.py b/tests/calculation/test_stoichiometry.py similarity index 55% rename from tests/calculation/test_topology.py rename to tests/calculation/test_stoichiometry.py index be7995b7..30e1aaef 100644 --- a/tests/calculation/test_topology.py +++ b/tests/calculation/test_stoichiometry.py @@ -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,23 @@ 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 +100,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 +123,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 9b7761ea..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/conftest.py b/tests/conftest.py index 97bdacbe..5defef01 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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() @@ -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(): @@ -836,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, ) @@ -866,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), ) @@ -881,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"), ) @@ -898,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), ) @@ -913,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"), ) @@ -949,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), ) @@ -964,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"), ) @@ -983,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"), ) @@ -1083,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, ) @@ -1113,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") ) @@ -1149,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_parser.py b/tests/util/test_parser.py index 60c47686..49c88b5c 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, Structure, Stoichiometry 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]) From 412c285e7e61003f9982c7bc173db5b5225763a9 Mon Sep 17 00:00:00 2001 From: Martin Schlipf Date: Tue, 28 Jan 2025 11:25:08 +0100 Subject: [PATCH 14/15] Use poetry v1 for CI --- .github/workflows/test_full.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: | From 10485a610a5c1c9f7dd7aa2e12eeb6c9e244e3a7 Mon Sep 17 00:00:00 2001 From: Martin Schlipf Date: Tue, 28 Jan 2025 11:37:13 +0100 Subject: [PATCH 15/15] Fix format --- src/py4vasp/_util/parser.py | 2 +- tests/calculation/test_stoichiometry.py | 4 +++- tests/util/test_parser.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/py4vasp/_util/parser.py b/src/py4vasp/_util/parser.py index 976f5579..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, Stoichiometry +from py4vasp._raw.data import CONTCAR, Cell, Stoichiometry, Structure from py4vasp._raw.data_wrapper import VaspData from py4vasp.exception import ParserError diff --git a/tests/calculation/test_stoichiometry.py b/tests/calculation/test_stoichiometry.py index 30e1aaef..6dc438e8 100644 --- a/tests/calculation/test_stoichiometry.py +++ b/tests/calculation/test_stoichiometry.py @@ -71,7 +71,9 @@ def unique_elements(self): class TestSr2TiO4(Base): @pytest.fixture(autouse=True) def _setup(self, raw_data): - self.stoichiometry = calculation._stoichiometry.from_data(raw_data.stoichiometry("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"] diff --git a/tests/util/test_parser.py b/tests/util/test_parser.py index 49c88b5c..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, Stoichiometry +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