Skip to content

Commit

Permalink
Added validate_quantity_wrt_value decorator for PhysicalProperty
Browse files Browse the repository at this point in the history
Fixed test_band_structure

Fixed methods in ElectronicEigenvalues

Added .vscode/settings.json to gitignore
  • Loading branch information
JosePizarro3 committed Jun 3, 2024
1 parent b5781a9 commit 7e841f2
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 126 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ cython_debug/

# VSCode settings
.vscode/launch.json
.vscode/settings.json

# comments scripts
comments.py
Expand Down
24 changes: 0 additions & 24 deletions .vscode/settings.json

This file was deleted.

1 change: 1 addition & 0 deletions src/nomad_simulations/numerical_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,7 @@ def resolve_reciprocal_lattice_vectors(
if is_not_representative(model_system, logger):
continue

# TODO extend this for other dimensions (@ndaelman-hu)
if model_system.type is not None and model_system.type != 'bulk':
logger.warning('`ModelSystem.type` is not describing a bulk system.')
continue
Expand Down
36 changes: 36 additions & 0 deletions src/nomad_simulations/physical_property.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import numpy as np
from typing import Any, Optional
from functools import wraps

from nomad import utils
from nomad.datamodel.data import ArchiveSection
Expand All @@ -42,6 +43,41 @@
logger = utils.get_logger(__name__)


def validate_quantity_wrt_value(name: str = ''):
"""
Decorator to validate the existence of a quantity and its shape with respect to the `PhysicalProperty.value` before calling a method.
Args:
name (str, optional): The name of the `quantity` to validate. Defaults to ''.
"""

def decorator(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
# Checks if `quantity` is defined
quantity = getattr(self, name, None)
if quantity is None or len(quantity) == 0:
logger.warning(f'The quantity `{name}` is not defined.')
return False

# Checks if `value` exists and has the same shape as `quantity`
value = getattr(self, 'value', None)
if value is None:
logger.warning(f'The quantity `value` is not defined.')
return False
if value is not None and value.shape != quantity.shape:
logger.warning(
f'The shape of the quantity `{name}` does not match the shape of the `value`.'
)
return False

return func(self, *args, **kwargs)

return wrapper

return decorator


class PhysicalProperty(ArchiveSection):
"""
A base section used to define the physical properties obtained in a simulation, experiment, or in a post-processing
Expand Down
96 changes: 28 additions & 68 deletions src/nomad_simulations/properties/band_structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,23 @@

import numpy as np
from structlog.stdlib import BoundLogger
from typing import Optional, Tuple
from typing import Optional, Tuple, Union
import pint

from nomad.metainfo import Quantity, Section, Context, SubSection

from nomad_simulations.numerical_settings import KSpace
from nomad_simulations.physical_property import PhysicalProperty
from nomad_simulations.physical_property import (
PhysicalProperty,
validate_quantity_wrt_value,
)
from nomad_simulations.properties import ElectronicBandGap, FermiSurface
from nomad_simulations.utils import get_sibling_section


class BaseElectronicEigenvalues(PhysicalProperty):
"""
A base section used to define basic quantities for the `ElectronicEigenvalues`, `FermiSurface`, and
`ElectronicBandStructure` properties. This section serves to define `FermiSurface` and without needing to specify
other quantities that appear in `ElectronicEigenvalues`
A base section used to define basic quantities for the `ElectronicEigenvalues` and `ElectronicBandStructure` properties.
"""

iri = ''
Expand Down Expand Up @@ -80,13 +81,12 @@ class ElectronicEigenvalues(BaseElectronicEigenvalues):
type=np.float64,
shape=['*', 'n_bands'],
description="""
Occupation of the electronic eigenvalues. This is a number between 0 and 2, where 0 means
that the state is unoccupied and 2 means that the state is fully occupied. It is controlled
by the Fermi-Dirac distribution:
$ f(E) = 1 / (1 + exp((E - E_F) / kT)) $
The shape of this quantity is defined as `[KMesh.n_points, KMesh.dimensionality, n_bands]`, where `KMesh` is a `variable`.
Occupation of the electronic eigenvalues. This is a number depending whether the `spin_channel` has been set or not.
If `spin_channel` is set, then this number is between 0 and 2, where 0 means that the state is unoccupied and 2 means
that the state is fully occupied; if `spin_channel` is not set, then this number is between 0 and 1. The shape of
this quantity is defined as `[K.n_points, K.dimensionality, n_bands]`, where `K` is a `variable` which can
be `KMesh` or `KLinePath`, depending whether the simulation mapped the whole Brillouin zone or just a specific
path.
""",
)

Expand All @@ -108,6 +108,9 @@ class ElectronicEigenvalues(BaseElectronicEigenvalues):
""",
)

# ? Should we add functionalities to handle min/max of the `value` in some specific cases, .e.g, bands around the Fermi level,
# ? core bands separated by gaps, and equivalently, higher-energy valence bands separated by gaps?

value_contributions = SubSection(
sub_section=BaseElectronicEigenvalues.m_def,
repeats=True,
Expand All @@ -125,8 +128,7 @@ class ElectronicEigenvalues(BaseElectronicEigenvalues):
reciprocal_cell = Quantity(
type=KSpace.reciprocal_lattice_vectors,
description="""
Reference to the reciprocal lattice vectors stored under `KSpace`. This reference is useful when resolving the Brillouin zone
for the front-end visualization.
Reference to the reciprocal lattice vectors stored under `KSpace`.
""",
)

Expand All @@ -136,48 +138,16 @@ def __init__(
super().__init__(m_def, m_context, **kwargs)
self.name = self.m_def.name

def validate_occupation(self, logger: BoundLogger) -> bool:
"""
Validate `occupation` by checking if they exist and have the same shape as `value`.
Args:
logger (BoundLogger): The logger to log messages.
Returns:
(bool): True if the shape of `occupation` is the same as `value`, False otherwise.
"""
if self.occupation is None or len(self.occupation) == 0:
logger.warning('Cannot find `occupation` defined.')
return False
if self.value is not None and self.value.shape != self.occupation.shape:
logger.warning(
'The shape of `value` and `occupation` are different. They should have the same shape.'
)
return False
return True

def order_eigenvalues(
self, logger: BoundLogger
) -> Tuple[Optional[pint.Quantity], Optional[np.ndarray]]:
@validate_quantity_wrt_value(name='occupation')
def order_eigenvalues(self) -> Union[bool, Tuple[pint.Quantity, np.ndarray]]:
"""
Order the eigenvalues based on the `value` and `occupation`. The return `value` and
`occupation` are flattened.
Args:
logger (BoundLogger): The logger to log messages.
Returns:
(Tuple[Optional[pint.Quantity], Optional[list]]): The flattened and sorted `value` and `occupation`.
(Union[bool, Tuple[pint.Quantity, np.ndarray]]): The flattened and sorted `value` and `occupation`. If validation
fails, then it returns `False`.
"""

# Check if `value` exists
if self.value is None or len(self.value) == 0:
logger.error('Could not find `value` defined.')
return None, None

# Check if `occupation` exists and have the same shape as `value`
if not self.validate_occupation(logger):
return None, None
total_shape = np.prod(self.value.shape)

# Order the indices in the flattened list of `value`
Expand All @@ -196,26 +166,23 @@ def order_eigenvalues(
return sorted_value, sorted_occupation

def resolve_homo_lumo_eigenvalues(
self, logger: BoundLogger
self,
) -> Tuple[Optional[pint.Quantity], Optional[pint.Quantity]]:
"""
Resolve the `highest_occupied` and `lowest_unoccupied` eigenvalues by performing a binary search on the
flattened and sorted `value` and `occupation`. If these quantities already exist, overwrite them or return
them if it is not possible to resolve from `value` and `occupation`.
Args:
logger (BoundLogger): The logger to log messages.
Returns:
(Tuple[Optional[pint.Quantity], Optional[pint.Quantity]]): The `highest_occupied` and
`lowest_unoccupied` eigenvalues.
"""
# Sorting `value` and `occupation`
sorted_value, sorted_occupation = self.order_eigenvalues(logger)
if sorted_value is None or sorted_occupation is None:
if not self.order_eigenvalues(): # validation fails
if self.highest_occupied is not None and self.lowest_unoccupied is not None:
return self.highest_occupied, self.lowest_unoccupied
return None, None
sorted_value, sorted_occupation = self.order_eigenvalues()
sorted_value_unit = sorted_value.u
sorted_value = sorted_value.magnitude

Expand Down Expand Up @@ -244,19 +211,16 @@ def resolve_homo_lumo_eigenvalues(

return homo, lumo

def extract_band_gap(self, logger: BoundLogger) -> Optional[ElectronicBandGap]:
def extract_band_gap(self) -> Optional[ElectronicBandGap]:
"""
Extract the electronic band gap from the `highest_occupied` and `lowest_unoccupied` eigenvalues.
If the difference of `highest_occupied` and `lowest_unoccupied` is negative, the band gap `value` is set to 0.0.
Args:
logger (BoundLogger): The logger to log messages.
Returns:
(Optional[ElectronicBandGap]): The extracted electronic band gap section to be stored in `Outputs`.
"""
band_gap = None
homo, lumo = self.resolve_homo_lumo_eigenvalues(logger)
homo, lumo = self.resolve_homo_lumo_eigenvalues()
if homo and lumo:
band_gap = ElectronicBandGap(is_derived=True, physical_property_ref=self)

Expand All @@ -278,7 +242,7 @@ def extract_fermi_surface(self, logger: BoundLogger) -> Optional[FermiSurface]:
(Optional[FermiSurface]): The extracted Fermi surface section to be stored in `Outputs`.
"""
# Check if the system has a finite band gap
homo, lumo = self.resolve_homo_lumo_eigenvalues(logger)
homo, lumo = self.resolve_homo_lumo_eigenvalues()
if (homo and lumo) and (lumo - homo).magnitude > 0:
return None

Expand Down Expand Up @@ -336,17 +300,13 @@ def resolve_reciprocal_cell(self) -> Optional[pint.Quantity]:
def normalize(self, archive, logger) -> None:
super().normalize(archive, logger)

# Check if `occupation` exists and has the same shape as `value`
if not self.validate_occupation(logger):
return

# Resolve `highest_occupied` and `lowest_unoccupied` eigenvalues
self.highest_occupied, self.lowest_unoccupied = (
self.resolve_homo_lumo_eigenvalues(logger)
self.resolve_homo_lumo_eigenvalues()
)

# `ElectronicBandGap` extraction
band_gap = self.extract_band_gap(logger)
band_gap = self.extract_band_gap()
if band_gap is not None:
self.m_parent.electronic_band_gaps.append(band_gap)

Expand Down
Loading

0 comments on commit 7e841f2

Please sign in to comment.