From 4938fa0fea27308b4d9f549f4253dfc99e357604 Mon Sep 17 00:00:00 2001 From: Max Liebetreu Date: Fri, 20 Mar 2026 17:39:26 +0100 Subject: [PATCH 1/8] Make try-except adjustments; some tests fail --- src/py4vasp/_calculation/cell.py | 7 +-- src/py4vasp/_calculation/current_density.py | 6 +-- src/py4vasp/_calculation/dielectric_tensor.py | 16 +++--- src/py4vasp/_calculation/dos.py | 8 +-- src/py4vasp/_calculation/elastic_modulus.py | 32 ++++-------- .../_calculation/electronic_minimization.py | 10 ++-- .../_calculation/piezoelectric_tensor.py | 12 ++--- src/py4vasp/_calculation/polarization.py | 6 +-- src/py4vasp/_calculation/run_info.py | 52 +++++++------------ src/py4vasp/_calculation/structure.py | 18 +++---- src/py4vasp/_calculation/workfunction.py | 13 ++--- src/py4vasp/_third_party/view/view.py | 5 +- src/py4vasp/_util/database.py | 12 ++--- 13 files changed, 83 insertions(+), 114 deletions(-) diff --git a/src/py4vasp/_calculation/cell.py b/src/py4vasp/_calculation/cell.py index 23dde5f9..db35d702 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 @@ -127,7 +128,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(exception.Py4VaspError): lattice_vectors = self.lattice_vectors() dipole_direction = _idipol_to_direction( self._raw_data.idipol, self._raw_data.ldipol @@ -142,8 +143,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..2c483cd1 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 @@ -104,12 +105,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(exception.Py4VaspError): 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..504eeafe 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 contextlib import suppress + import numpy as np from py4vasp import exception @@ -55,14 +57,12 @@ 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 suppress(exception.Py4VaspError): tensor_reduced[idt] = list(symmetry_reduce(tensor.T)) ( isotropic_dielectric_constant[idt], polarizability_2d[idt], ) = self._calculate_dielectric_quantities(tensor) - except: - pass method = ( convert.text_to_string(self._raw_data.method) @@ -93,13 +93,12 @@ def _calculate_dielectric_quantities(self, tensor: np.ndarray) -> float: # 2D polarizability for slab systems # TODO migrate finding vacuum direction to structure polarizability_2d = None - try: + with suppress(exception.Py4VaspError): 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 + # 3D isotropic dielectric constant isotropic_dielectric_constant = None isotropic_dielectric_constant = float(np.mean(np.diag(tensor))) @@ -156,7 +155,7 @@ def _calculate_2d_polarizability( """ Compute 2D polarizability (alpha_2D) for a slab system with unknown vacuum direction. """ - try: + with suppress(exception.Py4VaspError): vacuum_dir = cell_._find_likely_vacuum_direction() if vacuum_dir is None: return None @@ -168,5 +167,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..4e5b41f8 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 @@ -223,7 +226,7 @@ def _to_database(self, *args, **kwargs): } def _dos_at_energy(self, energy): - try: + with suppress(exception.Py4VaspError): energies = self._raw_data.energies[:] dos_dict = self._read_total_dos() # interpolate between DOS at closest energies @@ -254,8 +257,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..9141389b 100644 --- a/src/py4vasp/_calculation/elastic_modulus.py +++ b/src/py4vasp/_calculation/elastic_modulus.py @@ -1,10 +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 math import pow from typing import Optional 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 @@ -70,7 +72,7 @@ def _to_database(self, *args, **kwargs): for idt, tensor in enumerate([total_tensor, ionic_tensor, electronic_tensor]): voigt_tensor = None - try: + with suppress(exception.Py4VaspError): 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 +81,8 @@ def _to_database(self, *args, **kwargs): if compact_tensor[idt] is not None else None ) - except: - pass - try: + + with suppress(exception.Py4VaspError): # Properties from elastic tensor ( bulk_modulus[idt], @@ -94,8 +95,6 @@ def _to_database(self, *args, **kwargs): ) = self._compute_elastic_properties( voigt_tensor, volume_per_atom=volume_per_atom ) - except: - pass return { "elastic_modulus": ElasticModulus_DB( @@ -147,39 +146,30 @@ def _compute_elastic_properties( fracture_toughness, ) = (None, None, None, None, None, None, None) - try: + with suppress(exception.Py4VaspError): elastic_tensor = _ElasticTensor.from_array(voigt_tensor) - try: + with suppress(exception.Py4VaspError): bulk_modulus, shear_modulus, youngs_modulus, poisson_ratio = ( elastic_tensor.get_VRH() ) - except Exception as e: - pass - try: + with suppress(exception.Py4VaspError): 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 suppress(exception.Py4VaspError): vickers_hardness = elastic_tensor.get_hardness() - except Exception as e: - pass - try: + with suppress(exception.Py4VaspError): 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..11d02d12 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 @@ -89,7 +91,7 @@ def _to_database(self, *args, **kwargs): elmin_is_converged_all = None elmin_is_converged_final = None - try: + with suppress(exception.Py4VaspError): 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 +99,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): ( 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/piezoelectric_tensor.py b/src/py4vasp/_calculation/piezoelectric_tensor.py index bcca303c..8c09f23b 100644 --- a/src/py4vasp/_calculation/piezoelectric_tensor.py +++ b/src/py4vasp/_calculation/piezoelectric_tensor.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, cell from py4vasp._raw import data as raw_data from py4vasp._raw.data_db import PiezoelectricTensor_DB @@ -82,7 +85,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(exception.Py4VaspError): e_tensor = _extract_tensor( tensor ) # 3x6 tensor, column order: XX YY ZZ YZ ZX XY @@ -96,10 +99,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(exception.Py4VaspError): ( e11[idt], e22[idt], @@ -108,8 +109,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..37221247 100644 --- a/src/py4vasp/_calculation/run_info.py +++ b/src/py4vasp/_calculation/run_info.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._calculation import bandgap, base, exception from py4vasp._calculation._dispersion import Dispersion from py4vasp._raw import data as raw_data @@ -39,10 +41,8 @@ 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 is_success = None # TODO implement @@ -72,43 +72,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(exception.Py4VaspError, exception.OutdatedVaspVersion, exception.NoData): 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 +111,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 +122,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 +136,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 +146,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 +157,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..e5bfd2e1 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 @@ -263,7 +264,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(exception.Py4VaspError): 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 +272,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(exception.Py4VaspError): volumes = self.volume() volume_final = ( volumes[-1] @@ -286,8 +286,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 +297,10 @@ def _to_database(self, *args, **kwargs): cell_area_2d_span_initial, ) = (None for _ in range(4)) dimensionality = 3 - try: + with suppress(exception.Py4VaspError): dimensionality = self._dimensionality() - except Exception as e: - pass - try: + with suppress(exception.Py4VaspError): cell_: cell.Cell = self._cell() lengths = cell_.lengths() lengths_final = lengths[-1] if lengths.ndim == 2 else lengths @@ -334,8 +330,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/_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..e5762988 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: From a1a090d23847d421955de45a2706b0feb00b6d7e Mon Sep 17 00:00:00 2001 From: Max Liebetreu Date: Wed, 25 Mar 2026 11:09:07 +0100 Subject: [PATCH 2/8] Fix tests by expanding the list of suppressed errors --- src/py4vasp/_calculation/dielectric_tensor.py | 13 ++++++++--- src/py4vasp/_calculation/elastic_modulus.py | 23 +++++++++++++------ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/py4vasp/_calculation/dielectric_tensor.py b/src/py4vasp/_calculation/dielectric_tensor.py index 504eeafe..e5720bf5 100644 --- a/src/py4vasp/_calculation/dielectric_tensor.py +++ b/src/py4vasp/_calculation/dielectric_tensor.py @@ -11,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`. @@ -57,7 +64,7 @@ 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]): - with suppress(exception.Py4VaspError): + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): tensor_reduced[idt] = list(symmetry_reduce(tensor.T)) ( isotropic_dielectric_constant[idt], @@ -93,7 +100,7 @@ def _calculate_dielectric_quantities(self, tensor: np.ndarray) -> float: # 2D polarizability for slab systems # TODO migrate finding vacuum direction to structure polarizability_2d = None - with suppress(exception.Py4VaspError): + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): if not (check.is_none(self._raw_data.cell)): final_cell = cell.Cell.from_data(self._raw_data.cell) if final_cell: @@ -155,7 +162,7 @@ def _calculate_2d_polarizability( """ Compute 2D polarizability (alpha_2D) for a slab system with unknown vacuum direction. """ - with suppress(exception.Py4VaspError): + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): vacuum_dir = cell_._find_likely_vacuum_direction() if vacuum_dir is None: return None diff --git a/src/py4vasp/_calculation/elastic_modulus.py b/src/py4vasp/_calculation/elastic_modulus.py index 9141389b..19cc2c3c 100644 --- a/src/py4vasp/_calculation/elastic_modulus.py +++ b/src/py4vasp/_calculation/elastic_modulus.py @@ -13,6 +13,15 @@ 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. @@ -72,7 +81,7 @@ def _to_database(self, *args, **kwargs): for idt, tensor in enumerate([total_tensor, ionic_tensor, electronic_tensor]): voigt_tensor = None - with suppress(exception.Py4VaspError): + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): 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 @@ -82,7 +91,7 @@ def _to_database(self, *args, **kwargs): else None ) - with suppress(exception.Py4VaspError): + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): # Properties from elastic tensor ( bulk_modulus[idt], @@ -146,15 +155,15 @@ def _compute_elastic_properties( fracture_toughness, ) = (None, None, None, None, None, None, None) - with suppress(exception.Py4VaspError): + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): elastic_tensor = _ElasticTensor.from_array(voigt_tensor) - with suppress(exception.Py4VaspError): + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): bulk_modulus, shear_modulus, youngs_modulus, poisson_ratio = ( elastic_tensor.get_VRH() ) - with suppress(exception.Py4VaspError): + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): if shear_modulus is not None and bulk_modulus is not None: pugh_ratio = ( shear_modulus / bulk_modulus @@ -162,10 +171,10 @@ def _compute_elastic_properties( else 0.0 if shear_modulus == 0 else None ) - with suppress(exception.Py4VaspError): + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): vickers_hardness = elastic_tensor.get_hardness() - with suppress(exception.Py4VaspError): + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): fracture_toughness = elastic_tensor.get_fracture_toughness( volume_per_atom ) From b3b927f8389a3ac83286d60d989276c62166875a Mon Sep 17 00:00:00 2001 From: Max Liebetreu Date: Wed, 25 Mar 2026 12:56:16 +0100 Subject: [PATCH 3/8] Enhance error handling by implementing error recording and suppression across multiple modules --- src/py4vasp/_calculation/__init__.py | 49 ++++++++++++-- src/py4vasp/_calculation/base.py | 56 ++++++++++++++++ src/py4vasp/_calculation/cell.py | 12 +++- src/py4vasp/_calculation/current_density.py | 9 ++- src/py4vasp/_calculation/dielectric_tensor.py | 54 ++++++++++++--- src/py4vasp/_calculation/dos.py | 11 +++- src/py4vasp/_calculation/elastic_modulus.py | 66 ++++++++++++++++--- .../_calculation/electronic_minimization.py | 12 +++- .../_calculation/piezoelectric_tensor.py | 12 +++- src/py4vasp/_calculation/run_info.py | 13 +++- src/py4vasp/_calculation/structure.py | 17 +++-- src/py4vasp/_raw/data.py | 4 ++ src/py4vasp/_util/database.py | 2 +- 13 files changed, 279 insertions(+), 38 deletions(-) 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..5b8cb7c0 100644 --- a/src/py4vasp/_calculation/base.py +++ b/src/py4vasp/_calculation/base.py @@ -5,6 +5,7 @@ import functools import inspect import pathlib +from contextlib import suppress from typing import Any, Optional from py4vasp import exception, raw @@ -32,6 +33,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 +334,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 db35d702..6c5ce85a 100644 --- a/src/py4vasp/_calculation/cell.py +++ b/src/py4vasp/_calculation/cell.py @@ -12,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.""" @@ -128,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.""" - with suppress(exception.Py4VaspError): + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): lattice_vectors = self.lattice_vectors() dipole_direction = _idipol_to_direction( self._raw_data.idipol, self._raw_data.ldipol @@ -143,7 +151,7 @@ def _find_likely_vacuum_direction(self): return dipole_direction or int( np.argmax(np.linalg.norm(lattice_vectors, axis=-1)) ) - + return None diff --git a/src/py4vasp/_calculation/current_density.py b/src/py4vasp/_calculation/current_density.py index 2c483cd1..4eddb22f 100644 --- a/src/py4vasp/_calculation/current_density.py +++ b/src/py4vasp/_calculation/current_density.py @@ -15,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 @@ -106,7 +113,7 @@ def _read_current_density(self, key=None): def _to_database(self, *args, **kwargs): density_dict = {"current_density": {}} structure_ = {} - with suppress(exception.Py4VaspError): + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): structure_ = structure.Structure.from_data( self._raw_data.structure )._read_to_database(*args, **kwargs) diff --git a/src/py4vasp/_calculation/dielectric_tensor.py b/src/py4vasp/_calculation/dielectric_tensor.py index e5720bf5..5fefcfaa 100644 --- a/src/py4vasp/_calculation/dielectric_tensor.py +++ b/src/py4vasp/_calculation/dielectric_tensor.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 contextlib import suppress +from typing import Optional import numpy as np @@ -49,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] @@ -64,12 +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]): - with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): + 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) + ) = self._calculate_dielectric_quantities( + tensor, + encountered_errors=encountered_errors, + error_key=error_key, + ) method = ( convert.text_to_string(self._raw_data.method) @@ -96,15 +109,31 @@ 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 - with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): + 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) + polarizability_2d = _calculate_2d_polarizability( + tensor, + final_cell, + encountered_errors=encountered_errors, + error_key=error_key, + ) # 3D isotropic dielectric constant isotropic_dielectric_constant = None @@ -157,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. """ - with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): + 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 diff --git a/src/py4vasp/_calculation/dos.py b/src/py4vasp/_calculation/dos.py index 4e5b41f8..e3e965f3 100644 --- a/src/py4vasp/_calculation/dos.py +++ b/src/py4vasp/_calculation/dos.py @@ -14,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. @@ -226,7 +235,7 @@ def _to_database(self, *args, **kwargs): } def _dos_at_energy(self, energy): - with suppress(exception.Py4VaspError): + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): energies = self._raw_data.energies[:] dos_dict = self._read_total_dos() # interpolate between DOS at closest energies diff --git a/src/py4vasp/_calculation/elastic_modulus.py b/src/py4vasp/_calculation/elastic_modulus.py index 19cc2c3c..2017d2ea 100644 --- a/src/py4vasp/_calculation/elastic_modulus.py +++ b/src/py4vasp/_calculation/elastic_modulus.py @@ -1,6 +1,5 @@ # Copyright © VASP Software GmbH, # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) -from contextlib import suppress from math import pow from typing import Optional @@ -54,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, @@ -81,7 +84,12 @@ def _to_database(self, *args, **kwargs): for idt, tensor in enumerate([total_tensor, ionic_tensor, electronic_tensor]): voigt_tensor = None - with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): + 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 @@ -91,7 +99,12 @@ def _to_database(self, *args, **kwargs): else None ) - with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): + 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], @@ -102,7 +115,10 @@ 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, ) return { @@ -143,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, @@ -155,15 +176,30 @@ def _compute_elastic_properties( fracture_toughness, ) = (None, None, None, None, None, None, None) - with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): + 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) - with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): + 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() ) - with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): + 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 @@ -171,10 +207,20 @@ def _compute_elastic_properties( else 0.0 if shear_modulus == 0 else None ) - with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): + with base.suppress_and_record( + encountered_errors, + error_key, + *_TO_DATABASE_SUPPRESSED_EXCEPTIONS, + context="compute_elastic_properties.hardness", + ): vickers_hardness = elastic_tensor.get_hardness() - with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): + 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 ) diff --git a/src/py4vasp/_calculation/electronic_minimization.py b/src/py4vasp/_calculation/electronic_minimization.py index 11d02d12..b8625773 100644 --- a/src/py4vasp/_calculation/electronic_minimization.py +++ b/src/py4vasp/_calculation/electronic_minimization.py @@ -12,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. @@ -91,7 +99,7 @@ def _to_database(self, *args, **kwargs): elmin_is_converged_all = None elmin_is_converged_final = None - with suppress(exception.Py4VaspError): + 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) @@ -100,7 +108,7 @@ def _to_database(self, *args, **kwargs): self._raw_data.is_elmin_converged[-1] == 0.0 ) - with suppress(exception.NoData): + with suppress(exception.NoData, *_TO_DATABASE_SUPPRESSED_EXCEPTIONS): ( num_max_electronic_steps_per_ionic, num_min_electronic_steps_per_ionic, diff --git a/src/py4vasp/_calculation/piezoelectric_tensor.py b/src/py4vasp/_calculation/piezoelectric_tensor.py index 8c09f23b..4f6530e7 100644 --- a/src/py4vasp/_calculation/piezoelectric_tensor.py +++ b/src/py4vasp/_calculation/piezoelectric_tensor.py @@ -11,6 +11,14 @@ 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. @@ -85,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) - with suppress(exception.Py4VaspError): + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): e_tensor = _extract_tensor( tensor ) # 3x6 tensor, column order: XX YY ZZ YZ ZX XY @@ -100,7 +108,7 @@ def _to_database(self, *args, **kwargs): if in_plane[idt] is not None and lvac is not None: tensor_2d[idt] = e_tensor * lvac - with suppress(exception.Py4VaspError): + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): ( e11[idt], e22[idt], diff --git a/src/py4vasp/_calculation/run_info.py b/src/py4vasp/_calculation/run_info.py index 37221247..4015fc6d 100644 --- a/src/py4vasp/_calculation/run_info.py +++ b/src/py4vasp/_calculation/run_info.py @@ -14,6 +14,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." @@ -90,12 +99,12 @@ def _is_noncollinear(self): return None def _is_metallic(self): - with suppress(exception.Py4VaspError, exception.OutdatedVaspVersion, exception.NoData): + 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) - + return None def _dict_from_system(self) -> dict: diff --git a/src/py4vasp/_calculation/structure.py b/src/py4vasp/_calculation/structure.py index e5bfd2e1..15c0e71e 100644 --- a/src/py4vasp/_calculation/structure.py +++ b/src/py4vasp/_calculation/structure.py @@ -19,6 +19,15 @@ __all__ = ["Structure"] +_TO_DATABASE_SUPPRESSED_EXCEPTIONS = ( + exception.Py4VaspError, + np.linalg.LinAlgError, + AttributeError, + TypeError, + ValueError, + IndexError, +) + @dataclass class _Format: @@ -264,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)) - with suppress(exception.Py4VaspError): + 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 @@ -274,7 +283,7 @@ def _to_database(self, *args, **kwargs): initial_lattice = [None, None, None] volume_final, volume_initial = (None for _ in range(2)) - with suppress(exception.Py4VaspError): + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): volumes = self.volume() volume_final = ( volumes[-1] @@ -297,10 +306,10 @@ def _to_database(self, *args, **kwargs): cell_area_2d_span_initial, ) = (None for _ in range(4)) dimensionality = 3 - with suppress(exception.Py4VaspError): + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): dimensionality = self._dimensionality() - with suppress(exception.Py4VaspError): + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): cell_: cell.Cell = self._cell() lengths = cell_.lengths() lengths_final = lengths[-1] if lengths.ndim == 2 else lengths 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/_util/database.py b/src/py4vasp/_util/database.py index e5762988..f2357590 100644 --- a/src/py4vasp/_util/database.py +++ b/src/py4vasp/_util/database.py @@ -358,7 +358,7 @@ def _get_dataclass_field_docstrings(dataclass: Any) -> Dict[str, Optional[str]]: continue docstrings[field_name] = _extract_following_docstring(class_body, index) return docstrings - + return {} From d1ca2479c60fc49f007295dc92bc1728ca865ba5 Mon Sep 17 00:00:00 2001 From: Max Liebetreu Date: Tue, 7 Apr 2026 14:52:02 +0200 Subject: [PATCH 4/8] Fix: run_info fermi_energy should be `float | None` instead of `VaspData` --- src/py4vasp/_calculation/run_info.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/py4vasp/_calculation/run_info.py b/src/py4vasp/_calculation/run_info.py index 4015fc6d..f485338c 100644 --- a/src/py4vasp/_calculation/run_info.py +++ b/src/py4vasp/_calculation/run_info.py @@ -6,6 +6,7 @@ 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 @@ -52,6 +53,8 @@ def _dict_additional_collection(self) -> dict: fermi_energy = None with suppress(exception.NoData): fermi_energy = self._raw_data.fermi_energy + if isinstance(fermi_energy, VaspData): + fermi_energy = fermi_energy._data is_success = None # TODO implement From d81b0cf04145b72bc347c110cf527f7bf7511e99 Mon Sep 17 00:00:00 2001 From: Max Liebetreu Date: Tue, 7 Apr 2026 15:19:31 +0200 Subject: [PATCH 5/8] Fix: Robustify wrapper around database dataclasses so that VaspData objects are converted to underlying data --- src/py4vasp/_raw/data_db.py | 7 +++++++ tests/util/test_database.py | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+) 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/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 From 8d133d2b56cd7b1df2ffdf74b46862d0368dc125 Mon Sep 17 00:00:00 2001 From: Max Liebetreu Date: Tue, 7 Apr 2026 17:03:40 +0200 Subject: [PATCH 6/8] fix: Remove unused import --- src/py4vasp/_calculation/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/py4vasp/_calculation/base.py b/src/py4vasp/_calculation/base.py index 5b8cb7c0..4580292c 100644 --- a/src/py4vasp/_calculation/base.py +++ b/src/py4vasp/_calculation/base.py @@ -5,7 +5,6 @@ import functools import inspect import pathlib -from contextlib import suppress from typing import Any, Optional from py4vasp import exception, raw From ecaf85c884878cb8c4ab2e13a4c2dbb9b8ddc157 Mon Sep 17 00:00:00 2001 From: Max Liebetreu Date: Wed, 8 Apr 2026 12:53:57 +0200 Subject: [PATCH 7/8] feat: improve Kpoint_DB robustness --- src/py4vasp/_calculation/kpoint.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/py4vasp/_calculation/kpoint.py b/src/py4vasp/_calculation/kpoint.py index d112e00c..7a5eb7a3 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,11 @@ of the default one defined by the KPOINTS file. """ +_TO_DATABASE_SUPPRESSED_EXCEPTIONS = ( + exception.Py4VaspError, + exception.RefinementError, +) + class Kpoint(base.Refinery): """The **k**-point mesh used in the VASP calculation. @@ -124,12 +130,22 @@ 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, line_length, num_kpoints_total, num_lines = None, None, None, None + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): + mode = self.mode() + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): + line_length = self.line_length() + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): + num_kpoints_total = self.number_kpoints() + with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): + num_lines = 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, From 44dc229c660ae2e2099dea92b258f5cbd6163d2f Mon Sep 17 00:00:00 2001 From: Max Liebetreu Date: Wed, 8 Apr 2026 14:06:43 +0200 Subject: [PATCH 8/8] fix: cleaner way to use suppress syntax --- src/py4vasp/_calculation/kpoint.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/py4vasp/_calculation/kpoint.py b/src/py4vasp/_calculation/kpoint.py index 7a5eb7a3..7a94fd2a 100644 --- a/src/py4vasp/_calculation/kpoint.py +++ b/src/py4vasp/_calculation/kpoint.py @@ -26,6 +26,12 @@ ) +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. @@ -130,15 +136,10 @@ 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, line_length, num_kpoints_total, num_lines = None, None, None, None - with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): - mode = self.mode() - with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): - line_length = self.line_length() - with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): - num_kpoints_total = self.number_kpoints() - with suppress(*_TO_DATABASE_SUPPRESSED_EXCEPTIONS): - num_lines = self.number_lines() + 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(