Skip to content

Commit

Permalink
- Apply new PhysicalProperty def to band_gap
Browse files Browse the repository at this point in the history
- Add testing
  • Loading branch information
Nathan Daelman committed Nov 17, 2024
1 parent a398c0a commit d8a5d9f
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 207 deletions.
159 changes: 47 additions & 112 deletions src/nomad_simulations/schema_packages/properties/band_gap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
145 changes: 50 additions & 95 deletions tests/properties/test_band_gap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit d8a5d9f

Please sign in to comment.