|
| 1 | +# |
| 2 | +# Copyright The NOMAD Authors. |
| 3 | +# |
| 4 | +# This file is part of NOMAD. See https://nomad-lab.eu for further info. |
| 5 | +# |
| 6 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 7 | +# you may not use this file except in compliance with the License. |
| 8 | +# You may obtain a copy of the License at |
| 9 | +# |
| 10 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | +# |
| 12 | +# Unless required by applicable law or agreed to in writing, software |
| 13 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 14 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 15 | +# See the License for the specific language governing permissions and |
| 16 | +# limitations under the License. |
| 17 | +# |
| 18 | + |
| 19 | +import numpy as np |
| 20 | +from structlog.stdlib import BoundLogger |
| 21 | +import pint |
| 22 | +from typing import Optional |
| 23 | + |
| 24 | +from nomad.metainfo import Quantity, MEnum, Section, Context |
| 25 | + |
| 26 | +from nomad_simulations.physical_property import PhysicalProperty |
| 27 | + |
| 28 | + |
| 29 | +class ElectronicBandGap(PhysicalProperty): |
| 30 | + """ |
| 31 | + Energy difference between the highest occupied electronic state and the lowest unoccupied electronic state. |
| 32 | + """ |
| 33 | + |
| 34 | + # ! implement `iri` and `rank` as part of `m_def = Section()` |
| 35 | + |
| 36 | + iri = 'http://fairmat-nfdi.eu/taxonomy/ElectronicBandGap' |
| 37 | + |
| 38 | + type = Quantity( |
| 39 | + type=MEnum('direct', 'indirect'), |
| 40 | + description=""" |
| 41 | + Type categorization of the electronic band gap. This quantity is directly related with `momentum_transfer` as by |
| 42 | + definition, the electronic band gap is `'direct'` for zero momentum transfer (or if `momentum_transfer` is `None`) and `'indirect'` |
| 43 | + for finite momentum transfer. |
| 44 | +
|
| 45 | + Note: in the case of finite `variables`, this quantity refers to all of the `value` in the array. |
| 46 | + """, |
| 47 | + ) |
| 48 | + |
| 49 | + momentum_transfer = Quantity( |
| 50 | + type=np.float64, |
| 51 | + shape=[2, 3], |
| 52 | + description=""" |
| 53 | + If the electronic band gap is `'indirect'`, the reciprocal momentum transfer for which the band gap is defined |
| 54 | + in units of the `reciprocal_lattice_vectors`. The initial and final momentum 3D vectors are given in the first |
| 55 | + and second element. Example, the momentum transfer in bulk Si2 happens between the Γ and the (approximately) |
| 56 | + X points in the Brillouin zone; thus: |
| 57 | + `momentum_transfer = [[0, 0, 0], [0.5, 0.5, 0]]`. |
| 58 | +
|
| 59 | + Note: this quantity only refers to scalar `value`, not to arrays of `value`. |
| 60 | + """, |
| 61 | + ) |
| 62 | + |
| 63 | + spin_channel = Quantity( |
| 64 | + type=np.int32, |
| 65 | + description=""" |
| 66 | + Spin channel of the corresponding electronic band gap. It can take values of 0 or 1. |
| 67 | + """, |
| 68 | + ) |
| 69 | + |
| 70 | + value = Quantity( |
| 71 | + type=np.float64, |
| 72 | + unit='joule', |
| 73 | + description=""" |
| 74 | + The value of the electronic band gap. This value has to be positive, otherwise it will |
| 75 | + prop an error and be set to None by the `normalize()` function. |
| 76 | + """, |
| 77 | + ) |
| 78 | + |
| 79 | + def __init__( |
| 80 | + self, m_def: Section = None, m_context: Context = None, **kwargs |
| 81 | + ) -> None: |
| 82 | + super().__init__(m_def, m_context, **kwargs) |
| 83 | + self.name = self.m_def.name |
| 84 | + self.rank = [] |
| 85 | + |
| 86 | + def validate_values(self, logger: BoundLogger) -> Optional[pint.Quantity]: |
| 87 | + """ |
| 88 | + Validate the electronic band gap `value` by checking if they are negative and sets them to None if they are. |
| 89 | +
|
| 90 | + Args: |
| 91 | + logger (BoundLogger): The logger to log messages. |
| 92 | + """ |
| 93 | + value = self.value.magnitude |
| 94 | + if not isinstance(self.value.magnitude, np.ndarray): # for scalars |
| 95 | + value = np.array( |
| 96 | + [value] |
| 97 | + ) # ! check this when talking with Lauri and Theodore |
| 98 | + |
| 99 | + # Set the value to 0 when it is negative |
| 100 | + if (value < 0).any(): |
| 101 | + logger.error('The electronic band gap cannot be defined negative.') |
| 102 | + return None |
| 103 | + |
| 104 | + if not isinstance(self.value.magnitude, np.ndarray): # for scalars |
| 105 | + value = value[0] |
| 106 | + return value * self.value.u |
| 107 | + |
| 108 | + def resolve_type(self, logger: BoundLogger) -> Optional[str]: |
| 109 | + """ |
| 110 | + Resolves the `type` of the electronic band gap based on the stored `momentum_transfer` values. |
| 111 | +
|
| 112 | + Args: |
| 113 | + logger (BoundLogger): The logger to log messages. |
| 114 | +
|
| 115 | + Returns: |
| 116 | + (Optional[str]): The resolved `type` of the electronic band gap. |
| 117 | + """ |
| 118 | + mtr = self.momentum_transfer if self.momentum_transfer is not None else [] |
| 119 | + |
| 120 | + # Check if the `momentum_transfer` is [], and return the type and a warning in the log for `indirect` band gaps |
| 121 | + if len(mtr) == 0: |
| 122 | + if self.type == 'indirect': |
| 123 | + logger.warning( |
| 124 | + 'The `momentum_transfer` is not stored for an `indirect` band gap.' |
| 125 | + ) |
| 126 | + return self.type |
| 127 | + |
| 128 | + # Check if the `momentum_transfer` has at least two elements, and return None if it does not |
| 129 | + if len(mtr) == 1: |
| 130 | + logger.warning( |
| 131 | + '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.' |
| 132 | + ) |
| 133 | + return None |
| 134 | + |
| 135 | + # Resolve `type` from the difference between the initial and final momentum transfer |
| 136 | + momentum_difference = np.diff(mtr, axis=0) |
| 137 | + if (np.isclose(momentum_difference, np.zeros(3))).all(): |
| 138 | + return 'direct' |
| 139 | + else: |
| 140 | + return 'indirect' |
| 141 | + |
| 142 | + def normalize(self, archive, logger) -> None: |
| 143 | + super().normalize(archive, logger) |
| 144 | + |
| 145 | + # Checks if the `value` is negative and sets it to None if it is. |
| 146 | + self.value = self.validate_values(logger) |
| 147 | + if self.value is None: |
| 148 | + # ? What about deleting the class if `value` is None? |
| 149 | + logger.error('The `value` of the electronic band gap is not stored.') |
| 150 | + return |
| 151 | + |
| 152 | + # Resolve the `type` of the electronic band gap from `momentum_transfer`, ONLY for scalar `value` |
| 153 | + if isinstance(self.value.magnitude, np.ndarray): |
| 154 | + logger.info( |
| 155 | + 'We do not support `type` which describe individual elements in an array `value`.' |
| 156 | + ) |
| 157 | + else: |
| 158 | + self.type = self.resolve_type(logger) |
| 159 | + |
0 commit comments