Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions src/py4vasp/_calculation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,12 +380,10 @@ def __dir__(self):
def _compute_database_data(self) -> dict:
"""Iterate over all quantities in _REGISTRY and collect database properties.

Returns a flat dict mapping property keys to handler result dicts.
Keys use the format:
- ``<quantity>`` for the default selection
- ``<quantity>_<selection>`` for non-default selections
- ``<group>_<quantity>`` / ``<group>_<quantity>_<selection>`` for groups
Leading underscores are stripped from private quantity names.
Returns a nested dict ``{quantity: {selection: model}}``. The outer key is
the (underscore-stripped) quantity name; the inner dict is keyed by
selection with the default source keyed ``"default"``. Group members use
``<group>_<quantity>`` (e.g. ``phonon_mode``) as their outer key.
"""
_ensure_all_quantities_imported()
properties = {}
Expand Down Expand Up @@ -435,11 +433,12 @@ def _ensure_all_quantities_imported():


def _collect_to_database(dispatcher_cls, source, properties):
"""Call dispatcher._to_database() and merge results into *properties*.
"""Call dispatcher._to_database() and deep-merge results into *properties*.

Each dispatcher already keys its results by the full quantity name; group members
use ``<group>_<member>`` via their ``_quantity_name`` (e.g. ``phonon_mode``), so the
results merge directly without any additional group prefix.
Each dispatcher returns a nested ``{quantity: {selection: model}}`` dict keyed by
the full quantity name; group members use ``<group>_<member>`` via their
``_quantity_name`` (e.g. ``phonon_mode``). The nested dicts are merged so that
quantities and their selections accumulate without clobbering each other.
"""
dispatcher = dispatcher_cls(
source=source, quantity_name=dispatcher_cls._quantity_name
Expand All @@ -452,7 +451,8 @@ def _collect_to_database(dispatcher_cls, source, properties):
return
except Exception:
return
properties.update(result)
for quantity, selections in result.items():
properties.setdefault(quantity, {}).update(selections)


def _rebuild_public_registry_views():
Expand Down
12 changes: 3 additions & 9 deletions src/py4vasp/_calculation/_dispersion.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
DataSource,
merge_default,
merge_strings,
merge_to_database,
quantity,
)
from py4vasp._calculation.kpoint import KpointHandler
Expand Down Expand Up @@ -146,14 +145,9 @@ def plot(self, projections=None):
projections,
)

def _to_database(self) -> dict:
"""Return {quantity[_selection]: handler_result} for database storage."""
return merge_to_database(
self._source,
self._quantity_name,
DispersionHandler.from_data,
DispersionHandler.to_database,
)
# Dispersion has no standalone database entry: its data is folded into the band
# and phonon_band models (see BandHandler / PhononBandHandler.to_database), so the
# dispatcher deliberately exposes no `_to_database`.


def _band_structure(data, projections):
Expand Down
12 changes: 3 additions & 9 deletions src/py4vasp/_calculation/_stoichiometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
DataSource,
merge_default,
merge_strings,
merge_to_database,
quantity,
)
from py4vasp._calculation.selection import Selection
Expand Down Expand Up @@ -417,14 +416,9 @@ def number_atoms(self):
StoichiometryHandler.number_atoms,
)

def _to_database(self) -> dict:
"""Return {quantity[_selection]: handler_result} for database storage."""
return merge_to_database(
self._source,
self._quantity_name,
StoichiometryHandler.from_data,
StoichiometryHandler.to_database,
)
# Stoichiometry has no standalone database entry: its data is folded into the
# structure model (see StructureHandler.to_database), so the dispatcher
# deliberately exposes no `_to_database`.


def raw_stoichiometry_from_ase(structure):
Expand Down
8 changes: 8 additions & 0 deletions src/py4vasp/_calculation/band.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,21 @@ def to_database(self, selection=None, fermi_energy=None) -> Band_DB:
else None
)

dispersion = self._dispersion().to_database()

return Band_DB(
num_considered_bands=num_checked_bands,
num_occupied_bands=num_total_occupied,
num_occupied_bands_up=num_occupied_up,
num_occupied_bands_down=num_occupied_down,
fermi_energy_raw=raw_fermi_energy,
fermi_energy=fermi_energy or raw_fermi_energy,
eigenvalue_min=dispersion.eigenvalue_min,
eigenvalue_max=dispersion.eigenvalue_max,
eigenvalue_min_up=dispersion.eigenvalue_min_up,
eigenvalue_max_up=dispersion.eigenvalue_max_up,
eigenvalue_min_down=dispersion.eigenvalue_min_down,
eigenvalue_max_down=dispersion.eigenvalue_max_down,
)

def to_graph(self, selection=None, fermi_energy=None, width=0.5) -> graph.Graph:
Expand Down
28 changes: 25 additions & 3 deletions src/py4vasp/_calculation/current_density.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
)
from py4vasp._calculation.structure import StructureHandler
from py4vasp._raw import data as raw
from py4vasp._raw.data_db import CurrentDensity_DB
from py4vasp._third_party import graph
from py4vasp._util import documentation, import_, slicing
from py4vasp._util import check, documentation, import_, slicing
from py4vasp._util.density import SliceArguments, Visualizer

pretty = import_.optional("IPython.lib.pretty")
Expand Down Expand Up @@ -62,8 +63,29 @@ def to_dict(self) -> dict:
**self._read_current_densities(),
}

def to_database(self) -> dict:
return {}
def to_database(self) -> "CurrentDensity_DB":
"""Serialize a scalar summary of the current density for database storage."""
grid_shape, magnitudes = None, []
for key in self._raw_current_density.valid_indices:
current_density = self._raw_current_density[key].current_density
if check.is_none(current_density):
continue
# current_density has shape (3, nz, ny, nx); |j| is the norm over the
# vector component axis, leaving the grid.
vector_field = np.array(current_density[:])
magnitudes.append(np.linalg.norm(vector_field, axis=0).ravel())
if grid_shape is None:
nz, ny, nx = vector_field.shape[1:]
grid_shape = [nx, ny, nz]
if not magnitudes:
return CurrentDensity_DB()
magnitude = np.concatenate(magnitudes)
return CurrentDensity_DB(
grid_shape=grid_shape,
magnitude_min=float(np.min(magnitude)),
magnitude_max=float(np.max(magnitude)),
magnitude_mean=float(np.mean(magnitude)),
)

def to_contour(
self,
Expand Down
32 changes: 16 additions & 16 deletions src/py4vasp/_calculation/dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,9 +389,10 @@ def merge_to_database(

Returns
-------
dict[str, result]
Maps ``quantity_name`` (default source) or ``quantity_name_source``
(other sources) to each handler result.
dict[str, dict[str, result]]
Nested ``{quantity: {selection: result}}``. The outer key is the
(underscore-stripped) quantity name; the inner dict is keyed by selection
with the default source keyed ``"default"``. Empty quantities are omitted.
"""
wrapped_source = SuppressErrorsSourceWrapper(source)
base = (key_name or quantity_name).lstrip("_")
Expand All @@ -405,12 +406,11 @@ def merge_to_database(
*args,
**kwargs,
)
results = {
base if sel == "default" else f"{base}_{sel}": result
for sel, result in raw_results.items()
if _result_has_data(result)
selections = {
sel: result for sel, result in raw_results.items() if _result_has_data(result)
}
return _drop_duplicates_of_default(results, base)
selections = _drop_duplicates_of_default(selections)
return {base: selections} if selections else {}


def _all_sources_selection(quantity_name):
Expand All @@ -426,15 +426,15 @@ def _all_sources_selection(quantity_name):
return ", ".join(str(source_name) for source_name in sources)


def _drop_duplicates_of_default(results, base):
"""Drop non-default entries whose result equals the default result."""
if base not in results:
return results
default_result = results[base]
def _drop_duplicates_of_default(selections):
"""Drop non-default selections whose result equals the ``default`` result."""
if "default" not in selections:
return selections
default_result = selections["default"]
return {
key: result
for key, result in results.items()
if key == base or result != default_result
sel: result
for sel, result in selections.items()
if sel == "default" or result != default_result
}


Expand Down
40 changes: 38 additions & 2 deletions src/py4vasp/_calculation/pair_correlation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
import copy

import numpy as np

from py4vasp import raw
from py4vasp._calculation import slice_
from py4vasp._calculation.dispatch import (
Expand All @@ -17,6 +19,11 @@
from py4vasp._third_party import graph
from py4vasp._util import check, convert, documentation, index, select

# Minimum height of the total pair correlation function for a local maximum to be
# considered the first peak. The value corresponds to the ideal-gas baseline, so
# small bumps below it (e.g. numerical noise before the first shell) are ignored.
_FIRST_PEAK_THRESHOLD = 1.0


def _selection_string(default):
return f"""\
Expand Down Expand Up @@ -62,13 +69,42 @@ def labels(self) -> tuple:
"""Return all possible labels for the selection string."""
return tuple(convert.text_to_string(label) for label in self._raw_data.labels)

def to_database(self) -> dict:
def to_database(self) -> PairCorrelation_DB:
"""Serialize pair-correlation data for database storage."""
distance_min, distance_max = None, None
if not check.is_none(self._raw_data.distances):
distance_min = float(self._raw_data.distances[0])
distance_max = float(self._raw_data.distances[-1])
return PairCorrelation_DB(distance_min=distance_min, distance_max=distance_max)
first_peak_position, first_peak_height = self._first_peak()
return PairCorrelation_DB(
distance_min=distance_min,
distance_max=distance_max,
first_peak_position=first_peak_position,
first_peak_height=first_peak_height,
)

def _first_peak(self):
"""Position and height of the first peak of the total pair correlation.

Scans the existing sample points for the first strict local maximum of the
total g(r) whose height exceeds :data:`_FIRST_PEAK_THRESHOLD`, so small
bumps before the first real shell are skipped. Returns ``(None, None)`` when
no such peak exists.
"""
if check.is_none(self._raw_data.distances) or check.is_none(
self._raw_data.function
):
return None, None
distances = np.asarray(self._raw_data.distances[:])
# index 0 of the function is the total pair correlation by convention
total = np.asarray(self._raw_data.function)[self._steps_or_last, 0]
if distances.shape != total.shape or total.size < 3:
return None, None
for i in range(1, total.size - 1):
is_local_max = total[i] > total[i - 1] and total[i] > total[i + 1]
if is_local_max and total[i] > _FIRST_PEAK_THRESHOLD:
return float(distances[i]), float(total[i])
return None, None

@property
def _steps_or_last(self):
Expand Down
9 changes: 7 additions & 2 deletions src/py4vasp/_calculation/phonon_band.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
merge_to_database,
quantity,
)
from py4vasp._raw.data_db import PhononBand_DB
from py4vasp._third_party import graph
from py4vasp._util import convert, documentation, index, select

Expand Down Expand Up @@ -46,8 +47,12 @@ def to_dict(self) -> dict:
"modes": self._modes(),
}

def to_database(self) -> dict:
return {}
def to_database(self) -> PhononBand_DB:
dispersion = self._dispersion().to_database()
return PhononBand_DB(
eigenvalue_min=dispersion.eigenvalue_min,
eigenvalue_max=dispersion.eigenvalue_max,
)

def to_graph(self, selection=None, width=1.0) -> graph.Graph:
projections = self._projections(selection, width)
Expand Down
Loading
Loading