diff --git a/src/py4vasp/_calculation/__init__.py b/src/py4vasp/_calculation/__init__.py index 847c175a..117f6e0f 100644 --- a/src/py4vasp/_calculation/__init__.py +++ b/src/py4vasp/_calculation/__init__.py @@ -18,6 +18,17 @@ from py4vasp._raw.schema import Link from py4vasp._util import convert, database, import_ + +def _append_database_error( + encountered_errors: dict[str, list[str]], + key: str, + error: Exception, + context: str, +): + message = f"{context} | {type(error).__name__}: {error}" + encountered_errors.setdefault(key, []).append(message) + + INPUT_FILES = ("INCAR", "KPOINTS", "POSCAR") QUANTITIES = ( "band", @@ -293,9 +304,11 @@ def _to_database( ) # Check available quantities and compute additional properties - database_data.available_quantities, database_data.additional_properties = ( - self._compute_database_data(hdf5_path, fermi_energy=fermi_energy) - ) + ( + database_data.available_quantities, + database_data.additional_properties, + database_data.encountered_errors, + ) = self._compute_database_data(hdf5_path, fermi_energy=fermi_energy) # Return DatabaseData object for VaspDB to process return database_data @@ -334,7 +347,9 @@ def path(self): def _compute_database_data( self, hdf5_path: pathlib.Path, fermi_energy: Optional[float] = None - ) -> Tuple[dict[str, tuple[bool, list[str]]], dict[str, dict]]: + ) -> Tuple[ + dict[str, tuple[bool, list[str]]], dict[str, dict], dict[str, list[str]] + ]: """Computes a dict of available py4vasp dataclasses and all available database data. Returns @@ -352,6 +367,7 @@ def _compute_database_data( """ available_quantities = {} additional_properties = {} + encountered_errors = {} # clear cached calls to should_load database.should_load.cache_clear() @@ -363,6 +379,7 @@ def _compute_database_data( QUANTITIES, available_quantities, additional_properties, + encountered_errors, fermi_energy=fermi_energy, ) for group, quantities in GROUPS.items(): @@ -371,6 +388,7 @@ def _compute_database_data( quantities, available_quantities, additional_properties, + encountered_errors, group_name=group, fermi_energy=fermi_energy, ) @@ -383,7 +401,7 @@ def _compute_database_data( available_quantities = database.clean_db_dict_keys(available_quantities) additional_properties = database.clean_db_dict_keys(additional_properties) - return available_quantities, additional_properties + return available_quantities, additional_properties, encountered_errors def _loop_quantities( self, @@ -391,6 +409,7 @@ def _loop_quantities( quantities, available_quantities, additional_properties, + encountered_errors, group_name=None, fermi_energy: Optional[float] = None, ) -> Tuple[dict[str, tuple[bool, list[str]]], dict[str, dict]]: @@ -414,6 +433,7 @@ def _loop_quantities( quantity, group_name, additional_properties, + encountered_errors, fermi_energy=fermi_energy, ) ) @@ -444,6 +464,7 @@ def _compute_quantity_db_data( quantity_name: str, group_name: Optional[str] = None, current_db: dict = {}, + encountered_errors: Optional[dict[str, list[str]]] = None, fermi_energy: Optional[float] = None, ) -> Tuple[bool, dict, list[str]]: "Compute additional data to be stored in the database." @@ -459,6 +480,9 @@ def _compute_quantity_db_data( ) additional_properties = {} additional_related_keys = [] + base_key, _ = database.construct_database_data_key( + group_name, quantity_name, selection + ) try: # check if readable @@ -494,6 +518,13 @@ def _compute_quantity_db_data( except exception.FileAccessError: pass # happens when vaspout.h5 or vaspwave.h5 (where relevant) are missing except Exception as e: + if encountered_errors is not None: + _append_database_error( + encountered_errors, + base_key, + e, + context="availability_check", + ) # print( # f"[CHECK] Unexpected error on {quantity_name} (group={type(group)}) with selection {selection}:", # e, @@ -507,6 +538,7 @@ def _compute_quantity_db_data( )._read_to_database( selection=str(selection), current_db=current_db, + encountered_errors=encountered_errors, original_group_name=group_name, fermi_energy=fermi_energy, ) @@ -516,6 +548,13 @@ def _compute_quantity_db_data( # ) pass # happens when VASP version is too old for this quantity except Exception as e: + if encountered_errors is not None: + _append_database_error( + encountered_errors, + base_key, + e, + context="read_to_database", + ) # print( # f"[ADD] Unexpected error on {quantity_name} (group={type(group)}) with selection {selection} (please consider filing a bug report):", # e, diff --git a/src/py4vasp/_calculation/base.py b/src/py4vasp/_calculation/base.py index 40ed3d48..4580292c 100644 --- a/src/py4vasp/_calculation/base.py +++ b/src/py4vasp/_calculation/base.py @@ -32,6 +32,35 @@ def func_with_access(self, *args, **kwargs): return func_with_access +def record_encountered_error( + encountered_errors: Optional[dict[str, list[str]]], + key: str, + error: Exception, + context: Optional[str] = None, +): + """Store a concise error message for later inspection by database callers.""" + if encountered_errors is None or key is None: + return + message = f"{type(error).__name__}: {error}" + if context: + message = f"{context} | {message}" + encountered_errors.setdefault(key, []).append(message) + + +@contextlib.contextmanager +def suppress_and_record( + encountered_errors: Optional[dict[str, list[str]]], + key: str, + *exceptions, + context: Optional[str] = None, +): + """Like contextlib.suppress, but also records the suppressed error message.""" + try: + yield + except exceptions as error: + record_encountered_error(encountered_errors, key, error, context=context) + + class Refinery: def __init__(self, data_context, **kwargs): self._data_context = data_context @@ -304,11 +333,37 @@ def _to_database(self, *args, **kwargs): ) return database_data except AttributeError as e: + selection = kwargs.get("selection") or "default" + key = database.clean_db_key( + raw_db_key if "raw_db_key" in locals() else _quantity(self.__class__), + db_key_suffix=f":{selection}", + group_name=original_group_name, + ) + record_encountered_error( + kwargs.get("encountered_errors"), + key, + e, + context="_read_to_database", + ) # print( # f"[CHECK] AttributeError in _read_to_database of {self.__class__.__name__} (original: {original_quantity}:{original_selection}, {subquantity_chain}): {e}" # ) # if the particular quantity does not implement database reading, return empty dict return {} + except Exception as e: + selection = kwargs.get("selection") or "default" + key = database.clean_db_key( + raw_db_key if "raw_db_key" in locals() else _quantity(self.__class__), + db_key_suffix=f":{selection}", + group_name=original_group_name, + ) + record_encountered_error( + kwargs.get("encountered_errors"), + key, + e, + context="_read_to_database", + ) + return {} @data_access def selections(self): diff --git a/src/py4vasp/_calculation/cell.py b/src/py4vasp/_calculation/cell.py index 23dde5f9..6c5ce85a 100644 --- a/src/py4vasp/_calculation/cell.py +++ b/src/py4vasp/_calculation/cell.py @@ -1,5 +1,6 @@ # Copyright © VASP Software GmbH, # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +from contextlib import suppress from typing import Optional, Union import numpy as np @@ -11,6 +12,14 @@ _VACUUM_RATIO = 2.5 +_TO_DATABASE_SUPPRESSED_EXCEPTIONS = ( + exception.Py4VaspError, + AttributeError, + TypeError, + ValueError, + np.linalg.LinAlgError, +) + class Cell(slice_.Mixin, base.Refinery): """Cell parameters of the simulation cell.""" @@ -127,7 +136,7 @@ def _is_trajectory(self): def _find_likely_vacuum_direction(self): """Identify likeliest vacuum direction as the lattice vector with the largest length, or from IDIPOL flag.""" - try: + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): lattice_vectors = self.lattice_vectors() dipole_direction = _idipol_to_direction( self._raw_data.idipol, self._raw_data.ldipol @@ -142,8 +151,8 @@ def _find_likely_vacuum_direction(self): return dipole_direction or int( np.argmax(np.linalg.norm(lattice_vectors, axis=-1)) ) - except Exception: - return None + + return None def _is_suspected_2d( diff --git a/src/py4vasp/_calculation/current_density.py b/src/py4vasp/_calculation/current_density.py index bc5254aa..4eddb22f 100644 --- a/src/py4vasp/_calculation/current_density.py +++ b/src/py4vasp/_calculation/current_density.py @@ -1,6 +1,7 @@ # Copyright © VASP Software GmbH, # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +from contextlib import suppress from typing import Optional, Union import numpy as np @@ -14,6 +15,13 @@ pretty = import_.optional("IPython.lib.pretty") +_TO_DATABASE_SUPPRESSED_EXCEPTIONS = ( + exception.Py4VaspError, + AttributeError, + TypeError, + ValueError, +) + _COMMON_PARAMETERS = f"""\ selection : str | None = None Selects which of the possible available currents is used. Check the @@ -104,12 +112,11 @@ def _read_current_density(self, key=None): @base.data_access def _to_database(self, *args, **kwargs): density_dict = {"current_density": {}} - try: + structure_ = {} + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): structure_ = structure.Structure.from_data( self._raw_data.structure )._read_to_database(*args, **kwargs) - except: - structure_ = {} return database.combine_db_dicts(density_dict, structure_) @base.data_access diff --git a/src/py4vasp/_calculation/dielectric_tensor.py b/src/py4vasp/_calculation/dielectric_tensor.py index 64f498f3..5fefcfaa 100644 --- a/src/py4vasp/_calculation/dielectric_tensor.py +++ b/src/py4vasp/_calculation/dielectric_tensor.py @@ -1,5 +1,7 @@ # Copyright © VASP Software GmbH, # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +from typing import Optional + import numpy as np from py4vasp import exception @@ -9,6 +11,13 @@ from py4vasp._util import check, convert from py4vasp._util.tensor import symmetry_reduce +_TO_DATABASE_SUPPRESSED_EXCEPTIONS = ( + exception.Py4VaspError, + AttributeError, + TypeError, + ValueError, +) + class DielectricTensor(base.Refinery): """The dielectric tensor is the static limit of the :attr:`dielectric function`. @@ -40,6 +49,10 @@ def to_dict(self): @base.data_access def _to_database(self, *args, **kwargs): + encountered_errors = kwargs.get("encountered_errors") + selection = kwargs.get("selection") or "default" + error_key = f"dielectric_tensor:{selection}" + tensor_reduced = [None, None, None] isotropic_dielectric_constant = [None, None, None] polarizability_2d = [None, None, None] @@ -55,14 +68,21 @@ def _to_database(self, *args, **kwargs): total_tensor = self._raw_data.electron[:] + self._raw_data.ion[:] for idt, tensor in enumerate([total_tensor, ionic_tensor, electronic_tensor]): - try: + with base.suppress_and_record( + encountered_errors, + error_key, + *_TO_DATABASE_SUPPRESSED_EXCEPTIONS, + context=f"to_database.tensor[{idt}]", + ): tensor_reduced[idt] = list(symmetry_reduce(tensor.T)) ( isotropic_dielectric_constant[idt], polarizability_2d[idt], - ) = self._calculate_dielectric_quantities(tensor) - except: - pass + ) = self._calculate_dielectric_quantities( + tensor, + encountered_errors=encountered_errors, + error_key=error_key, + ) method = ( convert.text_to_string(self._raw_data.method) @@ -89,17 +109,32 @@ def _to_database(self, *args, **kwargs): return dielectric_tensor_db @base.data_access - def _calculate_dielectric_quantities(self, tensor: np.ndarray) -> float: + def _calculate_dielectric_quantities( + self, + tensor: np.ndarray, + *, + encountered_errors: Optional[dict[str, list[str]]] = None, + error_key: Optional[str] = None, + ) -> float: # 2D polarizability for slab systems # TODO migrate finding vacuum direction to structure polarizability_2d = None - try: + with base.suppress_and_record( + encountered_errors, + error_key, + *_TO_DATABASE_SUPPRESSED_EXCEPTIONS, + context="calculate_dielectric_quantities", + ): if not (check.is_none(self._raw_data.cell)): final_cell = cell.Cell.from_data(self._raw_data.cell) if final_cell: - polarizability_2d = _calculate_2d_polarizability(tensor, final_cell) - except Exception: - pass + polarizability_2d = _calculate_2d_polarizability( + tensor, + final_cell, + encountered_errors=encountered_errors, + error_key=error_key, + ) + # 3D isotropic dielectric constant isotropic_dielectric_constant = None isotropic_dielectric_constant = float(np.mean(np.diag(tensor))) @@ -151,12 +186,21 @@ def _description(method): def _calculate_2d_polarizability( - dielectric_tensor: np.ndarray, cell_: cell.Cell + dielectric_tensor: np.ndarray, + cell_: cell.Cell, + *, + encountered_errors: Optional[dict[str, list[str]]] = None, + error_key: Optional[str] = None, ) -> float: """ Compute 2D polarizability (alpha_2D) for a slab system with unknown vacuum direction. """ - try: + with base.suppress_and_record( + encountered_errors, + error_key, + *_TO_DATABASE_SUPPRESSED_EXCEPTIONS, + context="calculate_2d_polarizability", + ): vacuum_dir = cell_._find_likely_vacuum_direction() if vacuum_dir is None: return None @@ -168,5 +212,4 @@ def _calculate_2d_polarizability( alpha_2d = (l_vacuum / (4.0 * np.pi)) * (eps_parallel - 1.0) return alpha_2d - except Exception: - return None + return None diff --git a/src/py4vasp/_calculation/dos.py b/src/py4vasp/_calculation/dos.py index 7f4d2d7d..e3e965f3 100644 --- a/src/py4vasp/_calculation/dos.py +++ b/src/py4vasp/_calculation/dos.py @@ -1,7 +1,10 @@ # Copyright © VASP Software GmbH, # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +from contextlib import suppress + import numpy as np +from py4vasp import exception from py4vasp._calculation import base, projector from py4vasp._raw import data as raw_data from py4vasp._raw.data_db import Dos_DB @@ -11,6 +14,15 @@ pd = import_.optional("pandas") pretty = import_.optional("IPython.lib.pretty") +_TO_DATABASE_SUPPRESSED_EXCEPTIONS = ( + exception.Py4VaspError, + AttributeError, + TypeError, + ValueError, + IndexError, + ZeroDivisionError, +) + class Dos(base.Refinery, graph.Mixin): """The density of states (DOS) describes the number of states per energy. @@ -223,7 +235,7 @@ def _to_database(self, *args, **kwargs): } def _dos_at_energy(self, energy): - try: + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): energies = self._raw_data.energies[:] dos_dict = self._read_total_dos() # interpolate between DOS at closest energies @@ -254,8 +266,7 @@ def _dos_at_energy(self, energy): / (energy_high - energy_low) ) return dos_at_energy - except: - return {} + return {} @base.data_access @documentation.format(selection_doc=projector.selection_doc) diff --git a/src/py4vasp/_calculation/elastic_modulus.py b/src/py4vasp/_calculation/elastic_modulus.py index 9c5cae9c..2017d2ea 100644 --- a/src/py4vasp/_calculation/elastic_modulus.py +++ b/src/py4vasp/_calculation/elastic_modulus.py @@ -5,12 +5,22 @@ import numpy as np +from py4vasp import exception from py4vasp._calculation import base, structure from py4vasp._raw import data as raw_data from py4vasp._raw.data_db import ElasticModulus_DB from py4vasp._util import check from py4vasp._util.tensor import symmetry_reduce +_TO_DATABASE_SUPPRESSED_EXCEPTIONS = ( + exception.Py4VaspError, + np.linalg.LinAlgError, + AttributeError, + TypeError, + ValueError, + ZeroDivisionError, +) + class ElasticModulus(base.Refinery): """The elastic modulus is the second derivative of the energy with respect to strain. @@ -43,6 +53,10 @@ def to_dict(self): @base.data_access def _to_database(self, *args, **kwargs): + encountered_errors = kwargs.get("encountered_errors") + selection = kwargs.get("selection") or "default" + error_key = f"elastic_modulus:{selection}" + volume_per_atom = None ( bulk_modulus, @@ -70,7 +84,12 @@ def _to_database(self, *args, **kwargs): for idt, tensor in enumerate([total_tensor, ionic_tensor, electronic_tensor]): voigt_tensor = None - try: + with base.suppress_and_record( + encountered_errors, + error_key, + *_TO_DATABASE_SUPPRESSED_EXCEPTIONS, + context=f"to_database.tensor[{idt}]", + ): if not check.is_none(tensor): compact_tensor[idt] = symmetry_reduce(symmetry_reduce(tensor).T).T voigt_tensor = compact_tensor[idt] / 10.0 # converting kbar to GPa @@ -79,9 +98,13 @@ def _to_database(self, *args, **kwargs): if compact_tensor[idt] is not None else None ) - except: - pass - try: + + with base.suppress_and_record( + encountered_errors, + error_key, + *_TO_DATABASE_SUPPRESSED_EXCEPTIONS, + context=f"to_database.properties[{idt}]", + ): # Properties from elastic tensor ( bulk_modulus[idt], @@ -92,10 +115,11 @@ def _to_database(self, *args, **kwargs): vickers_hardness[idt], fracture_toughness[idt], ) = self._compute_elastic_properties( - voigt_tensor, volume_per_atom=volume_per_atom + voigt_tensor, + volume_per_atom=volume_per_atom, + encountered_errors=encountered_errors, + error_key=error_key, ) - except: - pass return { "elastic_modulus": ElasticModulus_DB( @@ -135,7 +159,12 @@ def __str__(self): {_elastic_modulus_string(self._raw_data.relaxed_ion[:], "relaxed-ion")}""" def _compute_elastic_properties( - self, voigt_tensor: np.ndarray, volume_per_atom: Optional[float] = None + self, + voigt_tensor: np.ndarray, + volume_per_atom: Optional[float] = None, + *, + encountered_errors: Optional[dict[str, list[str]]] = None, + error_key: Optional[str] = None, ) -> tuple: ( bulk_modulus, @@ -147,39 +176,55 @@ def _compute_elastic_properties( fracture_toughness, ) = (None, None, None, None, None, None, None) - try: + with base.suppress_and_record( + encountered_errors, + error_key, + *_TO_DATABASE_SUPPRESSED_EXCEPTIONS, + context="compute_elastic_properties.init", + ): elastic_tensor = _ElasticTensor.from_array(voigt_tensor) - try: + with base.suppress_and_record( + encountered_errors, + error_key, + *_TO_DATABASE_SUPPRESSED_EXCEPTIONS, + context="compute_elastic_properties.vrh", + ): bulk_modulus, shear_modulus, youngs_modulus, poisson_ratio = ( elastic_tensor.get_VRH() ) - except Exception as e: - pass - try: + with base.suppress_and_record( + encountered_errors, + error_key, + *_TO_DATABASE_SUPPRESSED_EXCEPTIONS, + context="compute_elastic_properties.pugh", + ): if shear_modulus is not None and bulk_modulus is not None: pugh_ratio = ( shear_modulus / bulk_modulus if (bulk_modulus != 0 and shear_modulus != 0) else 0.0 if shear_modulus == 0 else None ) - except Exception as e: - pass - try: + with base.suppress_and_record( + encountered_errors, + error_key, + *_TO_DATABASE_SUPPRESSED_EXCEPTIONS, + context="compute_elastic_properties.hardness", + ): vickers_hardness = elastic_tensor.get_hardness() - except Exception as e: - pass - try: + with base.suppress_and_record( + encountered_errors, + error_key, + *_TO_DATABASE_SUPPRESSED_EXCEPTIONS, + context="compute_elastic_properties.fracture", + ): fracture_toughness = elastic_tensor.get_fracture_toughness( volume_per_atom ) - except Exception as e: - pass - except Exception as e: - pass + vickers_hardness = elastic_tensor.get_hardness() return ( bulk_modulus, diff --git a/src/py4vasp/_calculation/electronic_minimization.py b/src/py4vasp/_calculation/electronic_minimization.py index 69c543c9..b8625773 100644 --- a/src/py4vasp/_calculation/electronic_minimization.py +++ b/src/py4vasp/_calculation/electronic_minimization.py @@ -1,6 +1,8 @@ # Copyright © VASP Software GmbH, # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +from contextlib import suppress + import numpy as np from py4vasp import exception, raw @@ -10,6 +12,14 @@ from py4vasp._third_party import graph from py4vasp._util import check +_TO_DATABASE_SUPPRESSED_EXCEPTIONS = ( + exception.Py4VaspError, + AttributeError, + TypeError, + ValueError, + IndexError, +) + class ElectronicMinimization(slice_.Mixin, base.Refinery, graph.Mixin): """Access the convergence data for each electronic step. @@ -89,7 +99,7 @@ def _to_database(self, *args, **kwargs): elmin_is_converged_all = None elmin_is_converged_final = None - try: + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): if not check.is_none(self._raw_data.is_elmin_converged): elmin_is_converged_all = bool( np.all(np.array(self._raw_data.is_elmin_converged[:]) == 0.0) @@ -97,17 +107,13 @@ def _to_database(self, *args, **kwargs): elmin_is_converged_final = bool( self._raw_data.is_elmin_converged[-1] == 0.0 ) - except: - pass - try: + with suppress(exception.NoData, *_TO_DATABASE_SUPPRESSED_EXCEPTIONS): ( num_max_electronic_steps_per_ionic, num_min_electronic_steps_per_ionic, num_electronic_steps, ) = self._get_electronic_steps_info() - except exception.NoData: - pass return { "electronic_minimization": ElectronicMinimization_DB( diff --git a/src/py4vasp/_calculation/kpoint.py b/src/py4vasp/_calculation/kpoint.py index d112e00c..7a94fd2a 100644 --- a/src/py4vasp/_calculation/kpoint.py +++ b/src/py4vasp/_calculation/kpoint.py @@ -1,6 +1,7 @@ # Copyright © VASP Software GmbH, # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) import functools +from contextlib import suppress from fractions import Fraction from typing import Any @@ -19,6 +20,17 @@ of the default one defined by the KPOINTS file. """ +_TO_DATABASE_SUPPRESSED_EXCEPTIONS = ( + exception.Py4VaspError, + exception.RefinementError, +) + + +def _safe_call(func): + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): + return func() + return None + class Kpoint(base.Refinery): """The **k**-point mesh used in the VASP calculation. @@ -124,12 +136,17 @@ def _to_database(self, *args, **kwargs): user_labels = None if len(user_labels) == 0 else user_labels sampled_points = sorted(set(user_labels)) if user_labels is not None else None + mode = _safe_call(self.mode) + line_length = _safe_call(self.line_length) + num_kpoints_total = _safe_call(self.number_kpoints) + num_lines = _safe_call(self.number_lines) + return { "kpoint": Kpoint_DB( - mode=self.mode(), - line_length=self.line_length(), - num_kpoints_total=self.number_kpoints(), - num_lines=self.number_lines(), + mode=mode, + line_length=line_length, + num_kpoints_total=num_kpoints_total, + num_lines=num_lines, num_kpoints_grid=grid_kpoints, labels=user_labels, labels_unique=sampled_points, diff --git a/src/py4vasp/_calculation/piezoelectric_tensor.py b/src/py4vasp/_calculation/piezoelectric_tensor.py index bcca303c..4f6530e7 100644 --- a/src/py4vasp/_calculation/piezoelectric_tensor.py +++ b/src/py4vasp/_calculation/piezoelectric_tensor.py @@ -1,13 +1,24 @@ # Copyright © VASP Software GmbH, # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +from contextlib import suppress + import numpy as np +from py4vasp import exception from py4vasp._calculation import base, cell from py4vasp._raw import data as raw_data from py4vasp._raw.data_db import PiezoelectricTensor_DB from py4vasp._util import check from py4vasp._util.tensor import symmetry_reduce, tensor_constants +_TO_DATABASE_SUPPRESSED_EXCEPTIONS = ( + exception.Py4VaspError, + AttributeError, + TypeError, + ValueError, + IndexError, +) + class PiezoelectricTensor(base.Refinery): """The piezoelectric tensor is the derivative of the energy with respect to strain and field. @@ -82,7 +93,7 @@ def _to_database(self, *args, **kwargs): for idt, tensor in enumerate([total_tensor, ionic_tensor, electronic_tensor]): e_tensor = None # Piezoelectric stress tensor e_ij (C/m^2) - try: + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): e_tensor = _extract_tensor( tensor ) # 3x6 tensor, column order: XX YY ZZ YZ ZX XY @@ -96,10 +107,8 @@ def _to_database(self, *args, **kwargs): in_plane[idt], lvac = _compute_2d_plane_and_conversion_factor(cCell) if in_plane[idt] is not None and lvac is not None: tensor_2d[idt] = e_tensor * lvac - except Exception as e: - pass - try: + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): ( e11[idt], e22[idt], @@ -108,8 +117,7 @@ def _to_database(self, *args, **kwargs): e_rms[idt], e_frobenius[idt], ) = _compute_bulk_quantities(e_tensor) - except: - pass + return { "piezoelectric_tensor": PiezoelectricTensor_DB( total_3d_tensor_x=reduced_tensor_x[0], diff --git a/src/py4vasp/_calculation/polarization.py b/src/py4vasp/_calculation/polarization.py index cb19118c..fa0049f1 100644 --- a/src/py4vasp/_calculation/polarization.py +++ b/src/py4vasp/_calculation/polarization.py @@ -1,5 +1,7 @@ # Copyright © VASP Software GmbH, # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +from contextlib import suppress + import numpy as np from py4vasp import exception @@ -59,7 +61,7 @@ def _to_database(self, *args, **kwargs): ion_dipole = None total_dipole = None - try: + with suppress(exception.NoData): electron_dipole = list(self._raw_data.electron[:]) ion_dipole = list(self._raw_data.ion[:]) total_dipole = list(self._raw_data.electron[:] + self._raw_data.ion[:]) @@ -69,8 +71,6 @@ def _to_database(self, *args, **kwargs): total_norm = np.linalg.norm( self._raw_data.electron[:] + self._raw_data.ion[:] ) - except exception.NoData: - pass return { "polarization": Polarization_DB( diff --git a/src/py4vasp/_calculation/run_info.py b/src/py4vasp/_calculation/run_info.py index cde40f42..f485338c 100644 --- a/src/py4vasp/_calculation/run_info.py +++ b/src/py4vasp/_calculation/run_info.py @@ -1,9 +1,12 @@ # Copyright © VASP Software GmbH, # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +from contextlib import suppress + from py4vasp._calculation import bandgap, base, exception from py4vasp._calculation._dispersion import Dispersion from py4vasp._raw import data as raw_data from py4vasp._raw.data_db import RunInfo_DB +from py4vasp._raw.data_wrapper import VaspData from py4vasp._util import check @@ -12,6 +15,15 @@ class RunInfo(base.Refinery): _raw_data: raw_data.RunInfo + _TO_DATABASE_SUPPRESSED_EXCEPTIONS = ( + exception.Py4VaspError, + exception.OutdatedVaspVersion, + exception.NoData, + AttributeError, + TypeError, + ValueError, + ) + @base.data_access def to_dict(self): "Convert the run information to a dictionary." @@ -39,10 +51,10 @@ def _read(self, *keys: str) -> dict: def _dict_additional_collection(self) -> dict: fermi_energy = None - try: + with suppress(exception.NoData): fermi_energy = self._raw_data.fermi_energy - except exception.NoData: - pass + if isinstance(fermi_energy, VaspData): + fermi_energy = fermi_energy._data is_success = None # TODO implement @@ -72,43 +84,37 @@ def _dict_additional_collection(self) -> dict: } def _is_collinear(self): - try: + if not check.is_none(self._raw_data.len_dos): return self._raw_data.len_dos == 2 - except exception.NoData: - try: + else: + if not check.is_none(self._raw_data.band_dispersion_eigenvalues): return len(self._raw_data.band_dispersion_eigenvalues) == 2 - except exception.NoData: + else: return None def _is_noncollinear(self): - try: + if not check.is_none(self._raw_data.len_dos): return self._raw_data.len_dos == 4 - except exception.NoData: - try: - if check.is_none(self._raw_data.band_projections): - return None + else: + if not check.is_none(self._raw_data.band_projections): return len(self._raw_data.band_projections) == 4 - except exception.NoData: + else: return None def _is_metallic(self): - try: + with suppress(*self._TO_DATABASE_SUPPRESSED_EXCEPTIONS): if check.is_none(self._raw_data.bandgap): return None gap = bandgap.Bandgap.from_data(self._raw_data.bandgap) return all(gap._output_gap("fundamental", to_string=False) <= 0.0) - except (exception.OutdatedVaspVersion, exception.NoData): - return None - except: - return None + + return None def _dict_from_system(self) -> dict: system_tag = None - try: + with suppress(exception.NoData): system_tag = self._read("system", "system") - except exception.NoData: - pass return { "system_tag": system_tag, @@ -117,11 +123,9 @@ def _dict_from_system(self) -> dict: def _dict_from_runtime(self) -> dict: vasp_version = None - try: + with suppress(exception.NoData): runtime_data = self._raw_data.runtime vasp_version = None if runtime_data is None else runtime_data.vasp_version - except exception.NoData: - pass return { "vasp_version": vasp_version, @@ -130,12 +134,10 @@ def _dict_from_runtime(self) -> dict: def _dict_from_structure(self) -> dict: num_ion_steps = None - try: + with suppress(exception.NoData, AttributeError): positions = self._read("structure", "positions") if not check.is_none(positions): num_ion_steps = 1 if positions.ndim == 2 else positions.shape[0] - except (exception.NoData, AttributeError): - pass return { "num_ionic_steps": num_ion_steps, @@ -146,7 +148,7 @@ def _dict_from_contcar(self) -> dict: has_lattice_velocities = None has_ion_velocities = None - try: + with suppress(exception.NoData): has_selective_dynamics = not check.is_none( self._read("contcar", "selective_dynamics") ) @@ -156,8 +158,6 @@ def _dict_from_contcar(self) -> dict: has_ion_velocities = not check.is_none( self._read("contcar", "ion_velocities") ) - except exception.NoData: - pass return { "has_selective_dynamics": has_selective_dynamics, @@ -169,12 +169,10 @@ def _dict_from_phonon_dispersion(self) -> dict: phonon_num_qpoints = None phonon_num_modes = None - try: + with suppress(exception.NoData): eigenvalues = self._raw_data.phonon_dispersion.eigenvalues phonon_num_qpoints = eigenvalues.shape[0] phonon_num_modes = eigenvalues.shape[1] - except exception.NoData: - pass return { "phonon_num_qpoints": phonon_num_qpoints, diff --git a/src/py4vasp/_calculation/structure.py b/src/py4vasp/_calculation/structure.py index dee646e1..15c0e71e 100644 --- a/src/py4vasp/_calculation/structure.py +++ b/src/py4vasp/_calculation/structure.py @@ -1,5 +1,6 @@ # Copyright © VASP Software GmbH, # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +from contextlib import suppress from dataclasses import dataclass from typing import Union @@ -18,6 +19,15 @@ __all__ = ["Structure"] +_TO_DATABASE_SUPPRESSED_EXCEPTIONS = ( + exception.Py4VaspError, + np.linalg.LinAlgError, + AttributeError, + TypeError, + ValueError, + IndexError, +) + @dataclass class _Format: @@ -263,7 +273,7 @@ def _to_database(self, *args, **kwargs): # TODO add more structure properties final_lattice, initial_lattice = ([None, None, None] for _ in range(2)) - try: + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): lattices = self.lattice_vectors() final_lattice = lattices[-1] if lattices.ndim == 3 else lattices initial_lattice = lattices[0] if lattices.ndim == 3 else lattices @@ -271,10 +281,9 @@ def _to_database(self, *args, **kwargs): final_lattice = [None, None, None] if initial_lattice.ndim != 2: initial_lattice = [None, None, None] - except: - pass + volume_final, volume_initial = (None for _ in range(2)) - try: + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): volumes = self.volume() volume_final = ( volumes[-1] @@ -286,8 +295,6 @@ def _to_database(self, *args, **kwargs): if not isinstance(volumes, (float, np.float64, np.float32)) else volumes ) - except Exception as e: - pass lengths_final, angles_final, lengths_initial, angles_initial = ( None for _ in range(4) @@ -299,12 +306,10 @@ def _to_database(self, *args, **kwargs): cell_area_2d_span_initial, ) = (None for _ in range(4)) dimensionality = 3 - try: + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): dimensionality = self._dimensionality() - except Exception as e: - pass - try: + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): cell_: cell.Cell = self._cell() lengths = cell_.lengths() lengths_final = lengths[-1] if lengths.ndim == 2 else lengths @@ -334,8 +339,6 @@ def _to_database(self, *args, **kwargs): if isinstance(cell_area_2d_span, list) else cell_area_2d_span ) - except Exception as e: - pass num_atoms = self.number_atoms() or None self._steps = steps_sel diff --git a/src/py4vasp/_calculation/workfunction.py b/src/py4vasp/_calculation/workfunction.py index b03d0dba..8d213aae 100644 --- a/src/py4vasp/_calculation/workfunction.py +++ b/src/py4vasp/_calculation/workfunction.py @@ -1,5 +1,7 @@ # Copyright © VASP Software GmbH, # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +from contextlib import suppress + from py4vasp import exception from py4vasp._calculation import bandgap, base from py4vasp._raw import data as raw_data @@ -46,14 +48,14 @@ def to_dict(self): Contains vacuum potential, average potential and relevant reference energies within the surface. """ - try: + band_extrema = {} + with suppress(exception.NoData): gap = bandgap.Bandgap.from_data(self._raw_data.reference_potential) band_extrema = { "valence_band_maximum": gap.valence_band_maximum(), "conduction_band_minimum": gap.conduction_band_minimum(), } - except exception.NoData: - band_extrema = {} + return { "direction": f"lattice vector {self._raw_data.idipol}", "distance": self._raw_data.distance[:], @@ -65,11 +67,10 @@ def to_dict(self): @base.data_access def _to_database(self, *args, **kwargs): - try: + is_metallic = None + with suppress(exception.NoData): gap = bandgap.Bandgap.from_data(self._raw_data.reference_potential) is_metallic = gap._output_gap("fundamental", to_string=False) <= 0.0 - except exception.NoData: - is_metallic = None return { "workfunction": Workfunction_DB( diff --git a/src/py4vasp/_raw/data.py b/src/py4vasp/_raw/data.py index 9f85d648..ed4e7994 100644 --- a/src/py4vasp/_raw/data.py +++ b/src/py4vasp/_raw/data.py @@ -206,6 +206,10 @@ class _DatabaseData: Keys are constructed like 'group.quantity:selection' where group and selection are optional. The values are dictionaries of properties.""" + encountered_errors: Optional[dict[str, list[str]]] = None + """Non-fatal errors encountered while assembling database data. + Keys follow the same quantity:selection convention as additional_properties.""" + @dataclasses.dataclass class Density: diff --git a/src/py4vasp/_raw/data_db.py b/src/py4vasp/_raw/data_db.py index 123a3aff..371fa24c 100644 --- a/src/py4vasp/_raw/data_db.py +++ b/src/py4vasp/_raw/data_db.py @@ -4,6 +4,8 @@ from dataclasses import dataclass, field from typing import Any, Dict, List, Optional +from py4vasp._raw.data_wrapper import VaspData + __SCHEMA_VERSION__ = "0.1.0" @@ -16,6 +18,11 @@ class _DBDataMixin: ) """The version of the database data schema. This can be used to track changes in the data structure and ensure compatibility when reading from the database.""" + def __post_init__(self): + for field_name, field_value in self.__dict__.items(): + if isinstance(field_value, VaspData): + setattr(self, field_name, field_value._data) + @dataclass class CONTCAR_DB(_DBDataMixin): diff --git a/src/py4vasp/_third_party/view/view.py b/src/py4vasp/_third_party/view/view.py index 69496a67..0b2988ef 100644 --- a/src/py4vasp/_third_party/view/view.py +++ b/src/py4vasp/_third_party/view/view.py @@ -3,6 +3,7 @@ import itertools import os import tempfile +from contextlib import suppress from dataclasses import dataclass from typing import NamedTuple, Optional, Sequence @@ -411,15 +412,13 @@ def _raise_error_if_present_on_multiple_steps(self, attributes, mode=None): if not attributes: return for attribute in attributes: - try: + with suppress(AttributeError): if len(attribute.quantity) > 1: if mode == "ngl": raise exception.NotImplemented("""\ Currently isosurfaces and ion arrows are implemented only for cases where there is only one frame in the trajectory. Make sure that either only one frame for the positions attribute is supplied with its corresponding grid scalar or ion arrow component.""") - except AttributeError: - pass def _raise_error_if_number_steps_inconsistent(self): if len(self.elements) == len(self.lattice_vectors) == len(self.positions): diff --git a/src/py4vasp/_util/database.py b/src/py4vasp/_util/database.py index 9a5254b0..f2357590 100644 --- a/src/py4vasp/_util/database.py +++ b/src/py4vasp/_util/database.py @@ -3,6 +3,7 @@ import ast import functools import inspect +from contextlib import suppress from math import gcd from pathlib import Path from typing import Any, Dict, List, Optional, Set, Tuple @@ -339,7 +340,7 @@ def _get_dataclass_field_docstrings(dataclass: Any) -> Dict[str, Optional[str]]: Expects field documentation to be provided as a string expression directly below the corresponding field definition. """ - try: + with suppress(Exception): source_file = inspect.getsourcefile(dataclass) if source_file is None: return {} @@ -357,8 +358,8 @@ def _get_dataclass_field_docstrings(dataclass: Any) -> Dict[str, Optional[str]]: continue docstrings[field_name] = _extract_following_docstring(class_body, index) return docstrings - except Exception: - return {} + + return {} def _find_class_node(tree: ast.AST, class_name: str) -> Optional[ast.ClassDef]: @@ -470,10 +471,9 @@ def get_all_possible_keys( """Map all available keys (group.quantity[:selection]) to dataclass names.""" for k in list(all_keys.keys()): - try: + selections_list = [] + with suppress(exception.Py4VaspError): selections_list = unique_selections(k) - except: - selections_list = [] constructed_key = _quantity_label_to_db_key(k) dataclass_name = _get_dataclass_name_for_quantity(k) for sel in selections_list: diff --git a/tests/util/test_database.py b/tests/util/test_database.py index 3adef01f..f99e2d76 100644 --- a/tests/util/test_database.py +++ b/tests/util/test_database.py @@ -1,5 +1,6 @@ # Copyright © VASP Software GmbH, # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +import dataclasses from pathlib import Path import pytest @@ -7,6 +8,8 @@ from py4vasp import demo from py4vasp._calculation import GROUPS, QUANTITIES from py4vasp._raw.data import CalculationMetaData, _DatabaseData +from py4vasp._raw.data_db import _DBDataMixin +from py4vasp._raw.data_wrapper import VaspData from py4vasp._raw.definition import DEFAULT_SOURCE from py4vasp._util import database @@ -263,3 +266,20 @@ def test_demo_db_with_tags(tags, tmp_path): demo_calc_db = demo_calc._to_database(tags=tags) basic_db_checks(demo_calc_db) assert demo_calc_db.metadata.tags == tags + + +def test_no_vaspdata_in_db(): + """Check that VaspData objects are converted to their underlying data under the _DBDataMixin for the database wrapper classes.""" + + @dataclasses.dataclass + class DummyClassDB(_DBDataMixin): + field1: int = None + field2: str = None + field3: bool = None + + dummyClass = DummyClassDB( + field1=VaspData(None), field2=VaspData("test"), field3=True + ) + assert dummyClass.field1 is None + assert dummyClass.field2 == "test" + assert dummyClass.field3 is True