Skip to content
Merged
49 changes: 44 additions & 5 deletions src/py4vasp/_calculation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -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():
Expand All @@ -371,6 +388,7 @@ def _compute_database_data(
quantities,
available_quantities,
additional_properties,
encountered_errors,
group_name=group,
fermi_energy=fermi_energy,
)
Expand All @@ -383,14 +401,15 @@ 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,
hdf5_path: pathlib.Path,
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]]:
Expand All @@ -414,6 +433,7 @@ def _loop_quantities(
quantity,
group_name,
additional_properties,
encountered_errors,
fermi_energy=fermi_energy,
)
)
Expand Down Expand Up @@ -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."
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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,
)
Expand All @@ -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,
Expand Down
55 changes: 55 additions & 0 deletions src/py4vasp/_calculation/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
15 changes: 12 additions & 3 deletions src/py4vasp/_calculation/cell.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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."""
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down
13 changes: 10 additions & 3 deletions src/py4vasp/_calculation/current_density.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading