From d8a5d9f58858681a7d1237429a020d2f3680adcb Mon Sep 17 00:00:00 2001 From: Nathan Daelman Date: Mon, 18 Nov 2024 00:11:46 +0100 Subject: [PATCH] - Apply new `PhysicalProperty` def to `band_gap` - Add testing --- .../schema_packages/properties/band_gap.py | 159 ++++++------------ tests/properties/test_band_gap.py | 145 ++++++---------- 2 files changed, 97 insertions(+), 207 deletions(-) diff --git a/src/nomad_simulations/schema_packages/properties/band_gap.py b/src/nomad_simulations/schema_packages/properties/band_gap.py index 05c1ba57..297effa6 100644 --- a/src/nomad_simulations/schema_packages/properties/band_gap.py +++ b/src/nomad_simulations/schema_packages/properties/band_gap.py @@ -2,143 +2,78 @@ import numpy as np import pint +from nomad.units import ureg from nomad.metainfo import MEnum, Quantity +from nomad.metainfo.dataset import MDataset, Dataset +from nomad.datamodel import EntryArchive +from nomad.datamodel.metainfo.physical_properties import PhysicalProperty if TYPE_CHECKING: from nomad.datamodel.datamodel import EntryArchive - from nomad.metainfo import Context, Section from structlog.stdlib import BoundLogger -from nomad_simulations.schema_packages.physical_property import PhysicalProperty - -class ElectronicBandGap(PhysicalProperty): - """ - Energy difference between the highest occupied electronic state and the lowest unoccupied electronic state. - """ - - # ! implement `iri` and `rank` as part of `m_def = Section()` - - iri = 'http://fairmat-nfdi.eu/taxonomy/ElectronicBandGap' - - type = Quantity( - type=MEnum('direct', 'indirect'), - description=""" - Type categorization of the electronic band gap. This quantity is directly related with `momentum_transfer` as by - definition, the electronic band gap is `'direct'` for zero momentum transfer (or if `momentum_transfer` is `None`) and `'indirect'` - for finite momentum transfer. - - Note: in the case of finite `variables`, this quantity refers to all of the `value` in the array. - """, +class SpinChannel(MDataset): + m_def = Dataset( + type=MEnum('up', 'down', 'all'), # ? alpha, beta ) - momentum_transfer = Quantity( + +class MomentumTransfer(MDataset): + m_def = Dataset( type=np.float64, shape=[2, 3], + unit='1/meter', description=""" - If the electronic band gap is `'indirect'`, the reciprocal momentum transfer for which the band gap is defined - in units of the `reciprocal_lattice_vectors`. The initial and final momentum 3D vectors are given in the first - and second element. Example, the momentum transfer in bulk Si2 happens between the Γ and the (approximately) - X points in the Brillouin zone; thus: - `momentum_transfer = [[0, 0, 0], [0.5, 0.5, 0]]`. + The change in momentum for any (quasi-)particle, e.g. electron, hole, + traversing the band gap. - Note: this quantity only refers to scalar `value`, not to arrays of `value`. + For example, the momentum transfer in bulk Si happens + between the Γ and X points in the Brillouin zone; thus: + `momentum_transfer = [[0, 0, 0], [0.5, 0.5, 0]]`. """, ) - spin_channel = Quantity( - type=np.int32, - description=""" - Spin channel of the corresponding electronic band gap. It can take values of 0 or 1. - """, - ) - value = Quantity( +class ElectronicBandGap(MDataset): # ? add optical band gap + m_def = PhysicalProperty( type=np.float64, unit='joule', + iri='http://fairmat-nfdi.eu/taxonomy/ElectronicBandGap', + description="""Energy difference between the highest occupied electronic state and the lowest unoccupied electronic state.""", + default_variables=['SpinChannel', 'MomentumTransfer'], + ) + + type = Quantity( + type=MEnum('direct', 'indirect'), description=""" - The value of the electronic band gap. This value has to be positive, otherwise it will - prop an error and be set to None by the `normalize()` function. + Type categorization of the electronic band gap. This quantity is directly related with `momentum_transfer` as by + definition, the electronic band gap is `'direct'` for zero momentum transfer (or if `momentum_transfer` is `None`) and `'indirect'` + for finite momentum transfer. + + Note: in the case of finite `variables`, this quantity refers to all of the `value` in the array. """, ) - def __init__( - self, m_def: 'Section' = None, m_context: 'Context' = None, **kwargs - ) -> None: - super().__init__(m_def, m_context, **kwargs) - self.name = self.m_def.name - self.rank = [] - - def validate_values(self, logger: 'BoundLogger') -> Optional[pint.Quantity]: - """ - Validate the electronic band gap `value` by checking if they are negative and sets them to None if they are. - - Args: - logger (BoundLogger): The logger to log messages. - """ - value = self.value.magnitude - if not isinstance(self.value.magnitude, np.ndarray): # for scalars - value = np.array( - [value] - ) # ! check this when talking with Lauri and Theodore - - # Set the value to 0 when it is negative - if (value < 0).any(): - logger.error('The electronic band gap cannot be defined negative.') - return None - - if not isinstance(self.value.magnitude, np.ndarray): # for scalars - value = value[0] - return value * self.value.u - - def resolve_type(self, logger: 'BoundLogger') -> Optional[str]: - """ - Resolves the `type` of the electronic band gap based on the stored `momentum_transfer` values. - - Args: - logger (BoundLogger): The logger to log messages. - - Returns: - (Optional[str]): The resolved `type` of the electronic band gap. - """ - mtr = self.momentum_transfer if self.momentum_transfer is not None else [] - - # Check if the `momentum_transfer` is [], and return the type and a warning in the log for `indirect` band gaps - if len(mtr) == 0: - if self.type == 'indirect': - logger.warning( - 'The `momentum_transfer` is not stored for an `indirect` band gap.' - ) - return self.type + def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: + # super().normalize(archive, logger) - # Check if the `momentum_transfer` has at least two elements, and return None if it does not - if len(mtr) == 1: + if np.any(self.data < 0): + logger.warning(f'Negative band gap detected: {self.data} J') + + if [True for var in self.variables if isinstance(var, SpinChannel)]: logger.warning( - 'The `momentum_transfer` should have at least two elements so that the difference can be calculated and the type of electronic band gap can be resolved.' + f'Band gap without specifying any spin channel: {self.variables}' ) - return None - # Resolve `type` from the difference between the initial and final momentum transfer - momentum_difference = np.diff(mtr, axis=0) - if (np.isclose(momentum_difference, np.zeros(3))).all(): - return 'direct' - else: - return 'indirect' + if not [True for var in self.variables if isinstance(var, MomentumTransfer)]: + if self.type == 'direct': + self.variables.append( + MomentumTransfer(data=[2 * [3 * [0.0]]] * ureg.angstrom**-1) + ) + elif self.type == 'indirect': + logger.warning( + f'Indirect band gap without specifying any momentum transfer: {self.variables}' + ) - def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: - super().normalize(archive, logger) - - # Checks if the `value` is negative and sets it to None if it is. - self.value = self.validate_values(logger) - if self.value is None: - # ? What about deleting the class if `value` is None? - logger.error('The `value` of the electronic band gap is not stored.') - return - - # Resolve the `type` of the electronic band gap from `momentum_transfer`, ONLY for scalar `value` - if isinstance(self.value.magnitude, np.ndarray): - logger.info( - 'We do not support `type` which describe individual elements in an array `value`.' - ) - else: - self.type = self.resolve_type(logger) + # ? how to enforce a fixed ordering diff --git a/tests/properties/test_band_gap.py b/tests/properties/test_band_gap.py index 48939ea8..3b4234e7 100644 --- a/tests/properties/test_band_gap.py +++ b/tests/properties/test_band_gap.py @@ -5,104 +5,59 @@ from nomad.datamodel import EntryArchive from nomad.units import ureg -from nomad_simulations.schema_packages.properties import ElectronicBandGap -from nomad_simulations.schema_packages.variables import Temperature +from nomad_simulations.schema_packages.properties.band_gap import ( + ElectronicBandGap, + SpinChannel, + MomentumTransfer, +) +# from nomad_simulations.schema_packages.variables import Temperature from . import logger -class TestElectronicBandGap: - """ - Test the `ElectronicBandGap` class defined in `properties/band_gap.py`. - """ - - # ! Include this initial `test_default_quantities` method when testing your PhysicalProperty classes - def test_default_quantities(self): - """ - Test the default quantities assigned when creating an instance of the `ElectronicBandGap` class. - """ - electronic_band_gap = ElectronicBandGap() - assert ( - electronic_band_gap.iri - == 'http://fairmat-nfdi.eu/taxonomy/ElectronicBandGap' - ) - assert electronic_band_gap.name == 'ElectronicBandGap' - assert electronic_band_gap.rank == [] - - @pytest.mark.parametrize( - 'value, result', - [ - (0.0, 0.0), - (1.0, 1.0), - (-1.0, None), - ([1.0, 2.0, -1.0], None), - ], +@pytest.mark.parametrize( + 'bg_data, bg_type, spins, mom_trans', + [ + ([1.0], None, None, None), + ([1.0], 'direct', ['up'], [2 * [3 * [0]]]), + ([1.0, 1.0], 'direct', ['up', 'down'], 2 * [2 * [3 * [0]]]), + ([1.0], 'direct', ['up', 'down'], [2 * [3 * [0]]]), + ], +) +def test_instantiation(bg_data, bg_type, spins, mom_trans): + assert ElectronicBandGap( + data=np.array(bg_data) * ureg.eV, + type=bg_type, + variables=[SpinChannel(data=spins), MomentumTransfer(data=mom_trans)], ) - def test_validate_values(self, value: Union[list[float], float], result: float): - """ - Test the `validate_values` method. - """ - if isinstance(value, list): - electronic_band_gap = ElectronicBandGap( - variables=[Temperature(points=[1, 2, 3] * ureg.kelvin)] - ) - else: - electronic_band_gap = ElectronicBandGap() - electronic_band_gap.value = value * ureg.joule - validated_value = electronic_band_gap.validate_values(logger) - if validated_value is not None: - assert np.isclose(validated_value.magnitude, result) - else: - assert validated_value == result - - @pytest.mark.parametrize( - 'momentum_transfer, type, result', - [ - (None, None, None), - (None, 'direct', 'direct'), - (None, 'indirect', 'indirect'), - ([[0, 0, 0]], None, None), - ([[0, 0, 0]], 'direct', None), - ([[0, 0, 0]], 'indirect', None), - ([[0, 0, 0], [0, 0, 0]], None, 'direct'), - ([[0, 0, 0], [0, 0, 0]], 'direct', 'direct'), - ([[0, 0, 0], [0, 0, 0]], 'indirect', 'direct'), - ([[0, 0, 0], [0.5, 0.5, 0.5]], None, 'indirect'), - ([[0, 0, 0], [0.5, 0.5, 0.5]], 'direct', 'indirect'), - ([[0, 0, 0], [0.5, 0.5, 0.5]], 'indirect', 'indirect'), - ], + # ! TODO add shape tests + + +@pytest.mark.parametrize( + 'bg_data, bg_type, moms, ref_moms', + [ + ([1.0], None, [], []), + ([1.0], None, [2 * [3 * [0.0]]], [2 * [3 * [0.0]]]), + ([1.0], 'direct', [], [2 * [3 * [0.0]]]), + ([1.0], 'indirect', [], []), + ], +) +def test_direct_bandgap_normalization( + bg_data: list[float], + bg_type: Optional[str], + moms: list[list[float]], + ref_moms: list[list[float]], +): + band_gap = ElectronicBandGap( + data=np.array(bg_data) * ureg.eV, + type=bg_type, + variables=[MomentumTransfer(data=moms)] if moms else [], ) - def test_resolve_type( - self, momentum_transfer: Optional[list[float]], type: str, result: Optional[str] - ): - """ - Test the `resolve_type` method. - """ - electronic_band_gap = ElectronicBandGap( - variables=[], - momentum_transfer=momentum_transfer, - type=type, - ) - assert electronic_band_gap.resolve_type(logger) == result - - def test_normalize(self): - """ - Test the `normalize` method for two different ElectronicBandGap instantiations, one with a scalar - `value` and another with a temperature-dependent `value` - """ - scalar_band_gap = ElectronicBandGap(variables=[], type='direct') - scalar_band_gap.value = 1.0 * ureg.joule - scalar_band_gap.normalize(EntryArchive(), logger) - assert scalar_band_gap.type == 'direct' - assert np.isclose(scalar_band_gap.value.magnitude, 1.0) - - t_dependent_band_gap = ElectronicBandGap( - variables=[Temperature(points=[0, 10, 20, 30] * ureg.kelvin)], - type='direct', - ) - t_dependent_band_gap.value = [1.0, 2.0, 3.0, 4.0] * ureg.joule - t_dependent_band_gap.normalize(EntryArchive(), logger) - assert t_dependent_band_gap.type == 'direct' - assert ( - np.isclose(t_dependent_band_gap.value.magnitude, [1.0, 2.0, 3.0, 4.0]) - ).all() + band_gap.normalize(EntryArchive(), logger) + + var_moms = [ + var.m_to_dict()['data'][0] + for var in band_gap.variables + if isinstance(var, MomentumTransfer) + ] + assert var_moms == ref_moms