Skip to content

Commit

Permalink
Add BandGap output (#53)
Browse files Browse the repository at this point in the history
* Moved electronic band gap definition to subfolder properties/

* Improved ElectronicBandGap property

Added iri to quantity in PhysicalProperty

* Added testing for ElectronicBandGap

Fix __setattr__ in PhysicalProperty

* Added extract_spin_polarized_property to Outputs and testing

* Changed negative value of ElectronicBandGap to be set to None

Added raise ValueError in PhysicalProperty when value is None

* Fix logic for momentum transfer and resolve_type

* Change variables.Energy to Energy2 to avoid bugs with Normalizers (to be fixed)
  • Loading branch information
JosePizarro3 authored Apr 18, 2024
1 parent f03e514 commit 95286c4
Show file tree
Hide file tree
Showing 9 changed files with 420 additions and 51 deletions.
57 changes: 22 additions & 35 deletions src/nomad_simulations/outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,47 +16,17 @@
# limitations under the License.
#

import numpy as np
from structlog.stdlib import BoundLogger
from typing import Optional

from nomad.datamodel.data import ArchiveSection
from nomad.metainfo import Quantity, SubSection, MEnum, Section, Context
from nomad.metainfo import Quantity, SubSection
from nomad.datamodel.metainfo.annotations import ELNAnnotation

from .model_system import ModelSystem
from .physical_property import PhysicalProperty
from .numerical_settings import SelfConsistency


class ElectronicBandGap(PhysicalProperty):
""" """

rank = []

type = Quantity(
type=MEnum('direct', 'indirect'),
description="""
Type categorization of the electronic band gap. The electronic band gap can be `'direct'` or `'indirect'`.
""",
)

value = Quantity(
type=np.float64,
unit='joule',
description="""
The value of the electronic band gap.
""",
)

# TODO add more functionalities here

def __init__(self, m_def: Section = None, m_context: Context = None, **kwargs):
super().__init__(m_def, m_context, **kwargs)
self.name = self.m_def.name

def normalize(self, archive, logger) -> None:
super().normalize(archive, logger)
from .properties import ElectronicBandGap


class Outputs(ArchiveSection):
Expand Down Expand Up @@ -94,12 +64,29 @@ class Outputs(ArchiveSection):
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
electronic_band_gap = SubSection(sub_section=ElectronicBandGap.m_def, repeats=True)

def extract_spin_polarized_property(self, property_name: str) -> list:
"""
Extracts the spin-polarized properties if present from the property name and returns them as a list of two elements in
which each element refers to each `spin_channel`. If the return list is empty, it means that the simulation is not
spin-polarized (i.e., `spin_channel` is not defined).
Args:
property_name (str): The name of the property to be extracted.
Returns:
(list): The list of spin-polarized properties.
"""
spin_polarized_properties = []
properties = getattr(self, property_name)
for prop in properties:
if prop.spin_channel is None:
continue
spin_polarized_properties.append(prop)
return spin_polarized_properties

def normalize(self, archive, logger) -> None:
super().normalize(archive, logger)

# Resolve if the output property `is_derived` or not.
# self.is_derived = self.resolve_is_derived(self.outputs_ref)


class SCFOutputs(Outputs):
"""
Expand Down
51 changes: 41 additions & 10 deletions src/nomad_simulations/physical_property.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# limitations under the License.
#

import numpy as np
from typing import Any, Optional

from nomad import utils
Expand All @@ -28,6 +29,7 @@
Section,
Context,
MEnum,
URL,
)
from nomad.metainfo.metainfo import DirectQuantity, Dimension, _placeholder_quantity
from nomad.datamodel.metainfo.basesections import Entity
Expand Down Expand Up @@ -56,6 +58,14 @@ class PhysicalProperty(ArchiveSection):
""",
)

iri = Quantity(
type=URL,
description="""
Internationalized Resource Identifier (IRI) of the physical property defined in the FAIRmat
taxonomy, https://fairmat-nfdi.github.io/fairmat-taxonomy/.
""",
)

source = Quantity(
type=MEnum('simulation', 'measurement', 'analysis'),
default='simulation',
Expand Down Expand Up @@ -185,24 +195,42 @@ def full_shape(self) -> list:
"""
return self.variables_shape + self.rank

def __init__(self, m_def: Section = None, m_context: Context = None, **kwargs):
super().__init__(m_def, m_context, **kwargs)
@property
def _new_value(self) -> Quantity:
"""
Initialize a new `Quantity` object for the `value` quantity with the correct `shape` extracted from
the `full_shape` attribute. This copies the main attributes from `value` (`type`, `description`, `unit`).
It is used in the `__setattr__` method.
# initialize a `_new_value` quantity copying the main attrs from the `_value` quantity (`type`, `unit`,
# `description`); this will then be used to setattr the `value` quantity to the `_new_value` one with the
# correct `shape=_full_shape`
Returns:
(Quantity): The new `Quantity` object for setting the `value` quantity.
"""
for quant in self.m_def.quantities:
if quant.name == 'value':
self._new_value = Quantity(
return Quantity(
type=quant.type,
unit=quant.unit, # ? this can be moved to __setattr__
description=quant.description,
)
break

def __init__(
self, m_def: Section = None, m_context: Context = None, **kwargs
) -> None:
super().__init__(m_def, m_context, **kwargs)

def __setattr__(self, name: str, val: Any) -> None:
# For the special case of `value`, its `shape` needs to be defined from `_full_shape`
if name == 'value':
if val is None:
raise ValueError(
f'The value of the physical property {self.name} is None. Please provide a finite valid value.'
)
_new_value = self._new_value

# patch for when `val` does not have units and it is passed as a list (instead of np.array)
if isinstance(val, list):
val = np.array(val)

# non-scalar or scalar `val`
try:
value_shape = list(val.shape)
Expand All @@ -214,9 +242,12 @@ def __setattr__(self, name: str, val: Any) -> None:
f'The shape of the stored `value` {value_shape} does not match the full shape {self.full_shape} '
f'extracted from the variables `n_grid_points` and the `shape` defined in `PhysicalProperty`.'
)
self._new_value.shape = self.full_shape
self._new_value = val.magnitude * val.u
return super().__setattr__(name, self._new_value)
_new_value.shape = self.full_shape
if hasattr(val, 'magnitude'):
_new_value = val.magnitude * val.u
else:
_new_value = val
return super().__setattr__(name, _new_value)
return super().__setattr__(name, val)

def _is_derived(self) -> bool:
Expand Down
19 changes: 19 additions & 0 deletions src/nomad_simulations/properties/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#
# Copyright The NOMAD Authors.
#
# This file is part of NOMAD.
# See https://nomad-lab.eu for further info.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from .band_gap import ElectronicBandGap
157 changes: 157 additions & 0 deletions src/nomad_simulations/properties/band_gap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
#
# Copyright The NOMAD Authors.
#
# This file is part of NOMAD. See https://nomad-lab.eu for further info.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

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

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

from ..physical_property import PhysicalProperty


class ElectronicBandGap(PhysicalProperty):
"""
Energy difference between the highest occupied electronic state and the lowest unoccupied electronic state.
"""

iri = 'http://fairmat-nfdi.eu/taxonomy/ElectronicBandGap'

# ? can `type` change character depending on the `variables`?
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.
""",
)

momentum_transfer = Quantity(
type=np.float64,
shape=[2, 3],
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]]`.
Note: this quantity only refers to scalar `value`, not to arrays of `value`.
""",
)

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(
type=np.float64,
unit='joule',
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.
""",
)

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 check_negative_values(self, logger: BoundLogger) -> Optional[pint.Quantity]:
"""
Checks if the electronic band gaps is 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

# Check if the `momentum_transfer` has at least two elements, and return None if it does not
if len(mtr) == 1:
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.'
)
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'

def normalize(self, archive, logger) -> None:
super().normalize(archive, logger)

# Checks if the `value` is negative and sets it to None if it is.
self.value = self.check_negative_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)
11 changes: 8 additions & 3 deletions src/nomad_simulations/variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,18 @@ class Temperature(Variables):
""",
)

def __init__(self, m_def: Section = None, m_context: Context = None, **kwargs):
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

def normalize(self, archive, logger) -> None:
super().normalize(archive, logger)


class Energy(Variables):
# ! This needs to be fixed as it gives errors when running normalizers with conflicting names (ask Area D)
class Energy2(Variables):
""" """

grid_points = Quantity(
Expand All @@ -121,7 +124,9 @@ class Energy(Variables):
""",
)

def __init__(self, m_def: Section = None, m_context: Context = None, **kwargs):
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

Expand Down
Loading

0 comments on commit 95286c4

Please sign in to comment.