From 17cf3bbf56baf5396508a3b2dbabc1bec23c2ffc Mon Sep 17 00:00:00 2001 From: ndaelman Date: Tue, 14 Jan 2025 21:24:50 +0100 Subject: [PATCH] - Add `PhysicalPropertyDecomposition` to standardize contributions - Rework energies, forces, thermodynamics --- .../schema_packages/physical_property.py | 39 +++ .../schema_packages/properties/energies.py | 49 ++- .../schema_packages/properties/forces.py | 82 +---- .../properties/thermodynamics.py | 255 +--------------- .../schema_packages/variables.py | 284 +++++++++--------- 5 files changed, 231 insertions(+), 478 deletions(-) diff --git a/src/nomad_simulations/schema_packages/physical_property.py b/src/nomad_simulations/schema_packages/physical_property.py index 5bb728bc..c411f1ad 100644 --- a/src/nomad_simulations/schema_packages/physical_property.py +++ b/src/nomad_simulations/schema_packages/physical_property.py @@ -15,6 +15,10 @@ SubSection, ) from nomad.metainfo.metainfo import Dimension, DirectQuantity, _placeholder_quantity +from nomad.metainfo.datasets import ( + ValuesTemplate, + DatasetTemplate, +) if TYPE_CHECKING: from nomad.datamodel.datamodel import EntryArchive @@ -331,3 +335,38 @@ def normalize(self, archive, logger) -> None: super().normalize(archive, logger) if not self.name: self.name = self.get('model_method_ref').get('name') + + +class PhysicalPropertyDecomposition: + """ + Generator class to convert a `values_template: ValuesTemplate` to a `DatasetTemplate` + with `mandatory_fields = [values_template, list[value_template], kind, reference]`. + """ + + def __init__( + self, + value_template: 'ValuesTemplate', + kind: Optional['ValuesTemplate'] = None, + reference_type: Optional['ValuesTemplate'] = None, + ) -> None: + self.value_template = value_template + self.kind = kind + self.reference_type = reference_type + + def _generate_repeating_value(self) -> 'ValuesTemplate': + """ + Generate a repeating `ValuesTemplate` for the `value` quantity. + """ + repeating_value = self.value.m_copy() + repeating_value.m_def.shape = repeating_value.shape + ['*'] + return repeating_value + + def __call__(self, *args, **kwargs) -> 'DatasetTemplate': + return DatasetTemplate( + mandatory_fields=[ + self.value_template, + self._generate_repeating_value(), + self.kind, + self.reference_type, + ], + ) diff --git a/src/nomad_simulations/schema_packages/properties/energies.py b/src/nomad_simulations/schema_packages/properties/energies.py index ef189359..f7b17387 100644 --- a/src/nomad_simulations/schema_packages/properties/energies.py +++ b/src/nomad_simulations/schema_packages/properties/energies.py @@ -1,45 +1,32 @@ from typing import TYPE_CHECKING - import numpy as np -from nomad.metainfo import MEnum, Quantity, Reference -from nomad.metainfo.dataset import MDataset, Dataset -from nomad.datamodel.metainfo.model import ModelMethod + +from ..physical_property import PhysicalPropertyDecomposition +from ..variables import Energy, EnergyType, MethodReference if TYPE_CHECKING: from nomad.datamodel.datamodel import EntryArchive from structlog.stdlib import BoundLogger +EnergyTemplateGenerator = PhysicalPropertyDecomposition( + Energy, + reference_type=MethodReference, +) -class Energy(MDataset): - m_def = Dataset( - type=np.float64, - unit='joule', - description="""A base section used to define basic quantities for the `TotalEnergy` property.""", - default_variables=['Energy'], # ? does this require variables of this shape - ) - - # ? origin_reference - kind = Quantity( - type=MEnum('kinetic', 'potential', 'total'), - ) +class ModelEnergySection('ArchiveSection'): + energy = EnergyTemplateGenerator()() # ? suggest energy_origin or kind - method_reference = Quantity( - type=Reference(ModelMethod), - description=""" - Reference to a `ModelMethod` definition, according to which the energy was calculated. - """, - ) + type = EnergyType() def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: super().normalize(archive, logger) - energy_sums = np.sum( - [var.data for var in self.variables if isinstance(var, Energy)], axis=0 - ) - if self.data is None or self.data == []: - self.data = energy_sums - elif not np.allclose(self.data, energy_sums): - logger.warning( - f'The sum of the energies in the variables is different from the total energy: {energy_sums} != {self.data}' - ) + # check that the contributions do not outgrow the total energy + for field in self.energy.fields: + if (total_energy := field[0]) < (energy_contributions := np.sum(field[1])): + logger.warning( + f'The contributions outweigh the total energy', + energy_contributions, + total_energy, + ) diff --git a/src/nomad_simulations/schema_packages/properties/forces.py b/src/nomad_simulations/schema_packages/properties/forces.py index cedf72f1..48e72232 100644 --- a/src/nomad_simulations/schema_packages/properties/forces.py +++ b/src/nomad_simulations/schema_packages/properties/forces.py @@ -1,79 +1,21 @@ from typing import TYPE_CHECKING -import numpy as np -from nomad.metainfo import Context, Quantity, Section, SubSection +from ..physical_property import PhysicalPropertyDecomposition +from ..variables import Force, MethodReference +from nomad.metainfo.datasets import DatasetTemplate -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, - PropertyContribution, +ForceTemplateGenerator = PhysicalPropertyDecomposition( + Force, + reference_type=MethodReference, ) -################## -# Abstract classes -################## - - -class BaseForce(PhysicalProperty): - """ - Abstract class used to define a common `value` quantity with the appropriate units - for different types of forces, which avoids repeating the definitions for each - force class. - """ - - value = Quantity( - type=np.dtype(np.float64), - unit='newton', - description=""" - """, - ) - - def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: - super().normalize(archive, logger) - - -class ForceContribution(BaseForce, PropertyContribution): - """ - Abstract class for incorporating specific force contributions to the `TotalForce`. - The inheritance from `PropertyContribution` allows to link this contribution to a - specific component (of class `BaseModelMethod`) of the over `ModelMethod` using the - `model_method_ref` quantity. - - For example, for a force field calculation, the `model_method_ref` may point to a - particular potential type (e.g., a Lennard-Jones potential between atom types X and Y), - while for a DFT calculation, it may point to a particular electronic interaction term - (e.g., 'XC' for the exchange-correlation term, or 'Hartree' for the Hartree term). - Then, the contribution will be named according to this model component and the `value` - quantity will contain the force contribution from this component evaluated over all - relevant atoms or electrons or as a function of them. - """ - - def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: - super().normalize(archive, logger) - - -################################### -# List of specific force properties -################################### - -class TotalForce(BaseForce): - """ - The total force on a system. `contributions` specify individual force - contributions to the `TotalForce`. - """ +class ModelForceSection('ArchiveSection'): + force = ForceTemplateGenerator()() - contributions = SubSection(sub_section=ForceContribution.m_def, repeats=True) - 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 +# OR - def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: - super().normalize(archive, logger) +ModelForceSection = DatasetTemplate( + mandatory_fields=[Force], +)() diff --git a/src/nomad_simulations/schema_packages/properties/thermodynamics.py b/src/nomad_simulations/schema_packages/properties/thermodynamics.py index d6135943..3c1f873c 100644 --- a/src/nomad_simulations/schema_packages/properties/thermodynamics.py +++ b/src/nomad_simulations/schema_packages/properties/thermodynamics.py @@ -1,259 +1,28 @@ from typing import TYPE_CHECKING -import numpy as np -from nomad.metainfo import Quantity - 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 -from nomad_simulations.schema_packages.properties.energies import BaseEnergy - -###################################### -# fundamental thermodynamic properties -###################################### - - -class Pressure(PhysicalProperty): - """ - The force exerted per unit area by gas particles as they collide with the walls of - their container. - """ - - # iri = 'http://fairmat-nfdi.eu/taxonomy/Pressure' # ! Does not yet exist in taxonomy - - value = Quantity( - type=np.float64, - unit='pascal', - description=""" - """, - ) - - def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: - super().normalize(archive, logger) - - -class Volume(PhysicalProperty): - """ - the amount of three-dimensional space that a substance or material occupies. - """ - - #! Above description suggested for taxonomy - # TODO check back on definition after first taxonomy version - - iri = 'http://fairmat-nfdi.eu/taxonomy/Volume' - - value = Quantity( - type=np.float64, - unit='m ** 3', - description=""" - """, - ) - - def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: - super().normalize(archive, logger) - - -class Temperature(PhysicalProperty): - """ - a measure of the average kinetic energy of the particles in a system. - """ - - value = Quantity( - type=np.float64, - unit='kelvin', - description=""" - """, - ) - - def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: - super().normalize(archive, logger) - - -class Heat(BaseEnergy): - """ - The transfer of thermal energy **into** a system. - """ - - def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: - super().normalize(archive, logger) - - -class Work(BaseEnergy): - """ - The energy transferred to a system by means of force applied over a distance. - """ - - def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: - super().normalize(archive, logger) - - -class InternalEnergy(BaseEnergy): - """ - The total energy contained within a system, encompassing both kinetic and potential - energies of the particles. The change in `InternalEnergy` for some thermodynamic - process may be expressed as the `Heat` minus the `Work`. - """ - - def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: - super().normalize(archive, logger) - - -class Enthalpy(BaseEnergy): - """ - The total heat content of a system, defined as 'InternalEnergy' + 'Pressure' * 'Volume'. - """ - - def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: - super().normalize(archive, logger) - - -class Entropy(PhysicalProperty): - """ - A measure of the disorder or randomness in a system. - - From a thermodynamic perspective, `Entropy` is a measure of the system's energy - dispersal at a specific temperature, and can be interpreted as the unavailability of - a system's thermal energy for conversion into mechanical work. For a reversible - process, the change in `Entropy` is given mathematically by an integral over the - infinitesimal `Heat` (i.e., thermal energy transfered into the system) divided by the - `Temperature`. - - From a statistical mechanics viewpoint, entropy quantifies the number of microscopic - configurations (microstates) that correspond to a thermodynamic system's macroscopic - state, as given by the Boltzmann equation for entropy. - """ - - value = Quantity( - type=np.float64, - unit='joule / kelvin', - description=""" - """, - ) - - def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: - super().normalize(archive, logger) - - -class GibbsFreeEnergy(BaseEnergy): - """ - The energy available to do work in a system at constant temperature and pressure, - given by `Enthalpy` - `Temperature` * `Entropy`. - """ - - def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: - super().normalize(archive, logger) - +from nomad.datamodel.metainfo import ArchiveSection +from ..variables import VirialTensor, Hessian -class HelmholtzFreeEnergy(BaseEnergy): - """ - The energy available to do work in a system at constant volume and temperature, - given by `InternalEnergy` - `Temperature` * `Entropy`. - """ - - def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: - super().normalize(archive, logger) +def is_square(shape: tuple[int, int]) -> bool: + return shape[0] == shape[1] -class ChemicalPotential(BaseEnergy): - """ - Free energy cost of adding or extracting a particle from a thermodynamic system. - """ - - # ! implement `iri` and `rank` as part of `m_def = Section()` - - iri = 'http://fairmat-nfdi.eu/taxonomy/ChemicalPotential' - - def __init__( - self, m_def: 'Section' = None, m_context: 'Context' = None, **kwargs - ) -> None: - super().__init__(m_def, m_context, **kwargs) - self.rank = [] - self.name = self.m_def.name +class ModelVirialTensorSection(ArchiveSection): # ? set via decorator + virial_tensor = VirialTensor() def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: super().normalize(archive, logger) + if not is_square(self.virial_tensor.shape): + logger.error('Virial tensor is not square', shape=self.virial_tensor.shape) - -class HeatCapacity(PhysicalProperty): - """ - Amount of heat to be supplied to a material to produce a unit change in its temperature. - """ - - value = Quantity( - type=np.float64, - unit='joule / kelvin', - description=""" - """, - ) - - def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: - super().normalize(archive, logger) - - -################################ -# other thermodynamic properties -################################ - - -class VirialTensor(BaseEnergy): - """ - A measure of the distribution of internal forces and the overall stress within - a system of particles. Mathematically, the virial tensor is defined as minus the sum - of the dot product between the position and force vectors for each particle. - The `VirialTensor` can be related to the non-ideal pressure of the system through - the virial theorem. - """ - - def __init__( - self, m_def: 'Section' = None, m_context: 'Context' = None, **kwargs - ) -> None: - super().__init__(m_def, m_context, **kwargs) - self.rank = [3, 3] - self.name = self.m_def.name - - def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: - super().normalize(archive, logger) - - -class MassDensity(PhysicalProperty): - """ - Mass per unit volume of a material. - """ - - value = Quantity( - type=np.float64, - unit='kg / m ** 3', - description=""" - """, - ) - - def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: - super().normalize(archive, logger) - - -# ? fit better elsewhere -class Hessian(PhysicalProperty): - """ - A square matrix of second-order partial derivatives of a potential energy function, - describing the local curvature of the energy surface. - """ - - value = Quantity( - type=np.float64, - unit='joule / m ** 2', - description=""" - """, - ) - - def __init__( - self, m_def: 'Section' = None, m_context: 'Context' = None, **kwargs - ) -> None: - super().__init__(m_def, m_context, **kwargs) - self.rank = [3, 3] - self.name = self.m_def.name +class ModelHessianSection(ArchiveSection): + hessian = Hessian() def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: super().normalize(archive, logger) + if not is_square(self.hessian.shape): + logger.error('Hessian matrix is not square', shape=self.hessian.shape) diff --git a/src/nomad_simulations/schema_packages/variables.py b/src/nomad_simulations/schema_packages/variables.py index 8bd05709..f30b512c 100644 --- a/src/nomad_simulations/schema_packages/variables.py +++ b/src/nomad_simulations/schema_packages/variables.py @@ -1,12 +1,13 @@ -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING import numpy as np -from nomad.datamodel.data import ArchiveSection from nomad.metainfo import MEnum, Quantity from nomad.metainfo.datasets import ( ValuesTemplate, DatasetTemplate, Energy, + Count, + Temperature, ) if TYPE_CHECKING: @@ -15,6 +16,78 @@ from structlog.stdlib import BoundLogger +Pressure = ValuesTemplate( + type=np.float64, + unit='pascal', + iri = 'http://fairmat-nfdi.eu/taxonomy/Pressure' +) + + +Volume = ValuesTemplate( + type=np.float64, + unit='m ** 3', + iri = 'http://fairmat-nfdi.eu/taxonomy/Volume' +) + + +MassDensity = ValuesTemplate( + type=np.float64, + unit='kg / m ** 3', +) + + +Entropy = ValuesTemplate( + type=np.float64, + unit='joule / kelvin', + description="""A measure of the disorder or randomness in a system. + + From a thermodynamic perspective, `Entropy` is a measure of the system's energy + dispersal at a specific temperature, and can be interpreted as the unavailability of + a system's thermal energy for conversion into mechanical work. For a reversible + process, the change in `Entropy` is given mathematically by an integral over the + infinitesimal `Heat` (i.e., thermal energy transfered into the system) divided by the + `Temperature`. + + From a statistical mechanics viewpoint, entropy quantifies the number of microscopic + configurations (microstates) that correspond to a thermodynamic system's macroscopic + state, as given by the Boltzmann equation for entropy. + """ +) + + +HeatCapacity = ValuesTemplate( + type=np.float64, + unit='joule / kelvin', + description="""Amount of heat to be supplied to a material to produce a unit change in its temperature. + """ +) + + +VirialTensor = ValuesTemplate( + type=np.float64, + shape=['*', '*'], + unit='joule', + description=""" + A measure of the distribution of internal forces and the overall stress within + a system of particles. Mathematically, the virial tensor is defined as minus the sum + of the dot product between the position and force vectors for each particle. + The `VirialTensor` can be related to the non-ideal pressure of the system through + the virial theorem. + """ +) + + +Hessian = ValuesTemplate( + type=np.float64, + shape=['*', '*'], + unit='joule / m ** 2', + description=""" + A square matrix of second-order partial derivatives of a potential energy function, + describing the local curvature of the energy surface. + """ +) + + KPoint = ValuesTemplate( type=np.float64, # ? KMeshSettings.points, shape=['*'], @@ -38,18 +111,42 @@ between the Γ and X points in the Brillouin zone; thus: `momentum_transfer = [[0, 0, 0], [0.5, 0.5, 0]]`. """, - # ! iri -) +) # ! change name SpinChannel = ValuesTemplate( name='SpinChannel', type=MEnum('alpha', 'beta', 'both'), # ! iri +) # ? SpinState too + +EnergyType = ValuesTemplate( + type=MEnum( + 'work', + 'internal', + 'free', + 'zero_point', + 'entropy', + 'enthalpy', + 'gibbs free energy', + 'helmholtz free energy', + 'chemical potential', + 'heat', + ), +) # ? parallel ValuesTemplate + +Force = ValuesTemplate( + type=np.float64, + shape=['*'], + unit='newton', ) - -# ? SpinState +MethodReference = ValuesTemplate( + type=Reference(ModelMethod), + description=""" + Reference to a `ModelMethod` definition, according to which the energy was calculated. + """, +) FermiLevel = DatasetTemplate( @@ -84,18 +181,6 @@ ) -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], @@ -111,12 +196,12 @@ ElectronicBandGap = DatasetTemplate( name='ElectronicBandGap', mandatory_fields=[Energy], - mandatory_variables=[SpinChannel], # ? -) + mandatory_variables=[SpinChannel], # ? not relevant to exp. +) # reference to electronic structure BandGapType = ValuesTemplate( - mandatory_fields=MEnum('direct', 'indirect', 'unknown'), + type=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'` @@ -137,144 +222,75 @@ ) +ElectronicBandStructure = DatasetTemplate( + name='ElectronicBandStructure', + mandatory_fields=[Energy, ElectronicStateOccupation], + mandatory_variables=[KMomentumTransfer, SpinChannel], # ? +) + + ElectronicDensityOfStates = DatasetTemplate( name='DensityOfStates', - mandatory_fields=[Energy, ElectronicStateDensity], - mandatory_variables=[SpinChannel], # ? + mandatory_fields=[Count], + mandatory_variables=[Energy, SpinChannel], # ? iri='http://fairmat-nfdi.eu/taxonomy/ElectronicDensityOfStates', ) ProjectedElectronicDensityOfStates = DatasetTemplate( name='ProjectedDensityOfStates', - mandatory_fields=[Energy, ElectronicStateDensity], - mandatory_variables=[SpinChannel, ElectronicState], # ? + mandatory_fields=[Count], + mandatory_variables=[Energy, SpinChannel, ElectronicState], # ? description=""" The density of states projected on a specific electronic state. """, ) -SpectralEnergy = ValuesTemplate( - name='SpectralEnergy', +Spectrum = ValuesTemplate( + name='Spectrum', type=np.float64, - unit='spectral_energy', # ! TODO: define ureg + unit='m', # ? smaller scale shape=['*'], description=""" - Energy values at which the spectral function is calculated. + Wavelength of the spectral entity. """, ) -Spectrum = DatasetTemplate( - name='Spectrum', +SpectralCount = DatasetTemplate( + name='SpectralCount', mandatory_fields=[Count], - mandatory_variables=[SpectralEnergy], + mandatory_variables=[Spectrum], ) -class Variables(ArchiveSection): # ! TODO: deprecate - """ - Variables over which the physical property varies, and they are defined as grid points, i.e., discretized - values by `n_points` and `points`. These are used to calculate the `shape` of the physical property. - """ - - name = Quantity( - type=str, - default='Custom', - description=""" - Name of the variable. - """, - ) - - n_points = Quantity( - type=int, - description=""" - Number of points in which the variable is discretized. - """, - ) - - points = Quantity( - type=np.float64, - # shape=['n_points'], # ! if defined, this breaks using `points` as refs (e.g., `KMesh.points`) - description=""" - Points in which the variable is discretized. It might be overwritten with specific units. - """, - ) - - # ? Do we need to add `points_error`? - - def get_n_points(self, logger: 'BoundLogger') -> Optional[int]: - """ - Get the number of grid points from the `points` list. If `n_points` is previously defined - and does not coincide with the length of `points`, a warning is issued and this function re-assigns `n_points` - as the length of `points`. - - Args: - logger (BoundLogger): The logger to log messages. - - Returns: - (Optional[int]): The number of points. - """ - if self.points is not None and len(self.points) > 0: - if self.n_points != len(self.points) and self.n_points is not None: - logger.warning( - f'The stored `n_points`, {self.n_points}, does not coincide with the length of `points`, ' - f'{len(self.points)}. We will re-assign `n_points` as the length of `points`.' - ) - return len(self.points) - return self.n_points - - def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: - super().normalize(archive, logger) - - # Setting `n_points` if these are not defined - self.n_points = self.get_n_points(logger) - - -class Temperature(Variables): - """ """ - - points = Quantity( - type=np.float64, - unit='kelvin', - shape=['n_points'], - description=""" - Points in which the temperature is discretized. - """, - ) - - 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: 'EntryArchive', logger: 'BoundLogger') -> None: - super().normalize(archive, logger) - - -# ! This needs to be fixed as it gives errors when running normalizers with conflicting names (ask Area D) -class Energy2(Variables): - """ """ - - points = Quantity( - type=np.float64, - unit='joule', - shape=['n_points'], - description=""" - Points in which the energy is discretized. - """, - ) - - 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: 'EntryArchive', logger: 'BoundLogger') -> None: - super().normalize(archive, logger) +import abc +class SpectrumConverter(abc.ABC): + def from_wavelength(self, wavelengths: pint.Quantity) -> pint.Quantity: + return wavelengths + + @abc.abstractmethod + def from_frequency(self, frequencies: pint.Quantity) -> pint.Quantity: + pass + + @abc.abstractmethod + def from_energy(self, energies: pint.Quantity) -> pint.Quantity: + pass + + def convert(self, values: pint.Quantity) -> pint.Quantity: + if not isinstance(values, pint.Quantity): + raise ValueError("SpectrumConverter requires a pint.Quantity object.") + to = { + 'wavelength': self.from_wavelength, + 'frequency': self.from_frequency, + 'energy': self.from_energy, + } + return to[values.dimensionality](values) + + def __call__(self, *args, **kwds): + if len(args) == 1: + return self.convert(args[0]) class WignerSeitz(Variables):