diff --git a/src/nomad_simulations/schema_packages/properties/band_gap.py b/src/nomad_simulations/schema_packages/properties/band_gap.py index e744e1e7..7d2fdf29 100644 --- a/src/nomad_simulations/schema_packages/properties/band_gap.py +++ b/src/nomad_simulations/schema_packages/properties/band_gap.py @@ -3,11 +3,10 @@ import numpy as np from nomad.units import ureg from nomad.datamodel.data import ArchiveSection -from nomad.metainfo import MEnum, Quantity -from nomad.metainfo.datasets import DatasetTemplate, Energy from ..variables import ( - SpinChannel, - MomentumTransfer, + KMomentumTransfer, + ElectronicBandGap, + BandGapType, ) if TYPE_CHECKING: @@ -15,47 +14,35 @@ from structlog.stdlib import BoundLogger -class HomoLumoGap(ArchiveSection): # ? class description - values = DatasetTemplate( - name='HomoLumoGap', - mandatory_fields=[Energy], - mandatory_variables=[SpinChannel], # presence checked via annotations - iri='http://fairmat-nfdi.eu/taxonomy/HomoLumoGap', - description=""" - The energy difference between the highest occupied spin state - and the lowest unoccupied spin state. - """, - ) - - def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: - self.m_all_validate() # ensure constraints - if np.any((energies := self.get_variable(Energy).get_values()) < 0): # ? failure handling - logger.warning(f'Negative band gap detected: {energies.to('J').magnitude} J') +class ElectronicBandGapSection(ArchiveSection): # ! TODO: add optical band gap + band_gap = ElectronicBandGap() + band_gap_type = BandGapType() -class ElectronicBandGap(HomoLumoGap): # ! TODO: add optical band gap - values = HomoLumoGap.values.m_copy() # ? `m_copy` supported - values.mandatory_variables.append(MomentumTransfer) - values.iri = 'http://fairmat-nfdi.eu/taxonomy/ElectronicBandGap' + def type_to_gap(self, logger: 'BoundLogger'): + bg, bt = self.band_gap.get_fields(), self.band_gap_type.get_fields() + if bg is not None: + return + + if self.type == 'direct': + self.variables.append( + KMomentumTransfer(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}' + ) - 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. + def gap_to_type(self): + if self.band_gap_type.get_fields() is not None: + return + if self.band_gap.get_variables(KMomentumTransfer): + self.band_gap_type = ... + else: + self.band_gap_type = 'unknown' - Note: in the case of finite `variables`, this quantity refers to all of the `value` in the array. - """, - ) def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: - if not self.get_variable(MomentumTransfer).get_values(): # getter - 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}' - ) + super().normalize(archive, logger) + self.type_to_gap(logger) + self.gap_to_type() diff --git a/src/nomad_simulations/schema_packages/properties/band_structure.py b/src/nomad_simulations/schema_packages/properties/band_structure.py index 8080edef..bca4ebdf 100644 --- a/src/nomad_simulations/schema_packages/properties/band_structure.py +++ b/src/nomad_simulations/schema_packages/properties/band_structure.py @@ -2,18 +2,15 @@ import numpy as np import pint -import plotly.express as px from nomad.datamodel.data import ArchiveSection from nomad.metainfo import MEnum, Quantity -from nomad.metainfo.physical_properties import DatasetTemplate, Count, Energy -from ..variables import SpinChannel, KMesh +from ..variables import SpinChannel, KPoint, ElectronicEigenEnergies if TYPE_CHECKING: from nomad.datamodel.datamodel import EntryArchive from structlog.stdlib import BoundLogger -from nomad_simulations.schema_packages.atoms_state import AtomsState, OrbitalsState from nomad_simulations.schema_packages.numerical_settings import KSpace from nomad_simulations.schema_packages.properties.band_gap import ElectronicBandGap @@ -24,53 +21,16 @@ ) -Occupancy = Count.m_copy() # ? establish semantic connection # values between 0 - 1 or 0 - 2 -Occupancy.iri='http://fairmat-nfdi.eu/taxonomy/Occupancy', -Occupancy.description=""" -Electrons occupancy of an atom per orbital and spin. This is a number defined between 0 and 1 for -spin-polarized systems, and between 0 and 2 for non-spin-polarized systems. This property is -important when studying if an orbital or spin channel are fully occupied, at half-filling, or -fully emptied, which have an effect on the electron-electron interaction effects. -""" +class ElectronicEigenEnergiesSection(ArchiveSection): + electronic_eigen_energies = ElectronicEigenEnergies() + highest_occupied = Energy() # ? C4: remove -class ElectronicEigenstates(ArchiveSection): - values = DatasetTemplate( - name='ElectronicEigenstates', - mandatory_fields=[Energy, Occupancy], - mandatory_variables=[SpinChannel], - iri='http://fairmat-nfdi.eu/taxonomy/ElectronicEigenvalues', - description=""" - A base section used to define basic quantities for - the `ElectronicEigenvalues` and `ElectronicEigenstates` properties. - """, - ) + lowest_unoccupied = Energy() # ? C4: remove # ? 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? - # references - atoms_state_ref = Quantity( - type=AtomsState, - description=""" - Reference to the matching `AtomsState` section. - """, - ) - - orbitals_state_ref = Quantity( - type=OrbitalsState, - description=""" - Reference to the matching `OrbitalsState` section. - """, - ) # ! TODO: unify with `atoms_state_ref` - - # derived properties - n_eigenvalues = Count # ? remove - - highest_occupied = Energy # ? property - - lowest_unoccupied = Energy # ? property - def order_eigenvalues(self) -> Union[bool, tuple['pint.Quantity', np.ndarray]]: """ Order the eigenvalues based on the `value` and `occupation`. The return `value` and @@ -180,26 +140,6 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: self.reciprocal_cell = self.resolve_reciprocal_cell() -class DensityOfStates(ElectronicEigenstates): - # fermi_level - - # band_gap - - partial_dos = DatasetTemplate( - mandatory_fields=[Energy, Count], # ! relax - mandatory_variables=[SpinChannel, AtomsState], # to be interpreted as the symbol, i.e. p_x - ) # ? instead of subsection - - def plot(self): - self.m_all_validate() - energy_axes = self.values.get_values(Energy).by(SpinChannel) - figure_main = px.line( - x=np.sort(np.array(set(*energy_axes))), # overlay along spin dim - y=self.values.get_values(Count).by(SpinChannel), - color=self.values.get_variable(SpinChannel), - ) - - class BandStructure(ArchiveSection): values = DatasetTemplate( name='BandStructure', @@ -230,11 +170,11 @@ class BandStructure(ArchiveSection): highest_occupied = DatasetTemplate( name='HighestOccupied', mandatory_fields=[Energy, KMesh], - ) # ? property + ) # ? property lowest_unoccupied = ighest_occupied = DatasetTemplate( name='LowestUnoccupied', mandatory_fields=[Energy, KMesh], - ) # ? property + ) # ? property # ! plot diff --git a/src/nomad_simulations/schema_packages/properties/spectral_profile.py b/src/nomad_simulations/schema_packages/properties/spectral_profile.py index 8078ff3d..6548a72e 100644 --- a/src/nomad_simulations/schema_packages/properties/spectral_profile.py +++ b/src/nomad_simulations/schema_packages/properties/spectral_profile.py @@ -1,131 +1,32 @@ from typing import TYPE_CHECKING, Optional import numpy as np -import pint +import plotly.express as px + from nomad.config import config -from nomad.metainfo import MEnum, Quantity, SubSection +from nomad.metainfo import MEnum, Quantity +from ..variables import ( + Energy, + Count, + SpinChannel, + ElectronicDensityOfStates, + ProjectedElectronicDensityOfStates, +) 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.atoms_state import AtomsState, OrbitalsState -from nomad_simulations.schema_packages.physical_property import PhysicalProperty -from nomad_simulations.schema_packages.properties.band_gap import ElectronicBandGap -from nomad_simulations.schema_packages.utils import get_sibling_section, get_variables -from nomad_simulations.schema_packages.variables import Energy2 as Energy + import pint configuration = config.get_plugin_entry_point( 'nomad_simulations.schema_packages:nomad_simulations_plugin' ) -class SpectralProfile(PhysicalProperty): - """ - A base section used to define the spectral profile. - """ - - value = Quantity( - type=np.float64, - description=""" - The value of the intensities of a spectral profile in arbitrary units. - """, - ) # TODO check units and normalization_factor of DOS and Spectras and see whether they can be merged - - def __init__( - self, m_def: 'Section' = None, m_context: 'Context' = None, **kwargs - ) -> None: - super().__init__(m_def, m_context, **kwargs) - self.rank = [] - - def is_valid_spectral_profile(self) -> bool: - """ - Check if the spectral profile is valid, i.e., if all `value` are defined positive. - - Returns: - (bool): True if the spectral profile is valid, False otherwise. - """ - if (self.value < 0.0).any(): - return False - return True - - def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: - super().normalize(archive, logger) - - if self.is_valid_spectral_profile() is False: - logger.error( - 'Invalid negative intensities found: could not validate spectral profile.' - ) - return - - -class DOSProfile(SpectralProfile): - """ - A base section used to define the `value` of the `ElectronicDensityOfState` property. This is useful when containing - contributions for `projected_dos` with the correct unit. - """ - - value = Quantity( - type=np.float64, - unit='1/joule', - description=""" - The value of the electronic DOS. - """, - ) - - def __init__( - self, m_def: 'Section' = None, m_context: 'Context' = None, **kwargs - ) -> None: - super().__init__(m_def, m_context, **kwargs) - - def resolve_pdos_name(self, logger: 'BoundLogger') -> Optional[str]: - """ - Resolve the `name` of the projected `DOSProfile` from the `entity_ref` section. This is resolved as: - - `'atom X'` with 'X' being the chemical symbol for `AtomsState` references. - - `'orbital Y X'` with 'X' being the chemical symbol and 'Y' the orbital label for `OrbitalsState` references. - - Args: - logger (BoundLogger): The logger to log messages. - - Returns: - (Optional[str]): The resolved `name` of the projected DOS profile. - """ - if self.entity_ref is None and not self.name == 'ElectronicDensityOfStates': - logger.warning( - 'The `entity_ref` is not set for the DOS profile. Could not resolve the `name`.' - ) - return None - - # Resolve the `name` from the `entity_ref` - name = None - if isinstance(self.entity_ref, AtomsState): - name = f'atom {self.entity_ref.chemical_symbol}' - elif isinstance(self.entity_ref, OrbitalsState): - name = f'orbital {self.entity_ref.l_quantum_symbol}{self.entity_ref.ml_quantum_symbol} {self.entity_ref.m_parent.chemical_symbol}' - return name - - def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: - super().normalize(archive, logger) - - # We resolve - self.name = self.resolve_pdos_name(logger) - - -class ElectronicDensityOfStates(DOSProfile): - """ - Number of electronic states accessible for the charges per energy and per volume. - """ +class ElectronicDOSSection(ArchiveSection): + dos = ElectronicDensityOfStates() - # ! implement `iri` and `rank` as part of `m_def = Section()` - iri = 'http://fairmat-nfdi.eu/taxonomy/ElectronicDensityOfStates' - - spin_channel = Quantity( - type=np.int32, - description=""" - Spin channel of the corresponding electronic DOS. It can take values of 0 or 1. - """, - ) + pdos = ProjectedElectronicDensityOfStates() # TODO clarify the role of `energies_origin` once `ElectronicEigenvalues` is implemented energies_origin = Quantity( @@ -135,7 +36,7 @@ class ElectronicDensityOfStates(DOSProfile): Energy level denoting the origin along the energy axis, used for comparison and visualization. It is defined as the `ElectronicEigenvalues.highest_occupied_energy`. """, - ) + ) # ? i.e. Fermi-level normalization_factor = Quantity( type=np.float64, @@ -146,53 +47,23 @@ class ElectronicDensityOfStates(DOSProfile): """, ) - # ? Do we want to store the integrated value here os as part of an nomad-analysis tool? Check `dos_integrator.py` module in dos normalizer repository - # value_integrated = Quantity( - # type=np.float64, - # description=""" - # The cumulative intensities integrated from from the lowest (most negative) energy to the Fermi level. - # """, - # ) - - projected_dos = SubSection( - sub_section=DOSProfile.m_def, - repeats=True, - description=""" - Projected DOS. It can be atom- (different elements in the unit cell) or orbital-projected. These can be calculated in a cascade as: - - If the total DOS is not present, we sum all atom-projected DOS to obtain it. - - If the atom-projected DOS is not present, we sum all orbital-projected DOS to obtain it. - Note: the cover given by summing up contributions is not perfect, and will depend on the projection functions used. - - In `projected_dos`, `name` and `entity_ref` must be set in order for normalization to work: - - The `entity_ref` is the `OrbitalsState` or `AtomsState` sections. - - The `name` of the projected DOS should be `'atom X'` or `'orbital Y X'`, with 'X' being the chemical symbol and 'Y' the orbital label. - These can be extracted from `entity_ref`. - """, - ) - - 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 resolve_energies_origin( self, - energies: pint.Quantity, - fermi_level: Optional[pint.Quantity], + energies: 'pint.Quantity', + fermi_level: Optional['pint.Quantity'], logger: 'BoundLogger', - ) -> Optional[pint.Quantity]: + ) -> Optional['pint.Quantity']: """ Resolve the origin of reference for the energies from the sibling `ElectronicEigenvalues` section and its `highest_occupied` level, or if this does not exist, from the `fermi_level` value as extracted from the sibling property, `FermiLevel`. Args: - fermi_level (Optional[pint.Quantity]): The resolved Fermi level. - energies (pint.Quantity): The grid points of the `Energy` variable. - logger (BoundLogger): The logger to log messages. + fermi_level: The resolved Fermi level. + energies: The grid points of the `Energy` variable. + logger: The logger to log messages. Returns: - (Optional[pint.Quantity]): The resolved origin of reference for the energies. + The resolved origin of reference for the energies. """ # Check if the variables contain more than one variable (different than Energy) # ? Is this correct or should be use the index of energies to extract the proper shape element in `self.value` being used for `dos_values`? @@ -367,50 +238,18 @@ def extract_band_gap(self) -> Optional[ElectronicBandGap]: band_gap.value = homo - lumo return band_gap - def extract_projected_dos( - self, type: str, logger: 'BoundLogger' - ) -> list[Optional[DOSProfile]]: - """ - Extract the projected DOS from the `projected_dos` section and the specified `type`. - - Args: - type (str): The type of the projected DOS to extract. It can be `'atom'` or `'orbital'`. - - Returns: - (DOSProfile): The extracted projected DOS. - """ - extracted_pdos = [] - for pdos in self.projected_dos: - # We make sure each PDOS is normalized - pdos.normalize(None, logger) - - # Initial check for `name` and `entity_ref` - if ( - pdos.name is None - or pdos.entity_ref is None - or len(pdos.entity_ref) == 0 - ): - logger.warning( - '`name` or `entity_ref` are not set for `projected_dos` and they are required for normalization to work.' - ) - return None - - if type in pdos.name: - extracted_pdos.append(pdos) - return extracted_pdos - def generate_from_projected_dos( self, logger: 'BoundLogger' - ) -> Optional[pint.Quantity]: + ) -> Optional['pint.Quantity']: """ Generate the total `value` of the electronic DOS from the `projected_dos` contributions. If the `projected_dos` is not present, it returns `None`. Args: - logger (BoundLogger): The logger to log messages. + logger: The logger to log messages. Returns: - (Optional[pint.Quantity]): The total `value` of the electronic DOS. + The total `value` of the electronic DOS. """ if self.projected_dos is None or len(self.projected_dos) == 0: return None @@ -462,6 +301,15 @@ def generate_from_projected_dos( value = np.sum(atom_values, axis=0) * atom_unit return value + def plot(self): # ? to be usurped by plotting annotations + self.m_all_validate() + energy_axes = self.values.get_values(Energy).by(SpinChannel) + figure_main = px.line( + x=np.sort(np.array(set(*energy_axes))), # overlay along spin dim + y=self.values.get_values(Count).by(SpinChannel), + color=self.values.get_variable(SpinChannel), + ) + def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: super().normalize(archive, logger) @@ -502,12 +350,14 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: self.value = value_from_pdos -class AbsorptionSpectrum(SpectralProfile): - """ """ +class XASSpectrum(AbsorptionSpectrum): + xas = Spectrum() + + xanes_spectrum = Spectrum() - # ! implement `iri` and `rank` as part of `m_def = Section()` + exafs_spectrum = Spectrum() - axis = Quantity( + polarization_axis = Quantity( type=MEnum('xx', 'yy', 'zz'), description=""" Axis of the absorption spectrum. This is related with the polarization direction, and can be seen as the @@ -515,47 +365,6 @@ class AbsorptionSpectrum(SpectralProfile): """, ) - def __init__( - self, m_def: 'Section' = None, m_context: 'Context' = None, **kwargs - ) -> None: - super().__init__(m_def, m_context, **kwargs) - # Set the name of the section - self.name = self.m_def.name - - def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: - super().normalize(archive, logger) - - -class XASSpectrum(AbsorptionSpectrum): - """ - X-ray Absorption Spectrum (XAS). - """ - - # ! implement `iri` and `rank` as part of `m_def = Section()` - - xanes_spectrum = SubSection( - sub_section=AbsorptionSpectrum.m_def, - description=""" - X-ray Absorption Near Edge Structure (XANES) spectrum. - """, - repeats=False, - ) - - exafs_spectrum = SubSection( - sub_section=AbsorptionSpectrum.m_def, - description=""" - Extended X-ray Absorption Fine Structure (EXAFS) spectrum. - """, - repeats=False, - ) - - def __init__( - self, m_def: 'Section' = None, m_context: 'Context' = None, **kwargs - ) -> None: - super().__init__(m_def, m_context, **kwargs) - # Set the name of the section - self.name = self.m_def.name - def generate_from_contributions(self, logger: 'BoundLogger') -> None: """ Generate the `value` of the XAS spectrum by concatenating the XANES and EXAFS contributions. It also concatenates @@ -564,6 +373,8 @@ def generate_from_contributions(self, logger: 'BoundLogger') -> None: Args: logger (BoundLogger): The logger to log messages. """ + self.m_validate() + # TODO check if this method is general enough if self.xanes_spectrum is not None and self.exafs_spectrum is not None: # Concatenate XANE and EXAFS `Energy` grid points diff --git a/src/nomad_simulations/schema_packages/variables.py b/src/nomad_simulations/schema_packages/variables.py index 985a44e3..8bd05709 100644 --- a/src/nomad_simulations/schema_packages/variables.py +++ b/src/nomad_simulations/schema_packages/variables.py @@ -1,12 +1,12 @@ from typing import TYPE_CHECKING, Optional -from enum import Enum import numpy as np from nomad.datamodel.data import ArchiveSection from nomad.metainfo import MEnum, Quantity from nomad.metainfo.datasets import ( ValuesTemplate, DatasetTemplate, + Energy, ) if TYPE_CHECKING: @@ -14,11 +14,31 @@ from nomad.metainfo import Context, Section from structlog.stdlib import BoundLogger -from nomad_simulations.schema_packages.numerical_settings import ( - KLinePath as KLinePathSettings, + +KPoint = ValuesTemplate( + type=np.float64, # ? KMeshSettings.points, + shape=['*'], + unit='1/m', + description=""" + K-point mesh over which the physical property is calculated. + """, + # ! iri ) -from nomad_simulations.schema_packages.numerical_settings import ( - KMesh as KMeshSettings, + + +KMomentumTransfer = ValuesTemplate( + type=np.float64, + shape=[2, 3], + unit='1/meter', + description=""" + The change in momentum for any (quasi-)particle, e.g. electron, hole, + traversing the band gap. + + 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]]`. + """, + # ! iri ) @@ -29,34 +49,130 @@ ) -KMesh = ValuesTemplate( - type=np.float64, # ? KMeshSettings.points, - shape=[3], - unit='1/m', +# ? SpinState + + +FermiLevel = DatasetTemplate( + name='FermiLevel', + mandatory_fields=[Energy], + # iri +) + + +FermiSurface = DatasetTemplate( + name='FermiSurface', + mandatory_fields=[Energy], + mandatory_variables=[KPoint], # ? band indices description=""" - K-point mesh over which the physical property is calculated. This is used to define `ElectronicEigenvalues(PhysicalProperty)` and - other k-space properties. The `points` are obtained from a reference to the `NumericalSettings` section, `KMesh(NumericalSettings)`. - """, + The Fermi surface is the surface in the reciprocal space that separates the occupied states from the unoccupied states at absolute zero temperature. + """, # ! iri ) -MomentumTransfer = ValuesTemplate( - type=np.float64, - shape=[2, 3], - unit='1/meter', +ElectronicStateOccupation = ValuesTemplate( + name='ElectronicStateOccupation', + type=np.float64, # [0,1] or [0,2] + shape=['*'], description=""" - The change in momentum for any (quasi-)particle, e.g. electron, hole, - traversing the band gap. + Occupation of an orbital or spin channel. This is a number defined between 0 and 1 for + spin-polarized systems, and between 0 and 2 for non-spin-polarized systems. This property is + important when studying if an orbital or spin channel are fully occupied, at half-filling, or + fully emptied, which have an effect on the electron-electron interaction effects. + """, + # ! iri +) - 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]]`. - """, + +ElectronicStateDensity = ValuesTemplate( + name='ElectronicStateDensity', + type=np.float64, + shape=['*'], + description=""" + Density of electronic states. This property is important when studying the electronic structure + of a material, and it is used to calculate the density of states (DOS). + """, # ! iri ) +HomoLumoGap = DatasetTemplate( + name='HomoLumoGap', + mandatory_fields=[Energy], + mandatory_variables=[SpinChannel], # ? + iri='http://fairmat-nfdi.eu/taxonomy/HomoLumoGap', + description=""" + The energy difference between the highest occupied spin state + and the lowest unoccupied spin state. + """, +) + + +ElectronicBandGap = DatasetTemplate( + name='ElectronicBandGap', + mandatory_fields=[Energy], + mandatory_variables=[SpinChannel], # ? +) + + +BandGapType = ValuesTemplate( + mandatory_fields=MEnum('direct', 'indirect', 'unknown'), + 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. + """, +) + + +ElectronicEigenEnergies = DatasetTemplate( + name='ElectronicEigenEnergies', # ? HamiltonianEigenvalues + mandatory_fields=[Energy, ElectronicStateOccupation], + mandatory_variables=[SpinChannel], # ? + iri='http://fairmat-nfdi.eu/taxonomy/ElectronicEigenvalues', + description=""" + A base section used to define basic quantities for + the `ElectronicEigenvalues` and `ElectronicEigenstates` properties. + """, +) + + +ElectronicDensityOfStates = DatasetTemplate( + name='DensityOfStates', + mandatory_fields=[Energy, ElectronicStateDensity], + mandatory_variables=[SpinChannel], # ? + iri='http://fairmat-nfdi.eu/taxonomy/ElectronicDensityOfStates', +) + + +ProjectedElectronicDensityOfStates = DatasetTemplate( + name='ProjectedDensityOfStates', + mandatory_fields=[Energy, ElectronicStateDensity], + mandatory_variables=[SpinChannel, ElectronicState], # ? + description=""" + The density of states projected on a specific electronic state. + """, +) + + +SpectralEnergy = ValuesTemplate( + name='SpectralEnergy', + type=np.float64, + unit='spectral_energy', # ! TODO: define ureg + shape=['*'], + description=""" + Energy values at which the spectral function is calculated. + """, +) + + +Spectrum = DatasetTemplate( + name='Spectrum', + mandatory_fields=[Count], + mandatory_variables=[SpectralEnergy], +) + + class Variables(ArchiveSection): # ! TODO: deprecate """ Variables over which the physical property varies, and they are defined as grid points, i.e., discretized