From e0230c7ea646a38e3fdf9735d773155951105047 Mon Sep 17 00:00:00 2001 From: JosePizarro3 Date: Thu, 16 May 2024 14:17:34 +0200 Subject: [PATCH 01/16] Improved numerical_settings schema for KSpace Added KLinePath and KMesh variables with refs to the NumericalSettings sections --- src/nomad_simulations/model_method.py | 4 +- src/nomad_simulations/numerical_settings.py | 382 ++++++++++++++------ src/nomad_simulations/variables.py | 101 ++++++ 3 files changed, 364 insertions(+), 123 deletions(-) diff --git a/src/nomad_simulations/model_method.py b/src/nomad_simulations/model_method.py index 9737bae9..f2d4ce40 100644 --- a/src/nomad_simulations/model_method.py +++ b/src/nomad_simulations/model_method.py @@ -31,7 +31,7 @@ Context, ) -from .numerical_settings import NumericalSettings, KMesh +from .numerical_settings import NumericalSettings from .model_system import ModelSystem from .atoms_state import OrbitalsState, CoreHole from .utils import is_not_representative @@ -840,8 +840,6 @@ class ExcitedStateMethodology(ModelMethodElectronic): a_eln=ELNAnnotation(component='NumberEditQuantity'), ) - q_mesh = SubSection(sub_section=KMesh.m_def) - def normalize(self, archive, logger) -> None: super().normalize(archive, logger) diff --git a/src/nomad_simulations/numerical_settings.py b/src/nomad_simulations/numerical_settings.py index c0781d55..25158f13 100644 --- a/src/nomad_simulations/numerical_settings.py +++ b/src/nomad_simulations/numerical_settings.py @@ -18,8 +18,9 @@ import numpy as np import pint +import itertools from structlog.stdlib import BoundLogger -from typing import Optional, List, Tuple +from typing import Optional, List, Tuple, Union from ase.dft.kpoints import monkhorst_pack, get_monkhorst_pack_size_and_offset from nomad.units import ureg @@ -33,8 +34,8 @@ JSON, ) -from .model_system import ModelSystem -from .utils import is_not_representative +from nomad_simulations.model_system import ModelSystem +from nomad_simulations.utils import is_not_representative class NumericalSettings(ArchiveSection): @@ -56,7 +57,7 @@ def normalize(self, archive, logger) -> None: super().normalize(archive, logger) -class Mesh(NumericalSettings): +class Mesh(ArchiveSection): """ A base section used to specify the settings of a sampling mesh. It supports uniformly-spaced meshes and symmetry-reduced representations. @@ -166,88 +167,17 @@ def normalize(self, archive, logger) -> None: super().normalize(archive, logger) -class LinePathSegment(ArchiveSection): - """ - A base section used to define the settings of a single line path segment within a multidimensional mesh. - """ - - high_symmetry_path = Quantity( - type=str, - shape=[2], - description=""" - List of the two high-symmetry points followed in the line path segment, e.g., ['Gamma', 'X']. The - point's coordinates can be extracted from the values in the `self.m_parent.high_symmetry_points` JSON quantity. - """, - ) - - n_line_points = Quantity( - type=np.int32, - description=""" - Number of points in the line path segment. - """, - ) - - points = Quantity( - type=np.float64, - shape=['n_line_points', 3], - description=""" - List of all the points in the line path segment in units of the `reciprocal_lattice_vectors`. - """, - ) - - def resolve_points( - self, - high_symmetry_path: List[str], - n_line_points: int, - logger: BoundLogger, - ) -> Optional[np.ndarray]: - """ - Resolves the `points` of the `LinePathSegment` from the `high_symmetry_path` and the `n_line_points`. - - Args: - high_symmetry_path (List[str]): The high-symmetry path of the `LinePathSegment`. - n_line_points (int): The number of points in the line path segment. - logger (BoundLogger): The logger to log messages. - - Returns: - (Optional[List[np.ndarray]]): The resolved `points` of the `LinePathSegment`. - """ - if high_symmetry_path is None or n_line_points is None: - logger.warning( - 'Could not resolve `LinePathSegment.points` from `LinePathSegment.high_symmetry_path` and `LinePathSegment.n_line_points`.' - ) - return None - if self.m_parent.high_symmetry_points is None: - logger.warning( - 'Could not resolve the parent of `LinePathSegment` to extract `LinePathSegment.m_parent.high_symmetry_points`.' - ) - return None - start_point = self.m_parent.high_symmetry_points.get(self.high_symmetry_path[0]) - end_point = self.m_parent.high_symmetry_points.get(self.high_symmetry_path[1]) - return np.linspace(start_point, end_point, self.n_line_points) - - def normalize(self, archive, logger) -> None: - super().normalize(archive, logger) - - if self.points is None: - self.points = self.resolve_points( - self.high_symmetry_path, self.n_line_points, logger - ) - - class KMesh(Mesh): """ A base section used to specify the settings of a sampling mesh in reciprocal space. """ - reciprocal_lattice_vectors = Quantity( - type=np.float64, - shape=[3, 3], - unit='1/meter', + label = Quantity( + type=MEnum('k-mesh', 'q-mesh'), + default='k-mesh', description=""" - Reciprocal lattice vectors of the simulated cell, in Cartesian coordinates and - including the $2 pi$ pre-factor. The first index runs over each lattice vector. The - second index runs over the $x, y, z$ Cartesian coordinates. + Label used to identify the `KMesh` with the reciprocal vector used. In linear response, `k` is used for + refering to the wave-vector of electrons, while `q` is used for the scattering effect of the Coulomb potential. """, ) @@ -263,8 +193,8 @@ class KMesh(Mesh): type=np.float64, shape=['*', 3], description=""" - Full list of the mesh points without any symmetry operations. In the presence of symmetry - operations, this quantity is a larger list than `points` (as it will contain all the points in the Brillouin zone). + Full list of the mesh points without any symmetry operations. In the presence of symmetry operations, this quantity is a + larger list than `points` (as it will contain all the points in the Brillouin zone). """, ) @@ -273,10 +203,10 @@ class KMesh(Mesh): description=""" Dictionary containing the high-symmetry points and their points in terms of `reciprocal_lattice_vectors`. E.g., in a cubic lattice: - high_symmetry_points = { - 'Gamma': [0, 0, 0], + 'Gamma1': [0, 0, 0], 'X': [0.5, 0, 0], + ... } """, ) @@ -291,14 +221,30 @@ class KMesh(Mesh): """, ) - line_path_segments = SubSection(sub_section=LinePathSegment.m_def, repeats=True) - # TODO add extraction of `high_symmetry_points` using BandStructureNormalizer idea (left for later when defining outputs.py) - def __init__(self, m_def: Section = None, m_context: Context = None, **kwargs): - super().__init__(m_def, m_context, **kwargs) - # Set the name of the section - self.name = self.m_def.name + def _check_reciprocal_lattice_vectors( + self, reciprocal_lattice_vectors: Optional[pint.Quantity], logger: BoundLogger + ) -> bool: + """ + Check if the `reciprocal_lattice_vectors` exist and if they have the same dimensionality as `grid`. + + Args: + reciprocal_lattice_vectors (Optional[pint.Quantity]): The reciprocal lattice vectors of the atomic cell. + logger (BoundLogger): The logger to log messages. + + Returns: + (bool): True if the `reciprocal_lattice_vectors` exist and have the same dimensionality as `grid`, False otherwise. + """ + if reciprocal_lattice_vectors is None: + logger.warning('Could not find `reciprocal_lattice_vectors`.') + return False + if len(reciprocal_lattice_vectors) != 3 or len(self.grid) != 3: + logger.warning( + 'The `reciprocal_lattice_vectors` and the `grid` should have the same dimensionality.' + ) + return False + return True def resolve_points_and_offset( self, logger: BoundLogger @@ -330,7 +276,7 @@ def resolve_points_and_offset( return points, offset def get_k_line_density( - self, reciprocal_lattice_vectors: pint.Quantity, logger: BoundLogger + self, reciprocal_lattice_vectors: Optional[pint.Quantity], logger: BoundLogger ) -> Optional[np.float64]: """ Gets the k-line density of the `KMesh`. This quantity is used as a precision measure @@ -342,13 +288,8 @@ def get_k_line_density( Returns: (np.float64): The k-line density of the `KMesh`. """ - if reciprocal_lattice_vectors is None: - logger.error('No `reciprocal_lattice_vectors` input found.') - return None - if len(reciprocal_lattice_vectors) != 3 or len(self.grid) != 3: - logger.error( - 'The `reciprocal_lattice_vectors` and the `grid` should have the same dimensionality.' - ) + # Initial check + if self._check_reciprocal_lattice_vectors(reciprocal_lattice_vectors, logger): return None reciprocal_lattice_vectors = reciprocal_lattice_vectors.magnitude @@ -360,7 +301,10 @@ def get_k_line_density( ) def resolve_k_line_density( - self, model_systems: List[ModelSystem], logger: BoundLogger + self, + model_systems: List[ModelSystem], + reciprocal_lattice_vectors: pint.Quantity, + logger: BoundLogger, ) -> Optional[pint.Quantity]: """ Resolves the `k_line_density` of the `KMesh` from the the list of `ModelSystem`. @@ -372,6 +316,10 @@ def resolve_k_line_density( Returns: (Optional[pint.Quantity]): The resolved `k_line_density` of the `KMesh`. """ + # Initial check + if self._check_reciprocal_lattice_vectors(reciprocal_lattice_vectors, logger): + return None + for model_system in model_systems: # General checks to proceed with normalization if is_not_representative(model_system, logger): @@ -381,21 +329,9 @@ def resolve_k_line_density( logger.warning('`ModelSystem.type` is not describing a bulk system.') return None - atomic_cell = model_system.cell - if atomic_cell is None: - logger.warning('`ModelSystem.cell` was not found.') - return None - - # Set the `reciprocal_lattice_vectors` using ASE - ase_atoms = atomic_cell[0].to_ase_atoms(logger) - if self.reciprocal_lattice_vectors is None: - self.reciprocal_lattice_vectors = ( - 2 * np.pi * ase_atoms.get_reciprocal_cell() / ureg.angstrom - ) - # Resolve `k_line_density` if k_line_density := self.get_k_line_density( - self.reciprocal_lattice_vectors, logger + reciprocal_lattice_vectors, logger ): return k_line_density * ureg('m') return None @@ -413,33 +349,239 @@ def normalize(self, archive, logger) -> None: self.points, self.offset = self.resolve_points_and_offset(logger) # Calculate k_line_density for data quality measures - model_systems = self.m_xpath('m_parent.m_parent.model_system', dict=False) + model_systems = self.m_xpath( + 'm_parent.m_parent.m_parent.model_system', dict=False + ) + reciprocal_lattice_vectors = self.m_xpath( + 'm_parent.reciprocal_lattice_vectors', dict=False + ) if self.k_line_density is None: - self.k_line_density = self.resolve_k_line_density(model_systems, logger) + self.k_line_density = self.resolve_k_line_density( + model_systems, reciprocal_lattice_vectors, logger + ) -class QuasiparticlesFrequencyMesh(Mesh): +class KLinePath(ArchiveSection): """ - A base section used to specify the settings of a sampling mesh in the frequency real or imaginary space for quasiparticle calculations. + A base section used to define the settings of a k-line path within a multidimensional mesh. """ + high_symmetry_path = Quantity( + type=JSON, + description=""" + Dictionary containing the high-symmetry points (in units of the `reciprocal_lattice_vectors`) followed in + the k-line path. E.g., in a cubic lattice: + high_symmetry_path = { + 'Gamma': [0, 0, 0], + 'X': [0.5, 0, 0], + 'Y': [0, 0.5, 0], + } + """, + ) + + n_line_points = Quantity( + type=np.int32, + description=""" + Number of points in the k-line path. + """, + ) + points = Quantity( - type=np.complex128, - shape=['n_points', 'dimensionality'], - unit='joule', + type=np.float64, + shape=['n_line_points', 3], description=""" - List of all the points in the mesh in joules. + List of all the points in the k-line path in units of the `reciprocal_lattice_vectors`. """, ) + def get_high_symmetry_points_norm( + self, + reciprocal_lattice_vectors: Optional[pint.Quantity], + ) -> Optional[dict]: + """ + Get the high symmetry points norms from the dictionary of vectors in units of the `reciprocal_lattice_vectors`. This + function is useful when matching lists of points passed as norms to the high symmetry points in order to resolve + `KLinePath.points` + + Args: + reciprocal_lattice_vectors (Optional[np.ndarray]): The reciprocal lattice vectors of the atomic cell. + + Returns: + (Optional[dict]): The high symmetry points norms. + """ + # Checking if `reciprocal_lattice_vectors` is defined and taking its magnitude to operate + if reciprocal_lattice_vectors is None: + return None + rlv = reciprocal_lattice_vectors.magnitude + + # initializing the norms dictionary + high_symmetry_points_norms = { + key: 0.0 * reciprocal_lattice_vectors.u + for key in self.high_symmetry_path.keys() + } + # initializing the first point + prev_value_norm = 0.0 * reciprocal_lattice_vectors.u + prev_value_rlv = np.array([0, 0, 0]) + for i, (key, value) in enumerate(self.high_symmetry_path.items()): + if i == 0: + continue + value_rlv = value @ rlv + value_tot_rlv = value_rlv - prev_value_rlv + value_norm = ( + np.linalg.norm(value_tot_rlv) * reciprocal_lattice_vectors.u + + prev_value_norm + ) + high_symmetry_points_norms[key] = value_norm + + # accumulate value vector and norm + prev_value_rlv = value_rlv + prev_value_norm = value_norm + return high_symmetry_points_norms + + def resolve_points( + self, + points_norm: Union[np.ndarray, List[float]], + reciprocal_lattice_vectors: Optional[np.ndarray], + logger: BoundLogger, + ) -> None: + """ + Resolves the `points` of the `KLinePath` from the `points_norm` and the `reciprocal_lattice_vectors`. This is useful + when a list of points norms and the dictionary of high symmetry points are passed to resolve the `KLinePath.points`. + + Args: + points_norm (List[float]): List of points norms in the k-line path. + reciprocal_lattice_vectors (Optional[np.ndarray]): The reciprocal lattice vectors of the atomic cell. + logger (BoundLogger): The logger to log messages. + """ + # General checks for quantities + if self.high_symmetry_path is None: + logger.warning('Could not resolve `KLinePath.high_symmetry_path`.') + return None + if reciprocal_lattice_vectors is None: + logger.warning( + 'The `reciprocal_lattice_vectors` are not passed as an input.' + ) + return None + # Check if `points_norm` is a list and convert it to a numpy array + if isinstance(points_norm, list): + points_norm = np.array(points_norm) + + # Define `n_line_points` + if self.n_line_points is not None and len(points_norm) != self.n_line_points: + logger.info( + 'The length of the `points` and the stored `n_line_points` do not coincide. We will overwrite `n_line_points` with the new length of `points`.' + ) + self.n_line_points = len(points_norm) + + # Calculate the total norm of the path in order to find the closest indices in the list of `points_norm` + high_symmetry_points_norms = self.get_high_symmetry_points_norm( + reciprocal_lattice_vectors + ) + closest_indices = {} + for key, norm in high_symmetry_points_norms.items(): + closest_idx = (np.abs(points_norm - norm.magnitude)).argmin() + closest_indices[key] = closest_idx + + # Append the data in the new `points` in units of the `reciprocal_lattice_vectors` + points = [] + for i, (key, value) in enumerate(self.high_symmetry_path.items()): + if i == 0: + prev_value = value + prev_index = closest_indices[key] + continue + elif i == len(self.high_symmetry_path) - 1: + points.append( + np.linspace( + prev_value, value, num=closest_indices[key] - prev_index + 1 + ) + ) + else: + # pop the last element as it appears repeated in the next segment + points.append( + np.linspace( + prev_value, value, num=closest_indices[key] - prev_index + 1 + )[:-1] + ) + prev_value = value + prev_index = closest_indices[key] + new_points = list(itertools.chain(*points)) + # And store this information in the `points` quantity + if self.points is not None: + logger.info('Overwriting `KLinePath.points` with the resolved points.') + self.points = new_points + + def normalize(self, archive, logger) -> None: + super().normalize(archive, logger) + + +class KSpace(NumericalSettings): + """ + A base section used to specify the settings of the k-space. This section contains two main sub-sections, + depending on the k-space sampling: `k_mesh` or `k_line_path`. + """ + + reciprocal_lattice_vectors = Quantity( + type=np.float64, + shape=[3, 3], + unit='1/meter', + description=""" + Reciprocal lattice vectors of the simulated cell, in Cartesian coordinates and + including the $2 pi$ pre-factor. The first index runs over each lattice vector. The + second index runs over the $x, y, z$ Cartesian coordinates. + """, + ) + + k_mesh = SubSection(sub_section=KMesh.m_def, repeats=True) + + k_line_path = SubSection(sub_section=KLinePath.m_def) + def __init__(self, m_def: Section = None, m_context: Context = None, **kwargs): super().__init__(m_def, m_context, **kwargs) # Set the name of the section self.name = self.m_def.name + def resolve_reciprocal_lattice_vectors( + self, model_systems: List[ModelSystem], logger: BoundLogger + ) -> Optional[pint.Quantity]: + """ + Resolve the `reciprocal_lattice_vectors` of the `KSpace` from the representative `ModelSystem` section. + + Args: + model_systems (List[ModelSystem]): The list of `ModelSystem` sections. + logger (BoundLogger): The logger to log messages. + + Returns: + (Optional[pint.Quantity]): The resolved `reciprocal_lattice_vectors` of the `KSpace`. + """ + for model_system in model_systems: + # General checks to proceed with normalization + if is_not_representative(model_system, logger): + return None + # TODO extend this for other dimensions (@ndaelman-hu) + if model_system.type != 'bulk': + logger.warning('`ModelSystem.type` is not describing a bulk system.') + return None + + atomic_cell = model_system.cell + if atomic_cell is None: + logger.warning('`ModelSystem.cell` was not found.') + return None + + # Set the `reciprocal_lattice_vectors` using ASE + ase_atoms = atomic_cell[0].to_ase_atoms(logger) + return 2 * np.pi * ase_atoms.get_reciprocal_cell() / ureg.angstrom + return None + def normalize(self, archive, logger) -> None: super().normalize(archive, logger) + # Resolve `reciprocal_lattice_vectors` from the `ModelSystem` ASE object + model_systems = self.m_xpath('m_parent.m_parent.model_system', dict=False) + if self.reciprocal_lattice_vectors is None: + self.reciprocal_lattice_vectors = self.resolve_reciprocal_lattice_vectors( + model_systems, logger + ) + class SelfConsistency(NumericalSettings): """ diff --git a/src/nomad_simulations/variables.py b/src/nomad_simulations/variables.py index 03fdaa1e..e89bb576 100644 --- a/src/nomad_simulations/variables.py +++ b/src/nomad_simulations/variables.py @@ -23,6 +23,11 @@ from nomad.datamodel.data import ArchiveSection from nomad.metainfo import Quantity, Section, Context +from nomad_simulations.numerical_settings import ( + KMesh as KMeshSettings, + KLinePath as KLinePathSettings, +) + class Variables(ArchiveSection): """ @@ -150,3 +155,99 @@ def __init__( def normalize(self, archive, logger) -> None: super().normalize(archive, logger) + + +class KMesh(Variables): + """ + 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 refernece to the `NumericalSettings` section, `KMesh(NumericalSettings)`. + """ + + k_mesh_ref = Quantity( + type=KMeshSettings, + description=""" + Reference to the `KMesh(NumericalSettings)` section in the `ModelMethod` section. This reference is useful + to extract `points` and, then, obtain the shape of `value` of the `PhysicalProperty`. + """, + ) + + points = Quantity( + type=np.float64, + shape=['n_points', 'dimensionality'], + description=""" + K-point mesh over which the physical property is calculated. These are 3D arrays stored in fractional coordinates. + """, + ) + + 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 extract_points(self, logger: BoundLogger) -> Optional[list]: + """ + Extract the `points` list from the `k_mesh_settings_ref` pointing to the `KMesh` section. + Args: + logger (BoundLogger): The logger to log messages. + Returns: + (Optional[list]): The `points` list. + """ + if self.k_mesh_settings_ref is not None: + if self.k_mesh_settings_ref.points is not None: + return self.k_mesh_settings_ref.points + points, _ = self.k_mesh_settings_ref.resolve_points_and_offset(logger) + return points + logger.error('`k_mesh_settings_ref` is not defined.') + return None + + def normalize(self, archive, logger) -> None: + # Extracting `points` from the `k_mesh_settings_ref` BEFORE doing `super().normalize()` + self.points = self.extract_points(logger) + + super().normalize(archive, logger) + + +class KLinePath(Variables): + """ """ + + k_line_path_ref = Quantity( + type=KLinePathSettings, + description=""" + Reference to the `KLinePath(NumericalSettings)` section in the `ModelMethod.KMesh` section. This reference is useful + to extract `points` and, then, obtain the shape of `value` of the `PhysicalProperty`. + """, + ) + + points = Quantity( + type=np.float64, + shape=['n_points', 3], + description=""" + Points along the k-line path in which the physical property is calculated. These are 3D arrays stored in fractional coordinates. + """, + ) + + 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 extract_points(self, logger: BoundLogger) -> Optional[list]: + """ + Extract the `points` list from the `k_line_path_ref` pointing to the `KLinePath` section. + Args: + logger (BoundLogger): The logger to log messages. + Returns: + (Optional[list]): The `points` list. + """ + if self.k_line_path_ref is not None: + return self.k_line_path_ref.points + logger.error('`k_line_path_ref` is not defined.') + return None + + def normalize(self, archive, logger) -> None: + # Extracting `points` from the `k_line_path_ref` BEFORE doing `super().normalize()` + self.points = self.extract_points(logger) + + super().normalize(archive, logger) From 934a10d3034ca8b0ec0cf0135de902e7c2e9564e Mon Sep 17 00:00:00 2001 From: JosePizarro3 Date: Thu, 16 May 2024 14:58:19 +0200 Subject: [PATCH 02/16] Added testing for KSpace Added testing for KLinePath --- tests/conftest.py | 70 +++++++++++++++- tests/test_numerical_settings.py | 139 +++++++++++++++++++++++++++++++ 2 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 tests/test_numerical_settings.py diff --git a/tests/conftest.py b/tests/conftest.py index bde50e79..718aaf25 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,6 +17,7 @@ # import os +import numpy as np import pytest from typing import List, Optional @@ -29,7 +30,12 @@ from nomad_simulations.model_system import ModelSystem, AtomicCell from nomad_simulations.atoms_state import AtomsState, OrbitalsState from nomad_simulations.model_method import ModelMethod -from nomad_simulations.numerical_settings import SelfConsistency +from nomad_simulations.numerical_settings import ( + SelfConsistency, + KSpace, + KMesh as KMeshSettings, + KLinePath as KLinePathSettings, +) from nomad_simulations.outputs import Outputs, SCFOutputs from nomad_simulations.variables import Energy2 as Energy from nomad_simulations.properties import ( @@ -71,9 +77,12 @@ def generate_simulation( def generate_model_system( type: str = 'original', + system_type: str = 'bulk', positions: List[List[float]] = [[0, 0, 0], [0.5, 0.5, 0.5]], + lattice_vectors: List[List[float]] = [[1, 0, 0], [0, 1, 0], [0, 0, 1]], chemical_symbols: List[str] = ['Ga', 'As'], orbitals_symbols: List[List[str]] = [['s'], ['px', 'py']], + is_representative: bool = True, ) -> Optional[ModelSystem]: """ Generate a `ModelSystem` section with the given parameters. @@ -81,8 +90,12 @@ def generate_model_system( if len(chemical_symbols) != len(orbitals_symbols): return None - model_system = ModelSystem() - atomic_cell = AtomicCell(type=type, positions=positions * ureg.meter) + model_system = ModelSystem(type=system_type, is_representative=is_representative) + atomic_cell = AtomicCell( + type=type, + positions=positions * ureg.angstrom, + lattice_vectors=lattice_vectors * ureg.angstrom, + ) model_system.cell.append(atomic_cell) # Add atoms_state to the model_system @@ -208,6 +221,47 @@ def generate_simulation_electronic_dos( return simulation +def generate_k_line_path( + high_symmetry_path: dict = { + 'Gamma1': [0, 0, 0], + 'X': [0.5, 0, 0], + 'Y': [0, 0.5, 0], + 'Gamma2': [0, 0, 0], + }, # ! fix the patch on naming 'Gamma1' and 'Gamma2' +) -> KLinePathSettings: + return KLinePathSettings(high_symmetry_path=high_symmetry_path) + + +def generate_k_space_simulation( + system_type: str = 'bulk', + is_representative: bool = True, + reciprocal_lattice_vectors: Optional[List[List[int]]] = [ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1], + ], + high_symmetry_path: dict = { + 'Gamma1': [0, 0, 0], + 'X': [0.5, 0, 0], + 'Y': [0, 0.5, 0], + 'Gamma2': [0, 0, 0], + }, +) -> Simulation: + model_system = generate_model_system( + system_type=system_type, is_representative=is_representative + ) + k_space = KSpace() + if reciprocal_lattice_vectors is not None: + k_space.reciprocal_lattice_vectors = ( + 2 * np.pi * np.array(reciprocal_lattice_vectors) / ureg.angstrom + ) + k_line_path = KLinePathSettings(high_symmetry_path=high_symmetry_path) + k_space.k_line_path = k_line_path + model_method = ModelMethod() + model_method.numerical_settings.append(k_space) + return generate_simulation(model_method=model_method, model_system=model_system) + + @pytest.fixture(scope='session') def model_system() -> ModelSystem: return generate_model_system() @@ -226,3 +280,13 @@ def scf_electronic_band_gap() -> SCFOutputs: @pytest.fixture(scope='session') def simulation_electronic_dos() -> Simulation: return generate_simulation_electronic_dos() + + +@pytest.fixture(scope='session') +def k_line_path() -> KLinePathSettings: + return generate_k_line_path() + + +@pytest.fixture(scope='session') +def k_space_simulation() -> Simulation: + return generate_k_space_simulation() diff --git a/tests/test_numerical_settings.py b/tests/test_numerical_settings.py new file mode 100644 index 00000000..db4c3232 --- /dev/null +++ b/tests/test_numerical_settings.py @@ -0,0 +1,139 @@ +# +# 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 pytest +import numpy as np +from typing import Optional, List + +from nomad.units import ureg +from nomad.datamodel import EntryArchive + +from nomad_simulations.numerical_settings import KSpace, KMesh, KLinePath + +from . import logger +from .conftest import generate_k_space_simulation + + +class TestKSpace: + """ + Test the `KSpace` class defined in `numerical_settings.py`. + """ + + @pytest.mark.parametrize( + 'system_type, is_representative, reciprocal_lattice_vectors, result', + [ + ('bulk', False, None, None), + ('atom', True, None, None), + ('bulk', True, None, [[1, 0, 0], [0, 1, 0], [0, 0, 1]]), + ( + 'bulk', + True, + [[1, 0, 0], [0, 1, 0], [0, 0, 1]], + [[1, 0, 0], [0, 1, 0], [0, 0, 1]], + ), + ], + ) + def test_normalize( + self, + system_type: Optional[str], + is_representative: bool, + reciprocal_lattice_vectors: Optional[List[List[float]]], + result: List[List[float]], + ): + """ + Test the `normalize` method. This also test the `resolve_reciprocal_lattice_vectors` method. + """ + simulation = generate_k_space_simulation( + system_type=system_type, + is_representative=is_representative, + reciprocal_lattice_vectors=reciprocal_lattice_vectors, + ) + k_space = simulation.model_method[0].numerical_settings[0] + assert k_space.name == 'KSpace' + k_space.normalize(EntryArchive(), logger) + if k_space.reciprocal_lattice_vectors is not None: + value = k_space.reciprocal_lattice_vectors.to('1/angstrom').magnitude / ( + 2 * np.pi + ) + assert np.allclose(value, result) + else: + assert k_space.reciprocal_lattice_vectors == result + + +class TestKLinePath: + """ + Test the `KLinePath` class defined in `numerical_settings.py`. + """ + + def test_get_high_symmetry_points_norm(self, k_line_path: KLinePath): + """ + Test the `get_high_symmetry_points_norm` method. + """ + rlv = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) * ureg('1/meter') + high_symmetry_points_norms = k_line_path.get_high_symmetry_points_norm( + reciprocal_lattice_vectors=rlv + ) + hs_points = { + 'Gamma1': 0, + 'X': 0.5, + 'Y': 0.5 + 1 / np.sqrt(2), + 'Gamma2': 1 + 1 / np.sqrt(2), + } + for key, val in hs_points.items(): + assert np.isclose(high_symmetry_points_norms[key].magnitude, val) + + def test_resolve_points(self, k_line_path: KLinePath): + """ + Test the `resolve_points` method. + """ + rlv = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) * ureg('1/meter') + hs_points = { + 'Gamma1': 0, + 'X': 0.5, + 'Y': 0.5 + 1 / np.sqrt(2), + 'Gamma2': 1 + 1 / np.sqrt(2), + } + # Define paths + gamma_x = np.linspace(hs_points['Gamma1'], hs_points['X'], num=5) + x_y = np.linspace(hs_points['X'], hs_points['Y'], num=5) + y_gamma = np.linspace(hs_points['Y'], hs_points['Gamma2'], num=5) + points_norm = np.concatenate((gamma_x, x_y, y_gamma)) + k_line_path.resolve_points( + points_norm=points_norm, reciprocal_lattice_vectors=rlv, logger=logger + ) + assert len(points_norm) == len(k_line_path.points) + points = np.array( + [ + [0.0, 0.0, 0.0], # 'Gamma' + [0.125, 0.0, 0.0], + [0.25, 0.0, 0.0], + [0.375, 0.0, 0.0], + [0.5, 0.0, 0.0], # 'X' + [0.4, 0.1, 0.0], + [0.3, 0.2, 0.0], + [0.2, 0.3, 0.0], + [0.1, 0.4, 0.0], + [0.0, 0.5, 0.0], # 'Y' + [0.0, 0.4, 0.0], + [0.0, 0.3, 0.0], + [0.0, 0.2, 0.0], + [0.0, 0.1, 0.0], + [0.0, 0.0, 0.0], # 'Gamma' + ] + ) + assert np.allclose(k_line_path.points, points) From 7b989cbc8da3cf1ba600576d6c7cdcf9c102c05f Mon Sep 17 00:00:00 2001 From: JosePizarro3 Date: Thu, 16 May 2024 14:59:25 +0200 Subject: [PATCH 03/16] Add todo in testing --- tests/test_numerical_settings.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_numerical_settings.py b/tests/test_numerical_settings.py index db4c3232..1f82c182 100644 --- a/tests/test_numerical_settings.py +++ b/tests/test_numerical_settings.py @@ -75,6 +75,9 @@ def test_normalize( assert k_space.reciprocal_lattice_vectors == result +# TODO add testing for KMesh + + class TestKLinePath: """ Test the `KLinePath` class defined in `numerical_settings.py`. From cc4e05326fcb52e0c07bb29688cbc83446a94558 Mon Sep 17 00:00:00 2001 From: JosePizarro3 Date: Thu, 16 May 2024 15:00:45 +0200 Subject: [PATCH 04/16] Fixed mypy --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 718aaf25..0cafcfbc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -235,7 +235,7 @@ def generate_k_line_path( def generate_k_space_simulation( system_type: str = 'bulk', is_representative: bool = True, - reciprocal_lattice_vectors: Optional[List[List[int]]] = [ + reciprocal_lattice_vectors: Optional[List[List[float]]] = [ [1, 0, 0], [0, 1, 0], [0, 0, 1], From d5cb274c6ed29d28f8f29d5df711c331e90deaab Mon Sep 17 00:00:00 2001 From: JosePizarro3 Date: Tue, 21 May 2024 11:07:19 +0200 Subject: [PATCH 05/16] Added testing for KMesh --- src/nomad_simulations/numerical_settings.py | 31 +++- tests/conftest.py | 7 + tests/test_numerical_settings.py | 156 +++++++++++++++++++- 3 files changed, 185 insertions(+), 9 deletions(-) diff --git a/src/nomad_simulations/numerical_settings.py b/src/nomad_simulations/numerical_settings.py index 25158f13..63acc6b2 100644 --- a/src/nomad_simulations/numerical_settings.py +++ b/src/nomad_simulations/numerical_settings.py @@ -237,7 +237,12 @@ def _check_reciprocal_lattice_vectors( (bool): True if the `reciprocal_lattice_vectors` exist and have the same dimensionality as `grid`, False otherwise. """ if reciprocal_lattice_vectors is None: - logger.warning('Could not find `reciprocal_lattice_vectors`.') + logger.warning( + 'Could not find `reciprocal_lattice_vectors` from parent `KSpace`.' + ) + return False + if self.grid is None: + logger.warning('Could not find `KMesh.grid`.') return False if len(reciprocal_lattice_vectors) != 3 or len(self.grid) != 3: logger.warning( @@ -258,6 +263,10 @@ def resolve_points_and_offset( Returns: (Optional[List[pint.Quantity, pint.Quantity]]): The resolved `points` and `offset` of the `KMesh`. """ + if self.grid is None: + logger.warning('Could not find `KMesh.grid`.') + return None, None + points = None offset = None if self.center == 'Gamma-centered': @@ -272,7 +281,8 @@ def resolve_points_and_offset( logger.warning( 'Could not resolve `KMesh.points` and `KMesh.offset` from `KMesh.grid`. ASE `monkhorst_pack` failed.' ) - return None # this is a quick workaround: k_mesh.grid should be symmetry reduced + # this is a quick workaround: k_mesh.grid should be symmetry reduced + return None, None return points, offset def get_k_line_density( @@ -289,16 +299,19 @@ def get_k_line_density( (np.float64): The k-line density of the `KMesh`. """ # Initial check - if self._check_reciprocal_lattice_vectors(reciprocal_lattice_vectors, logger): + if not self._check_reciprocal_lattice_vectors( + reciprocal_lattice_vectors, logger + ): return None - reciprocal_lattice_vectors = reciprocal_lattice_vectors.magnitude - return min( + rlv = reciprocal_lattice_vectors.magnitude + k_line_density = min( [ k_point / (np.linalg.norm(k_vector)) - for k_vector, k_point in zip(reciprocal_lattice_vectors, self.grid) + for k_vector, k_point in zip(rlv, self.grid) ] ) + return k_line_density / reciprocal_lattice_vectors.u def resolve_k_line_density( self, @@ -317,7 +330,9 @@ def resolve_k_line_density( (Optional[pint.Quantity]): The resolved `k_line_density` of the `KMesh`. """ # Initial check - if self._check_reciprocal_lattice_vectors(reciprocal_lattice_vectors, logger): + if not self._check_reciprocal_lattice_vectors( + reciprocal_lattice_vectors, logger + ): return None for model_system in model_systems: @@ -333,7 +348,7 @@ def resolve_k_line_density( if k_line_density := self.get_k_line_density( reciprocal_lattice_vectors, logger ): - return k_line_density * ureg('m') + return k_line_density return None def normalize(self, archive, logger) -> None: diff --git a/tests/conftest.py b/tests/conftest.py index 0cafcfbc..e2c3c2ac 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -246,17 +246,24 @@ def generate_k_space_simulation( 'Y': [0, 0.5, 0], 'Gamma2': [0, 0, 0], }, + grid=[6, 6, 6], ) -> Simulation: model_system = generate_model_system( system_type=system_type, is_representative=is_representative ) k_space = KSpace() + # adding `reciprocal_lattice_vectors` if reciprocal_lattice_vectors is not None: k_space.reciprocal_lattice_vectors = ( 2 * np.pi * np.array(reciprocal_lattice_vectors) / ureg.angstrom ) + # adding `KMeshSettings + k_mesh = KMeshSettings(grid=grid) + k_space.k_mesh.append(k_mesh) + # adding `KLinePathSettings` k_line_path = KLinePathSettings(high_symmetry_path=high_symmetry_path) k_space.k_line_path = k_line_path + # appending `KSpace` to `ModelMethod.numerical_settings` model_method = ModelMethod() model_method.numerical_settings.append(k_space) return generate_simulation(model_method=model_method, model_system=model_system) diff --git a/tests/test_numerical_settings.py b/tests/test_numerical_settings.py index 1f82c182..5fdab0c7 100644 --- a/tests/test_numerical_settings.py +++ b/tests/test_numerical_settings.py @@ -23,7 +23,7 @@ from nomad.units import ureg from nomad.datamodel import EntryArchive -from nomad_simulations.numerical_settings import KSpace, KMesh, KLinePath +from nomad_simulations.numerical_settings import KMesh, KLinePath from . import logger from .conftest import generate_k_space_simulation @@ -76,6 +76,160 @@ def test_normalize( # TODO add testing for KMesh +class TestKMesh: + """ + Test the `KMesh` class defined in `numerical_settings.py`. + """ + + @pytest.mark.parametrize( + 'center, grid, result_points, result_offset', + [ + # No `center` and `grid` + (None, None, None, None), + # No `grid` + ('Gamma-centered', None, None, None), + ('Monkhorst-Pack', None, None, None), + # `center` is `'Gamma-centered'` + ( + 'Gamma-centered', + [2, 2, 2], + [[0.0, 1.0, 0.0, 1.0, 0.0, 1.0]], # ! this result is weird @ndaelman-hu + [0.0, 0.0, 0.0], + ), + # `center` is `'Monkhorst-Pack'` + ( + 'Monkhorst-Pack', + [2, 2, 2], + [ + [-0.25, -0.25, -0.25], + [-0.25, -0.25, 0.25], + [-0.25, 0.25, -0.25], + [-0.25, 0.25, 0.25], + [0.25, -0.25, -0.25], + [0.25, -0.25, 0.25], + [0.25, 0.25, -0.25], + [0.25, 0.25, 0.25], + ], + [0.0, 0.0, 0.0], + ), + # Invalid `grid` + ('Monkhorst-Pack', [-2, 2, 2], None, None), + ], + ) + def test_resolve_points_and_offset( + self, center, grid, result_points, result_offset + ): + """ + Test the `resolve_points_and_offset` method. + """ + k_mesh = KMesh(center=center) + if grid is not None: + k_mesh.grid = grid + points, offset = k_mesh.resolve_points_and_offset(logger) + if points is not None: + assert np.allclose(points, result_points) + else: + assert points == result_points + if offset is not None: + assert np.allclose(offset, result_offset) + else: + assert offset == result_offset + + @pytest.mark.parametrize( + 'system_type, is_representative, grid, reciprocal_lattice_vectors, result_check, result_get_k_line_density, result_k_line_density', + [ + # No `grid` and `reciprocal_lattice_vectors` + ('bulk', False, None, None, False, None, None), + # No `reciprocal_lattice_vectors` + ('bulk', False, [6, 6, 6], None, False, None, None), + # No `grid` + ('bulk', False, None, [[1, 0, 0], [0, 1, 0], [0, 0, 1]], False, None, None), + # `is_representative` set to False + ( + 'bulk', + False, + [6, 6, 6], + [[1, 0, 0], [0, 1, 0], [0, 0, 1]], + True, + 0.954929658, + None, + ), + # `system_type` is not 'bulk' + ( + 'atom', + True, + [6, 6, 6], + [[1, 0, 0], [0, 1, 0], [0, 0, 1]], + True, + 0.954929658, + None, + ), + # All parameters are set + ( + 'bulk', + True, + [6, 6, 6], + [[1, 0, 0], [0, 1, 0], [0, 0, 1]], + True, + 0.954929658, + 0.954929658, + ), + ], + ) + def test_resolve_k_line_density( + self, + system_type: Optional[str], + is_representative: bool, + grid: Optional[List[int]], + reciprocal_lattice_vectors: Optional[List[List[float]]], + result_check: bool, + result_get_k_line_density: Optional[float], + result_k_line_density: Optional[float], + ): + """ + Test the `resolve_k_line_density` and `get_k_line_density` methods, as well as the `_check_reciprocal_lattice_vectors` + private method. + """ + simulation = generate_k_space_simulation( + system_type=system_type, + is_representative=is_representative, + reciprocal_lattice_vectors=reciprocal_lattice_vectors, + grid=grid, + ) + k_space = simulation.model_method[0].numerical_settings[0] + reciprocal_lattice_vectors = k_space.reciprocal_lattice_vectors + k_mesh = k_space.k_mesh[0] + model_systems = simulation.model_system + # Checking the reciprocal lattice vectors + assert ( + k_mesh._check_reciprocal_lattice_vectors( + reciprocal_lattice_vectors=reciprocal_lattice_vectors, logger=logger + ) + == result_check + ) + # Applying method `get_k_line_density` + get_k_line_density_value = k_mesh.get_k_line_density( + reciprocal_lattice_vectors=reciprocal_lattice_vectors, logger=logger + ) + if get_k_line_density_value is not None: + assert np.isclose( + get_k_line_density_value.to('angstrom').magnitude, + result_get_k_line_density, + ) + else: + assert get_k_line_density_value == result_get_k_line_density + # Applying method `resolve_k_line_density` + k_line_density = k_mesh.resolve_k_line_density( + model_systems=model_systems, + reciprocal_lattice_vectors=reciprocal_lattice_vectors, + logger=logger, + ) + if k_line_density is not None: + assert np.isclose( + k_line_density.to('angstrom').magnitude, result_k_line_density + ) + else: + assert k_line_density == result_k_line_density class TestKLinePath: From ff3df346f2a87eeade01189ca890ffac69ffab6d Mon Sep 17 00:00:00 2001 From: JosePizarro3 Date: Tue, 21 May 2024 11:23:57 +0200 Subject: [PATCH 06/16] Fixed name of ref in KMesh and KLinePath variables --- src/nomad_simulations/variables.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/nomad_simulations/variables.py b/src/nomad_simulations/variables.py index e89bb576..68304e9f 100644 --- a/src/nomad_simulations/variables.py +++ b/src/nomad_simulations/variables.py @@ -163,7 +163,7 @@ class KMesh(Variables): other k-space properties. The `points` are obtained from a refernece to the `NumericalSettings` section, `KMesh(NumericalSettings)`. """ - k_mesh_ref = Quantity( + k_mesh_settings_ref = Quantity( type=KMeshSettings, description=""" Reference to the `KMesh(NumericalSettings)` section in the `ModelMethod` section. This reference is useful @@ -211,7 +211,7 @@ def normalize(self, archive, logger) -> None: class KLinePath(Variables): """ """ - k_line_path_ref = Quantity( + k_line_path_settings_ref = Quantity( type=KLinePathSettings, description=""" Reference to the `KLinePath(NumericalSettings)` section in the `ModelMethod.KMesh` section. This reference is useful @@ -235,19 +235,19 @@ def __init__( def extract_points(self, logger: BoundLogger) -> Optional[list]: """ - Extract the `points` list from the `k_line_path_ref` pointing to the `KLinePath` section. + Extract the `points` list from the `k_line_path_settings_ref` pointing to the `KLinePath` section. Args: logger (BoundLogger): The logger to log messages. Returns: (Optional[list]): The `points` list. """ - if self.k_line_path_ref is not None: - return self.k_line_path_ref.points - logger.error('`k_line_path_ref` is not defined.') + if self.k_line_path_settings_ref is not None: + return self.k_line_path_settings_ref.points + logger.error('`k_line_path_settings_ref` is not defined.') return None def normalize(self, archive, logger) -> None: - # Extracting `points` from the `k_line_path_ref` BEFORE doing `super().normalize()` + # Extracting `points` from the `k_line_path_settings_ref` BEFORE doing `super().normalize()` self.points = self.extract_points(logger) super().normalize(archive, logger) From 19dd21816c4da6e01d6b8f98f5c48819ca918609 Mon Sep 17 00:00:00 2001 From: JosePizarro3 Date: Wed, 22 May 2024 11:40:51 +0200 Subject: [PATCH 07/16] Fix numerical settings high_symmetry_path quantity Added functionality resolve_high_symmetry_points Fix mypy --- src/nomad_simulations/numerical_settings.py | 225 +++++++++++++++----- tests/conftest.py | 41 ++-- tests/test_numerical_settings.py | 60 ++++-- 3 files changed, 233 insertions(+), 93 deletions(-) diff --git a/src/nomad_simulations/numerical_settings.py b/src/nomad_simulations/numerical_settings.py index 63acc6b2..a9173a6e 100644 --- a/src/nomad_simulations/numerical_settings.py +++ b/src/nomad_simulations/numerical_settings.py @@ -20,19 +20,12 @@ import pint import itertools from structlog.stdlib import BoundLogger -from typing import Optional, List, Tuple, Union +from typing import Optional, List, Tuple, Union, Dict from ase.dft.kpoints import monkhorst_pack, get_monkhorst_pack_size_and_offset from nomad.units import ureg from nomad.datamodel.data import ArchiveSection -from nomad.metainfo import ( - Quantity, - SubSection, - MEnum, - Section, - Context, - JSON, -) +from nomad.metainfo import Quantity, SubSection, MEnum, Section, Context, JSON from nomad_simulations.model_system import ModelSystem from nomad_simulations.utils import is_not_representative @@ -169,7 +162,9 @@ def normalize(self, archive, logger) -> None: class KMesh(Mesh): """ - A base section used to specify the settings of a sampling mesh in reciprocal space. + A base section used to specify the settings of a sampling mesh in reciprocal space. The `points` and other + k-space quantities are defined in units of the reciprocal lattice vectors, so that to obtain their Cartesian coordinates + value, one should multiply them by the reciprocal lattice vectors (`points_cartesian = points @ reciprocal_lattice_vectors`). """ label = Quantity( @@ -193,21 +188,23 @@ class KMesh(Mesh): type=np.float64, shape=['*', 3], description=""" - Full list of the mesh points without any symmetry operations. In the presence of symmetry operations, this quantity is a - larger list than `points` (as it will contain all the points in the Brillouin zone). + Full list of the mesh points without any symmetry operations in units of the `reciprocal_lattice_vectors`. In the + presence of symmetry operations, this quantity is a larger list than `points` (as it will contain all the points + in the Brillouin zone). """, ) high_symmetry_points = Quantity( type=JSON, description=""" - Dictionary containing the high-symmetry points and their points in terms of `reciprocal_lattice_vectors`. + Dictionary containing the high-symmetry point labels and their values in units of `reciprocal_lattice_vectors`. E.g., in a cubic lattice: - high_symmetry_points = { - 'Gamma1': [0, 0, 0], + high_symmetry_points ={ + 'Gamma': [0, 0, 0], 'X': [0.5, 0, 0], + 'Y': [0, 0.5, 0], ... - } + ] """, ) @@ -215,8 +212,7 @@ class KMesh(Mesh): type=np.float64, unit='m', description=""" - Amount of sampled k-points per unit reciprocal length along each axis. - Contains the least precise density out of all axes. + Amount of sampled k-points per unit reciprocal length along each axis. Contains the least precise density out of all axes. Should only be compared between calulations of similar dimensionality. """, ) @@ -236,14 +232,11 @@ def _check_reciprocal_lattice_vectors( Returns: (bool): True if the `reciprocal_lattice_vectors` exist and have the same dimensionality as `grid`, False otherwise. """ - if reciprocal_lattice_vectors is None: + if reciprocal_lattice_vectors is None or self.grid is None: logger.warning( - 'Could not find `reciprocal_lattice_vectors` from parent `KSpace`.' + 'Could not find `reciprocal_lattice_vectors` from parent `KSpace` or could not find `KMesh.grid`.' ) return False - if self.grid is None: - logger.warning('Could not find `KMesh.grid`.') - return False if len(reciprocal_lattice_vectors) != 3 or len(self.grid) != 3: logger.warning( 'The `reciprocal_lattice_vectors` and the `grid` should have the same dimensionality.' @@ -338,11 +331,11 @@ def resolve_k_line_density( for model_system in model_systems: # General checks to proceed with normalization if is_not_representative(model_system, logger): - return None + continue # TODO extend this for other dimensions (@ndaelman-hu) if model_system.type != 'bulk': logger.warning('`ModelSystem.type` is not describing a bulk system.') - return None + continue # Resolve `k_line_density` if k_line_density := self.get_k_line_density( @@ -351,6 +344,99 @@ def resolve_k_line_density( return k_line_density return None + def resolve_high_symmetry_points( + self, + model_systems: List[ModelSystem], + logger: BoundLogger, + eps: float = 3e-3, + ) -> Optional[dict]: + """ + Resolves the `high_symmetry_points` of the `KMesh` from the list of `ModelSystem`. This method + relies on using the `ModelSystem` information in the sub-sections `Symmetry` and `AtomicCell`, and uses + the ASE package to extract the special (high symmetry) points information. + + Args: + model_systems (List[ModelSystem]): The list of `ModelSystem` sections. + logger (BoundLogger): The logger to log messages. + eps (float, optional): Tolerance factor to define the `lattice` ASE object. Defaults to 3e-3. + + Returns: + (Optional[dict]): The resolved `high_symmetry_points` of the `KMesh`. + """ + # Extracting `bravais_lattice` from `ModelSystem.symmetry` section and `ASE.cell` from `ModelSystem.cell` + lattice = None + for model_system in model_systems: + # General checks to proceed with normalization + if is_not_representative(model_system, logger): + continue + if model_system.symmetry is None: + logger.warning('Could not find `ModelSystem.symmetry`.') + continue + bravais_lattice = [symm.bravais_lattice for symm in model_system.symmetry] + if len(bravais_lattice) != 1: + logger.warning( + 'Could not uniquely determine `bravais_lattice` from `ModelSystem.symmetry`.' + ) + continue + bravais_lattice = bravais_lattice[0] + + if model_system.cell is None: + logger.warning('Could not find `ModelSystem.cell`.') + continue + prim_atomic_cell = None + for atomic_cell in model_system.cell: + if atomic_cell.type == 'primitive': + prim_atomic_cell = atomic_cell + break + if prim_atomic_cell is None: + logger.warning( + 'Could not find the primitive `AtomicCell` under `ModelSystem.cell`.' + ) + continue + # function defined in AtomicCell + atoms = prim_atomic_cell.to_ase_atoms(logger) + cell = atoms.get_cell() + lattice = cell.get_bravais_lattice(eps) + break # only cover the first representative `ModelSystem` + + # Checking if `bravais_lattice` and `lattice` are defined + if lattice is None: + logger.warning( + 'Could not resolve `bravais_lattice` and `lattice` ASE object from the `ModelSystem`.' + ) + return None + + # Non-conventional ordering testing for certain lattices: + if bravais_lattice in ['oP', 'oF', 'oI', 'oS']: + a, b, c = lattice.a, lattice.b, lattice.c + assert a < b + if bravais_lattice != 'oS': + assert b < c + elif bravais_lattice in ['mP', 'mS']: + a, b, c = lattice.a, lattice.b, lattice.c + alpha = lattice.alpha * np.pi / 180 + assert a <= c and b <= c # ordering of the conventional lattice + assert alpha < np.pi / 2 + + # Extracting the `high_symmetry_points` from the `lattice` object + special_points = lattice.get_special_points() + if special_points is None: + logger.warning( + 'Could not find `lattice.get_special_points()` from the ASE package.' + ) + return None + high_symmetry_points = {} + for key, value in lattice.get_special_points().items(): + if key == 'G': + key = 'Gamma' + if bravais_lattice == 'tI': + if key == 'S': + key = 'Sigma' + elif key == 'S1': + key = 'Sigma1' + high_symmetry_points[key] = list(value) + return high_symmetry_points + def normalize(self, archive, logger) -> None: super().normalize(archive, logger) @@ -375,22 +461,32 @@ def normalize(self, archive, logger) -> None: model_systems, reciprocal_lattice_vectors, logger ) + # Resolve `high_symmetry_points` + if self.high_symmetry_points is None: + self.high_symmetry_points = self.resolve_high_symmetry_points( + model_systems, logger + ) + class KLinePath(ArchiveSection): """ - A base section used to define the settings of a k-line path within a multidimensional mesh. + A base section used to define the settings of a k-line path within a multidimensional mesh. The `points` and other + k-space quantities are defined in units of the reciprocal lattice vectors, so that to obtain their Cartesian coordinates + value, one should multiply them by the reciprocal lattice vectors (`points_cartesian = points @ reciprocal_lattice_vectors`). """ high_symmetry_path = Quantity( type=JSON, + shape=['*'], description=""" - Dictionary containing the high-symmetry points (in units of the `reciprocal_lattice_vectors`) followed in + List of dictionaries containing the high-symmetry path (in units of the `reciprocal_lattice_vectors`) followed in the k-line path. E.g., in a cubic lattice: - high_symmetry_path = { - 'Gamma': [0, 0, 0], - 'X': [0.5, 0, 0], - 'Y': [0, 0.5, 0], - } + high_symmetry_path = [ + {'Gamma': [0, 0, 0]}, + {'X': [0.5, 0, 0]}, + {'Y': [0, 0.5, 0]}, + {'Gamma': [0, 0, 0]}, + ] """, ) @@ -409,20 +505,27 @@ class KLinePath(ArchiveSection): """, ) - def get_high_symmetry_points_norm( + def get_high_symmetry_path_norm( self, reciprocal_lattice_vectors: Optional[pint.Quantity], - ) -> Optional[dict]: + ) -> Optional[List[Dict[str, pint.Quantity]]]: """ - Get the high symmetry points norms from the dictionary of vectors in units of the `reciprocal_lattice_vectors`. This - function is useful when matching lists of points passed as norms to the high symmetry points in order to resolve - `KLinePath.points` + Get the high symmetry path points norms from the list of dictionaries of vectors in units of the `reciprocal_lattice_vectors`. + The norms are accummulated, such that the first high symmetry point in the path list has a norm of 0, while the others sum the + previous norm. This function is useful when matching lists of points passed as norms to the high symmetry path in order to + resolve `KLinePath.points`. Args: reciprocal_lattice_vectors (Optional[np.ndarray]): The reciprocal lattice vectors of the atomic cell. Returns: - (Optional[dict]): The high symmetry points norms. + (Optional[List[Dict[str, pint.Quantity]]]): The high symmetry points norms list of dictionaries, e.g. in a cubic lattice: + high_symmetry_path = [ + {'Gamma': 0}, + {'X': 0.5}, + {'Y': 0.5 + 1 / np.sqrt(2)}, + {'Gamma': 1 + 1 / np.sqrt(2)}, + ] """ # Checking if `reciprocal_lattice_vectors` is defined and taking its magnitude to operate if reciprocal_lattice_vectors is None: @@ -430,28 +533,32 @@ def get_high_symmetry_points_norm( rlv = reciprocal_lattice_vectors.magnitude # initializing the norms dictionary - high_symmetry_points_norms = { - key: 0.0 * reciprocal_lattice_vectors.u - for key in self.high_symmetry_path.keys() - } + high_symmetry_path_norms = [ + {key: 0.0 * reciprocal_lattice_vectors.u} + for point in self.high_symmetry_path + for key in point.keys() + ] # initializing the first point prev_value_norm = 0.0 * reciprocal_lattice_vectors.u prev_value_rlv = np.array([0, 0, 0]) - for i, (key, value) in enumerate(self.high_symmetry_path.items()): + for i, point in enumerate(self.high_symmetry_path): if i == 0: continue + [(key, value)] = point.items() value_rlv = value @ rlv value_tot_rlv = value_rlv - prev_value_rlv value_norm = ( np.linalg.norm(value_tot_rlv) * reciprocal_lattice_vectors.u + prev_value_norm ) - high_symmetry_points_norms[key] = value_norm + + # store in new path norms variable + high_symmetry_path_norms[i][key] = value_norm # accumulate value vector and norm prev_value_rlv = value_rlv prev_value_norm = value_norm - return high_symmetry_points_norms + return high_symmetry_path_norms def resolve_points( self, @@ -461,7 +568,7 @@ def resolve_points( ) -> None: """ Resolves the `points` of the `KLinePath` from the `points_norm` and the `reciprocal_lattice_vectors`. This is useful - when a list of points norms and the dictionary of high symmetry points are passed to resolve the `KLinePath.points`. + when a list of points norms and the list of dictionaries of the high symmetry path are passed to resolve the `KLinePath.points`. Args: points_norm (List[float]): List of points norms in the k-line path. @@ -489,36 +596,38 @@ def resolve_points( self.n_line_points = len(points_norm) # Calculate the total norm of the path in order to find the closest indices in the list of `points_norm` - high_symmetry_points_norms = self.get_high_symmetry_points_norm( + high_symmetry_path_norms = self.get_high_symmetry_path_norm( reciprocal_lattice_vectors ) - closest_indices = {} - for key, norm in high_symmetry_points_norms.items(): + closest_indices = [] + for i, point in enumerate(high_symmetry_path_norms): + [norm] = point.values() closest_idx = (np.abs(points_norm - norm.magnitude)).argmin() - closest_indices[key] = closest_idx + closest_indices.append(closest_idx) # Append the data in the new `points` in units of the `reciprocal_lattice_vectors` points = [] - for i, (key, value) in enumerate(self.high_symmetry_path.items()): + for i, point in enumerate(self.high_symmetry_path): + [value] = point.values() if i == 0: prev_value = value - prev_index = closest_indices[key] + prev_index = closest_indices[i] continue elif i == len(self.high_symmetry_path) - 1: points.append( np.linspace( - prev_value, value, num=closest_indices[key] - prev_index + 1 + prev_value, value, num=closest_indices[i] - prev_index + 1 ) ) else: # pop the last element as it appears repeated in the next segment points.append( np.linspace( - prev_value, value, num=closest_indices[key] - prev_index + 1 + prev_value, value, num=closest_indices[i] - prev_index + 1 )[:-1] ) prev_value = value - prev_index = closest_indices[key] + prev_index = closest_indices[i] new_points = list(itertools.chain(*points)) # And store this information in the `points` quantity if self.points is not None: @@ -571,16 +680,16 @@ def resolve_reciprocal_lattice_vectors( for model_system in model_systems: # General checks to proceed with normalization if is_not_representative(model_system, logger): - return None + continue # TODO extend this for other dimensions (@ndaelman-hu) if model_system.type != 'bulk': logger.warning('`ModelSystem.type` is not describing a bulk system.') - return None + continue atomic_cell = model_system.cell if atomic_cell is None: logger.warning('`ModelSystem.cell` was not found.') - return None + continue # Set the `reciprocal_lattice_vectors` using ASE ase_atoms = atomic_cell[0].to_ase_atoms(logger) diff --git a/tests/conftest.py b/tests/conftest.py index e2c3c2ac..2490011d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,7 +19,7 @@ import os import numpy as np import pytest -from typing import List, Optional +from typing import List, Optional, Dict from nomad.units import ureg from nomad.datamodel import EntryArchive @@ -83,6 +83,7 @@ def generate_model_system( chemical_symbols: List[str] = ['Ga', 'As'], orbitals_symbols: List[List[str]] = [['s'], ['px', 'py']], is_representative: bool = True, + pbc: List[bool] = [False, False, False], ) -> Optional[ModelSystem]: """ Generate a `ModelSystem` section with the given parameters. @@ -95,6 +96,7 @@ def generate_model_system( type=type, positions=positions * ureg.angstrom, lattice_vectors=lattice_vectors * ureg.angstrom, + periodic_boundary_conditions=pbc, ) model_system.cell.append(atomic_cell) @@ -222,12 +224,12 @@ def generate_simulation_electronic_dos( def generate_k_line_path( - high_symmetry_path: dict = { - 'Gamma1': [0, 0, 0], - 'X': [0.5, 0, 0], - 'Y': [0, 0.5, 0], - 'Gamma2': [0, 0, 0], - }, # ! fix the patch on naming 'Gamma1' and 'Gamma2' + high_symmetry_path: List[Dict[str, List[float]]] = [ + {'Gamma': [0, 0, 0]}, + {'X': [0.5, 0, 0]}, + {'Y': [0, 0.5, 0]}, + {'Gamma': [0, 0, 0]}, + ], ) -> KLinePathSettings: return KLinePathSettings(high_symmetry_path=high_symmetry_path) @@ -235,21 +237,32 @@ def generate_k_line_path( def generate_k_space_simulation( system_type: str = 'bulk', is_representative: bool = True, + positions: List[List[float]] = [[0, 0, 0], [0.5, 0.5, 0.5]], + lattice_vectors: List[List[float]] = [[1, 0, 0], [0, 1, 0], [0, 0, 1]], + chemical_symbols: List[str] = ['Ga', 'As'], + orbitals_symbols: List[List[str]] = [['s'], ['px', 'py']], + pbc: List[bool] = [False, False, False], reciprocal_lattice_vectors: Optional[List[List[float]]] = [ [1, 0, 0], [0, 1, 0], [0, 0, 1], ], - high_symmetry_path: dict = { - 'Gamma1': [0, 0, 0], - 'X': [0.5, 0, 0], - 'Y': [0, 0.5, 0], - 'Gamma2': [0, 0, 0], - }, + high_symmetry_path: List[Dict[str, List[float]]] = [ + {'Gamma': [0, 0, 0]}, + {'X': [0.5, 0, 0]}, + {'Y': [0, 0.5, 0]}, + {'Gamma': [0, 0, 0]}, + ], grid=[6, 6, 6], ) -> Simulation: model_system = generate_model_system( - system_type=system_type, is_representative=is_representative + system_type=system_type, + is_representative=is_representative, + positions=positions, + lattice_vectors=lattice_vectors, + chemical_symbols=chemical_symbols, + orbitals_symbols=orbitals_symbols, + pbc=pbc, ) k_space = KSpace() # adding `reciprocal_lattice_vectors` diff --git a/tests/test_numerical_settings.py b/tests/test_numerical_settings.py index 5fdab0c7..c0693152 100644 --- a/tests/test_numerical_settings.py +++ b/tests/test_numerical_settings.py @@ -18,7 +18,7 @@ import pytest import numpy as np -from typing import Optional, List +from typing import Optional, List, Dict from nomad.units import ureg from nomad.datamodel import EntryArchive @@ -231,44 +231,62 @@ def test_resolve_k_line_density( else: assert k_line_density == result_k_line_density + def test_resolve_high_symmetry_points(self): + """ + Test the `resolve_high_symmetry_points` method. Only testing the valid situation in which the `ModelSystem` normalization worked. + """ + # `ModelSystem.normalize()` need to extract `bulk` as a type. + simulation = generate_k_space_simulation( + pbc=[True, True, True], + ) + model_system = simulation.model_system[0] + model_system.normalize(EntryArchive(), logger) # normalize to extract symmetry + k_mesh = simulation.model_method[0].numerical_settings[0].k_mesh[0] + high_symmetry_points = k_mesh.resolve_high_symmetry_points( + simulation.model_system, logger + ) + assert len(high_symmetry_points) == 4 + assert high_symmetry_points == { + 'Gamma': [0, 0, 0], + 'M': [0.5, 0.5, 0], + 'R': [0.5, 0.5, 0.5], + 'X': [0, 0.5, 0], + } + class TestKLinePath: """ Test the `KLinePath` class defined in `numerical_settings.py`. """ - def test_get_high_symmetry_points_norm(self, k_line_path: KLinePath): + def test_get_high_symmetry_path_norm(self, k_line_path: KLinePath): """ - Test the `get_high_symmetry_points_norm` method. + Test the `get_high_symmetry_path_norm` method. """ rlv = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) * ureg('1/meter') - high_symmetry_points_norms = k_line_path.get_high_symmetry_points_norm( + high_symmetry_path_norms = k_line_path.get_high_symmetry_path_norm( reciprocal_lattice_vectors=rlv ) - hs_points = { - 'Gamma1': 0, - 'X': 0.5, - 'Y': 0.5 + 1 / np.sqrt(2), - 'Gamma2': 1 + 1 / np.sqrt(2), - } - for key, val in hs_points.items(): - assert np.isclose(high_symmetry_points_norms[key].magnitude, val) + hs_points: List[Dict[str, float]] = [ + {'Gamma': 0}, + {'X': 0.5}, + {'Y': 0.5 + 1 / np.sqrt(2)}, + {'Gamma': 1 + 1 / np.sqrt(2)}, + ] + for i, point in enumerate(hs_points): + [(key, val)] = point.items() + assert np.isclose(high_symmetry_path_norms[i][key].magnitude, val) def test_resolve_points(self, k_line_path: KLinePath): """ Test the `resolve_points` method. """ rlv = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) * ureg('1/meter') - hs_points = { - 'Gamma1': 0, - 'X': 0.5, - 'Y': 0.5 + 1 / np.sqrt(2), - 'Gamma2': 1 + 1 / np.sqrt(2), - } + hs_points = [0, 0.5, 0.5 + 1 / np.sqrt(2), 1 + 1 / np.sqrt(2)] # Define paths - gamma_x = np.linspace(hs_points['Gamma1'], hs_points['X'], num=5) - x_y = np.linspace(hs_points['X'], hs_points['Y'], num=5) - y_gamma = np.linspace(hs_points['Y'], hs_points['Gamma2'], num=5) + gamma_x = np.linspace(hs_points[0], hs_points[1], num=5) + x_y = np.linspace(hs_points[1], hs_points[2], num=5) + y_gamma = np.linspace(hs_points[2], hs_points[3], num=5) points_norm = np.concatenate((gamma_x, x_y, y_gamma)) k_line_path.resolve_points( points_norm=points_norm, reciprocal_lattice_vectors=rlv, logger=logger From efd3b13d02f8bb356d226ca06c666ce198b34d28 Mon Sep 17 00:00:00 2001 From: JosePizarro3 Date: Wed, 22 May 2024 11:53:34 +0200 Subject: [PATCH 08/16] Changed high_symmetry_path to 2 quantities and fixed tests --- src/nomad_simulations/numerical_settings.py | 117 +++++++++++++------- tests/conftest.py | 16 ++- tests/test_numerical_settings.py | 16 +-- 3 files changed, 93 insertions(+), 56 deletions(-) diff --git a/src/nomad_simulations/numerical_settings.py b/src/nomad_simulations/numerical_settings.py index a9173a6e..fd40f8d9 100644 --- a/src/nomad_simulations/numerical_settings.py +++ b/src/nomad_simulations/numerical_settings.py @@ -262,7 +262,7 @@ def resolve_points_and_offset( points = None offset = None - if self.center == 'Gamma-centered': + if self.center == 'Gamma-centered': # ! fix this (@ndaelman-hu) grid_space = [np.linspace(0, 1, n) for n in self.grid] points = np.meshgrid(grid_space) offset = np.array([0, 0, 0]) @@ -475,18 +475,36 @@ class KLinePath(ArchiveSection): value, one should multiply them by the reciprocal lattice vectors (`points_cartesian = points @ reciprocal_lattice_vectors`). """ - high_symmetry_path = Quantity( - type=JSON, + # high_symmetry_path = Quantity( + # type=JSON, + # shape=['*'], + # description=""" + # List of dictionaries containing the high-symmetry path (in units of the `reciprocal_lattice_vectors`) followed in + # the k-line path. E.g., in a cubic lattice: + # high_symmetry_path = [ + # {'Gamma': [0, 0, 0]}, + # {'X': [0.5, 0, 0]}, + # {'Y': [0, 0.5, 0]}, + # {'Gamma': [0, 0, 0]}, + # ] + # """, + # ) + + high_symmetry_path_names = Quantity( + type=str, shape=['*'], description=""" - List of dictionaries containing the high-symmetry path (in units of the `reciprocal_lattice_vectors`) followed in - the k-line path. E.g., in a cubic lattice: - high_symmetry_path = [ - {'Gamma': [0, 0, 0]}, - {'X': [0.5, 0, 0]}, - {'Y': [0, 0.5, 0]}, - {'Gamma': [0, 0, 0]}, - ] + List of the high-symmetry path names followed in the k-line path. This quantity is directly coupled with `high_symmetry_path_value`. + E.g., in a cubic lattice: `high_symmetry_path_names = ['Gamma', 'X', 'Y', 'Gamma']`. + """, + ) + + high_symmetry_path_values = Quantity( + type=np.float64, + shape=['*', 3], + description=""" + List of the high-symmetry path values in units of the `reciprocal_lattice_vectors` in the k-line path. This quantity is directly + coupled with `high_symmetry_path_names`. E.g., in a cubic lattice: `high_symmetry_path_value = [[0, 0, 0], [0.5, 0, 0], [0, 0.5, 0], [0, 0, 0]]`. """, ) @@ -505,10 +523,36 @@ class KLinePath(ArchiveSection): """, ) - def get_high_symmetry_path_norm( + def _check_high_symmetry_path(self, logger: BoundLogger) -> bool: + """ + Check if the `high_symmetry_path_names` and `high_symmetry_path_values` are defined and have the same length. + + Args: + logger (BoundLogger): The logger to log messages. + + Returns: + (bool): True if the `high_symmetry_path_names` and `high_symmetry_path_values` are defined and have the same length, False otherwise. + """ + if ( + self.high_symmetry_path_names is None + or self.high_symmetry_path_values is None + ): + logger.warning( + 'Could not find `KLinePath.high_symmetry_path_names` or `KLinePath.high_symmetry_path_values`.' + ) + return False + if len(self.high_symmetry_path_names) != len(self.high_symmetry_path_values): + logger.warning( + 'The length of `KLinePath.high_symmetry_path_names` and `KLinePath.high_symmetry_path_values` should coincide.' + ) + return False + return True + + def get_high_symmetry_path_norms( self, reciprocal_lattice_vectors: Optional[pint.Quantity], - ) -> Optional[List[Dict[str, pint.Quantity]]]: + logger: BoundLogger, + ) -> Optional[List[pint.Quantity]]: """ Get the high symmetry path points norms from the list of dictionaries of vectors in units of the `reciprocal_lattice_vectors`. The norms are accummulated, such that the first high symmetry point in the path list has a norm of 0, while the others sum the @@ -517,34 +561,28 @@ def get_high_symmetry_path_norm( Args: reciprocal_lattice_vectors (Optional[np.ndarray]): The reciprocal lattice vectors of the atomic cell. + logger (BoundLogger): The logger to log messages. Returns: - (Optional[List[Dict[str, pint.Quantity]]]): The high symmetry points norms list of dictionaries, e.g. in a cubic lattice: - high_symmetry_path = [ - {'Gamma': 0}, - {'X': 0.5}, - {'Y': 0.5 + 1 / np.sqrt(2)}, - {'Gamma': 1 + 1 / np.sqrt(2)}, - ] + (Optional[List[pint.Quantity]]): The high symmetry points norms list, e.g. in a cubic lattice: + `high_symmetry_path_value_norms = [0, 0.5, 0.5 + 1 / np.sqrt(2), 1 + 1 / np.sqrt(2)]` """ + # Checking the high symmetry path quantities + if not self._check_high_symmetry_path(logger): + return None # Checking if `reciprocal_lattice_vectors` is defined and taking its magnitude to operate if reciprocal_lattice_vectors is None: return None rlv = reciprocal_lattice_vectors.magnitude - # initializing the norms dictionary - high_symmetry_path_norms = [ - {key: 0.0 * reciprocal_lattice_vectors.u} - for point in self.high_symmetry_path - for key in point.keys() - ] + # initializing the norms list (the first point has a norm of 0) + high_symmetry_path_value_norms = [0.0 * reciprocal_lattice_vectors.u] # initializing the first point prev_value_norm = 0.0 * reciprocal_lattice_vectors.u prev_value_rlv = np.array([0, 0, 0]) - for i, point in enumerate(self.high_symmetry_path): + for i, value in enumerate(self.high_symmetry_path_values): if i == 0: continue - [(key, value)] = point.items() value_rlv = value @ rlv value_tot_rlv = value_rlv - prev_value_rlv value_norm = ( @@ -553,12 +591,12 @@ def get_high_symmetry_path_norm( ) # store in new path norms variable - high_symmetry_path_norms[i][key] = value_norm + high_symmetry_path_value_norms.append(value_norm) # accumulate value vector and norm prev_value_rlv = value_rlv prev_value_norm = value_norm - return high_symmetry_path_norms + return high_symmetry_path_value_norms def resolve_points( self, @@ -576,8 +614,7 @@ def resolve_points( logger (BoundLogger): The logger to log messages. """ # General checks for quantities - if self.high_symmetry_path is None: - logger.warning('Could not resolve `KLinePath.high_symmetry_path`.') + if not self._check_high_symmetry_path(logger): return None if reciprocal_lattice_vectors is None: logger.warning( @@ -596,24 +633,22 @@ def resolve_points( self.n_line_points = len(points_norm) # Calculate the total norm of the path in order to find the closest indices in the list of `points_norm` - high_symmetry_path_norms = self.get_high_symmetry_path_norm( - reciprocal_lattice_vectors + high_symmetry_path_value_norms = self.get_high_symmetry_path_norms( + reciprocal_lattice_vectors, logger ) closest_indices = [] - for i, point in enumerate(high_symmetry_path_norms): - [norm] = point.values() + for i, norm in enumerate(high_symmetry_path_value_norms): closest_idx = (np.abs(points_norm - norm.magnitude)).argmin() closest_indices.append(closest_idx) # Append the data in the new `points` in units of the `reciprocal_lattice_vectors` points = [] - for i, point in enumerate(self.high_symmetry_path): - [value] = point.values() + for i, value in enumerate(self.high_symmetry_path_values): if i == 0: prev_value = value prev_index = closest_indices[i] continue - elif i == len(self.high_symmetry_path) - 1: + elif i == len(self.high_symmetry_path_values) - 1: points.append( np.linspace( prev_value, value, num=closest_indices[i] - prev_index + 1 @@ -637,6 +672,10 @@ def resolve_points( def normalize(self, archive, logger) -> None: super().normalize(archive, logger) + # If `high_symmetry_path` is not defined, we do not normalize the KLinePath + if not self._check_high_symmetry_path(logger): + return + class KSpace(NumericalSettings): """ diff --git a/tests/conftest.py b/tests/conftest.py index 2490011d..d82e9c5f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -224,14 +224,18 @@ def generate_simulation_electronic_dos( def generate_k_line_path( - high_symmetry_path: List[Dict[str, List[float]]] = [ - {'Gamma': [0, 0, 0]}, - {'X': [0.5, 0, 0]}, - {'Y': [0, 0.5, 0]}, - {'Gamma': [0, 0, 0]}, + high_symmetry_path_names: List[str] = ['Gamma', 'X', 'Y', 'Gamma'], + high_symmetry_path_values: List[List[float]] = [ + [0, 0, 0], + [0.5, 0, 0], + [0, 0.5, 0], + [0, 0, 0], ], ) -> KLinePathSettings: - return KLinePathSettings(high_symmetry_path=high_symmetry_path) + return KLinePathSettings( + high_symmetry_path_names=high_symmetry_path_names, + high_symmetry_path_values=high_symmetry_path_values, + ) def generate_k_space_simulation( diff --git a/tests/test_numerical_settings.py b/tests/test_numerical_settings.py index c0693152..d1ac2208 100644 --- a/tests/test_numerical_settings.py +++ b/tests/test_numerical_settings.py @@ -264,18 +264,12 @@ def test_get_high_symmetry_path_norm(self, k_line_path: KLinePath): Test the `get_high_symmetry_path_norm` method. """ rlv = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) * ureg('1/meter') - high_symmetry_path_norms = k_line_path.get_high_symmetry_path_norm( - reciprocal_lattice_vectors=rlv + high_symmetry_path_norms = k_line_path.get_high_symmetry_path_norms( + reciprocal_lattice_vectors=rlv, logger=logger ) - hs_points: List[Dict[str, float]] = [ - {'Gamma': 0}, - {'X': 0.5}, - {'Y': 0.5 + 1 / np.sqrt(2)}, - {'Gamma': 1 + 1 / np.sqrt(2)}, - ] - for i, point in enumerate(hs_points): - [(key, val)] = point.items() - assert np.isclose(high_symmetry_path_norms[i][key].magnitude, val) + hs_points = [0, 0.5, 0.5 + 1 / np.sqrt(2), 1 + 1 / np.sqrt(2)] + for i, val in enumerate(hs_points): + assert np.isclose(high_symmetry_path_norms[i].magnitude, val) def test_resolve_points(self, k_line_path: KLinePath): """ From a650d88df8d66b2c98da3fbcf42b5e2d61086f2b Mon Sep 17 00:00:00 2001 From: JosePizarro3 Date: Wed, 22 May 2024 12:18:09 +0200 Subject: [PATCH 09/16] Added functional approach for get_high_symmetry_path_norms --- src/nomad_simulations/numerical_settings.py | 64 +++++++++++++++------ tests/conftest.py | 16 ++++-- 2 files changed, 55 insertions(+), 25 deletions(-) diff --git a/src/nomad_simulations/numerical_settings.py b/src/nomad_simulations/numerical_settings.py index fd40f8d9..b14e1e24 100644 --- a/src/nomad_simulations/numerical_settings.py +++ b/src/nomad_simulations/numerical_settings.py @@ -575,28 +575,54 @@ def get_high_symmetry_path_norms( return None rlv = reciprocal_lattice_vectors.magnitude - # initializing the norms list (the first point has a norm of 0) - high_symmetry_path_value_norms = [0.0 * reciprocal_lattice_vectors.u] - # initializing the first point - prev_value_norm = 0.0 * reciprocal_lattice_vectors.u - prev_value_rlv = np.array([0, 0, 0]) - for i, value in enumerate(self.high_symmetry_path_values): - if i == 0: - continue - value_rlv = value @ rlv + def calc_norms(value_rlv, prev_value_rlv): value_tot_rlv = value_rlv - prev_value_rlv - value_norm = ( - np.linalg.norm(value_tot_rlv) * reciprocal_lattice_vectors.u - + prev_value_norm - ) + return np.linalg.norm(value_tot_rlv) * reciprocal_lattice_vectors.u - # store in new path norms variable - high_symmetry_path_value_norms.append(value_norm) + from itertools import accumulate, tee - # accumulate value vector and norm - prev_value_rlv = value_rlv - prev_value_norm = value_norm - return high_symmetry_path_value_norms + # Compute `rlv` projections + rlv_projections = list( + map(lambda value: value @ rlv, self.high_symmetry_path_values) + ) + + # Create two iterators for the projections + rlv_projections_1, rlv_projections_2 = tee(rlv_projections) + + # Initialize the previous value iterators and skip the first element in the second iterator + prev_value_rlv = np.array([0, 0, 0]) + next(rlv_projections_2, None) + + # Calculate the norms using accumulate + norms = accumulate( + zip(rlv_projections_2, rlv_projections_1), + lambda acc, value_pair: calc_norms(value_pair[0], value_pair[1]) + acc, + initial=0.0 * reciprocal_lattice_vectors.u, + ) + return list(norms) + + # # initializing the norms list (the first point has a norm of 0) + # high_symmetry_path_value_norms = [0.0 * reciprocal_lattice_vectors.u] + # # initializing the first point + # prev_value_norm = 0.0 * reciprocal_lattice_vectors.u + # prev_value_rlv = np.array([0, 0, 0]) + # for i, value in enumerate(self.high_symmetry_path_values): + # if i == 0: + # continue + # value_rlv = value @ rlv + # value_tot_rlv = value_rlv - prev_value_rlv + # value_norm = ( + # np.linalg.norm(value_tot_rlv) * reciprocal_lattice_vectors.u + # + prev_value_norm + # ) + + # # store in new path norms variable + # high_symmetry_path_value_norms.append(value_norm) + + # # accumulate value vector and norm + # prev_value_rlv = value_rlv + # prev_value_norm = value_norm + # return high_symmetry_path_value_norms def resolve_points( self, diff --git a/tests/conftest.py b/tests/conftest.py index d82e9c5f..39107094 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -251,11 +251,12 @@ def generate_k_space_simulation( [0, 1, 0], [0, 0, 1], ], - high_symmetry_path: List[Dict[str, List[float]]] = [ - {'Gamma': [0, 0, 0]}, - {'X': [0.5, 0, 0]}, - {'Y': [0, 0.5, 0]}, - {'Gamma': [0, 0, 0]}, + high_symmetry_path_names: List[str] = ['Gamma', 'X', 'Y', 'Gamma'], + high_symmetry_path_values: List[List[float]] = [ + [0, 0, 0], + [0.5, 0, 0], + [0, 0.5, 0], + [0, 0, 0], ], grid=[6, 6, 6], ) -> Simulation: @@ -278,7 +279,10 @@ def generate_k_space_simulation( k_mesh = KMeshSettings(grid=grid) k_space.k_mesh.append(k_mesh) # adding `KLinePathSettings` - k_line_path = KLinePathSettings(high_symmetry_path=high_symmetry_path) + k_line_path = KLinePathSettings( + high_symmetry_path_names=high_symmetry_path_names, + high_symmetry_path_values=high_symmetry_path_values, + ) k_space.k_line_path = k_line_path # appending `KSpace` to `ModelMethod.numerical_settings` model_method = ModelMethod() From a9bdac23e69f2bfbc5714dc4b7c130e5009e19a4 Mon Sep 17 00:00:00 2001 From: JosePizarro3 Date: Wed, 22 May 2024 13:12:07 +0200 Subject: [PATCH 10/16] Added functional programming for KLinePath.resolve_points Added testing for check_high_symmetry_path --- src/nomad_simulations/numerical_settings.py | 102 +++++++++----------- tests/test_numerical_settings.py | 27 +++++- 2 files changed, 71 insertions(+), 58 deletions(-) diff --git a/src/nomad_simulations/numerical_settings.py b/src/nomad_simulations/numerical_settings.py index b14e1e24..224f7694 100644 --- a/src/nomad_simulations/numerical_settings.py +++ b/src/nomad_simulations/numerical_settings.py @@ -18,7 +18,7 @@ import numpy as np import pint -import itertools +from itertools import accumulate, tee, chain from structlog.stdlib import BoundLogger from typing import Optional, List, Tuple, Union, Dict from ase.dft.kpoints import monkhorst_pack, get_monkhorst_pack_size_and_offset @@ -536,6 +536,9 @@ def _check_high_symmetry_path(self, logger: BoundLogger) -> bool: if ( self.high_symmetry_path_names is None or self.high_symmetry_path_values is None + ) or ( + len(self.high_symmetry_path_names) == 0 + or len(self.high_symmetry_path_values) == 0 ): logger.warning( 'Could not find `KLinePath.high_symmetry_path_names` or `KLinePath.high_symmetry_path_values`.' @@ -575,12 +578,12 @@ def get_high_symmetry_path_norms( return None rlv = reciprocal_lattice_vectors.magnitude - def calc_norms(value_rlv, prev_value_rlv): + def calc_norms( + value_rlv: np.ndarray, prev_value_rlv: np.ndarray + ) -> pint.Quantity: value_tot_rlv = value_rlv - prev_value_rlv return np.linalg.norm(value_tot_rlv) * reciprocal_lattice_vectors.u - from itertools import accumulate, tee - # Compute `rlv` projections rlv_projections = list( map(lambda value: value @ rlv, self.high_symmetry_path_values) @@ -589,8 +592,7 @@ def calc_norms(value_rlv, prev_value_rlv): # Create two iterators for the projections rlv_projections_1, rlv_projections_2 = tee(rlv_projections) - # Initialize the previous value iterators and skip the first element in the second iterator - prev_value_rlv = np.array([0, 0, 0]) + # Skip the first element in the second iterator next(rlv_projections_2, None) # Calculate the norms using accumulate @@ -601,29 +603,6 @@ def calc_norms(value_rlv, prev_value_rlv): ) return list(norms) - # # initializing the norms list (the first point has a norm of 0) - # high_symmetry_path_value_norms = [0.0 * reciprocal_lattice_vectors.u] - # # initializing the first point - # prev_value_norm = 0.0 * reciprocal_lattice_vectors.u - # prev_value_rlv = np.array([0, 0, 0]) - # for i, value in enumerate(self.high_symmetry_path_values): - # if i == 0: - # continue - # value_rlv = value @ rlv - # value_tot_rlv = value_rlv - prev_value_rlv - # value_norm = ( - # np.linalg.norm(value_tot_rlv) * reciprocal_lattice_vectors.u - # + prev_value_norm - # ) - - # # store in new path norms variable - # high_symmetry_path_value_norms.append(value_norm) - - # # accumulate value vector and norm - # prev_value_rlv = value_rlv - # prev_value_norm = value_norm - # return high_symmetry_path_value_norms - def resolve_points( self, points_norm: Union[np.ndarray, List[float]], @@ -647,6 +626,7 @@ def resolve_points( 'The `reciprocal_lattice_vectors` are not passed as an input.' ) return None + # Check if `points_norm` is a list and convert it to a numpy array if isinstance(points_norm, list): points_norm = np.array(points_norm) @@ -658,38 +638,46 @@ def resolve_points( ) self.n_line_points = len(points_norm) - # Calculate the total norm of the path in order to find the closest indices in the list of `points_norm` + # Calculate the norms in the path and find the closest indices in points_norm to the high symmetry path norms high_symmetry_path_value_norms = self.get_high_symmetry_path_norms( reciprocal_lattice_vectors, logger ) - closest_indices = [] - for i, norm in enumerate(high_symmetry_path_value_norms): - closest_idx = (np.abs(points_norm - norm.magnitude)).argmin() - closest_indices.append(closest_idx) - - # Append the data in the new `points` in units of the `reciprocal_lattice_vectors` - points = [] - for i, value in enumerate(self.high_symmetry_path_values): - if i == 0: - prev_value = value - prev_index = closest_indices[i] - continue - elif i == len(self.high_symmetry_path_values) - 1: - points.append( - np.linspace( - prev_value, value, num=closest_indices[i] - prev_index + 1 - ) - ) - else: - # pop the last element as it appears repeated in the next segment - points.append( - np.linspace( - prev_value, value, num=closest_indices[i] - prev_index + 1 - )[:-1] + closest_indices = list( + map( + lambda norm: (np.abs(points_norm - norm.magnitude)).argmin(), + high_symmetry_path_value_norms, + ) + ) + + def linspace_segments( + prev_value: np.ndarray, value: np.ndarray, num: int + ) -> np.ndarray: + return np.linspace(prev_value, value, num=num + 1)[:-1] + + # Generate point segments using `map` and `linspace_segments` + points_segments = list( + map( + lambda i, value: linspace_segments( + self.high_symmetry_path_values[i - 1], + value, + closest_indices[i] - closest_indices[i - 1], ) - prev_value = value - prev_index = closest_indices[i] - new_points = list(itertools.chain(*points)) + if i > 0 + else np.array([]), + range(len(self.high_symmetry_path_values)), + self.high_symmetry_path_values, + ) + ) + # and handle the last segment to include all points + points_segments[-1] = np.linspace( + self.high_symmetry_path_values[-2], + self.high_symmetry_path_values[-1], + num=closest_indices[-1] - closest_indices[-2] + 1, + ) + + # Flatten the list of segments into a single list of points + new_points = list(chain.from_iterable(points_segments)) + # And store this information in the `points` quantity if self.points is not None: logger.info('Overwriting `KLinePath.points` with the resolved points.') diff --git a/tests/test_numerical_settings.py b/tests/test_numerical_settings.py index d1ac2208..fcb4c8ff 100644 --- a/tests/test_numerical_settings.py +++ b/tests/test_numerical_settings.py @@ -26,7 +26,7 @@ from nomad_simulations.numerical_settings import KMesh, KLinePath from . import logger -from .conftest import generate_k_space_simulation +from .conftest import generate_k_line_path, generate_k_space_simulation class TestKSpace: @@ -259,6 +259,31 @@ class TestKLinePath: Test the `KLinePath` class defined in `numerical_settings.py`. """ + @pytest.mark.parametrize( + 'high_symmetry_path_names, high_symmetry_path_values, result', + [ + (None, None, False), + ([], [], False), + (['Gamma', 'X', 'Y'], None, False), + ([], [[0, 0, 0], [0.5, 0, 0], [0, 0.5, 0]], False), + (['Gamma', 'X', 'Y'], [[0, 0, 0], [0.5, 0, 0], [0, 0.5, 0]], True), + ], + ) + def test_check_high_symmetry_path( + self, + high_symmetry_path_names: List[str], + high_symmetry_path_values: List[List[float]], + result: bool, + ): + """ + Test the `_check_high_symmetry_path` private method. + """ + k_line_path = generate_k_line_path( + high_symmetry_path_names=high_symmetry_path_names, + high_symmetry_path_values=high_symmetry_path_values, + ) + assert k_line_path._check_high_symmetry_path(logger) == result + def test_get_high_symmetry_path_norm(self, k_line_path: KLinePath): """ Test the `get_high_symmetry_path_norm` method. From 0113056ed3001d43ae3d226982e714e83d4a5222 Mon Sep 17 00:00:00 2001 From: JosePizarro3 Date: Mon, 27 May 2024 09:55:45 +0200 Subject: [PATCH 11/16] Added abstract class KMeshBase and functionalities in KLinePath --- src/nomad_simulations/numerical_settings.py | 298 ++++++++++++-------- 1 file changed, 180 insertions(+), 118 deletions(-) diff --git a/src/nomad_simulations/numerical_settings.py b/src/nomad_simulations/numerical_settings.py index 224f7694..7f0bc0c7 100644 --- a/src/nomad_simulations/numerical_settings.py +++ b/src/nomad_simulations/numerical_settings.py @@ -160,6 +160,134 @@ def normalize(self, archive, logger) -> None: super().normalize(archive, logger) +class KMeshBase(Mesh): + """ + A base section used for abstraction for `KMesh` and `KLinePath` sections. It contains the methods + `_check_reciprocal_lattice_vectors` and `resolve_high_symmetry_points` that are used in both sections. + """ + + def _check_reciprocal_lattice_vectors( + self, reciprocal_lattice_vectors: Optional[pint.Quantity], logger: BoundLogger + ) -> bool: + """ + Check if the `reciprocal_lattice_vectors` exist and if they have the same dimensionality as `grid`. + + Args: + reciprocal_lattice_vectors (Optional[pint.Quantity]): The reciprocal lattice vectors of the atomic cell. + logger (BoundLogger): The logger to log messages. + + Returns: + (bool): True if the `reciprocal_lattice_vectors` exist and have the same dimensionality as `grid`, False otherwise. + """ + if reciprocal_lattice_vectors is None or self.grid is None: + logger.warning( + 'Could not find `reciprocal_lattice_vectors` from parent `KSpace` or could not find `KMesh.grid`.' + ) + return False + if len(reciprocal_lattice_vectors) != 3 or len(self.grid) != 3: + logger.warning( + 'The `reciprocal_lattice_vectors` and the `grid` should have the same dimensionality.' + ) + return False + return True + + def resolve_high_symmetry_points( + self, + model_systems: List[ModelSystem], + logger: BoundLogger, + eps: float = 3e-3, + ) -> Optional[dict]: + """ + Resolves the `high_symmetry_points` from the list of `ModelSystem`. This method relies on using the `ModelSystem` + information in the sub-sections `Symmetry` and `AtomicCell`, and uses the ASE package to extract the + special (high symmetry) points information. + + Args: + model_systems (List[ModelSystem]): The list of `ModelSystem` sections. + logger (BoundLogger): The logger to log messages. + eps (float, optional): Tolerance factor to define the `lattice` ASE object. Defaults to 3e-3. + + Returns: + (Optional[dict]): The resolved `high_symmetry_points`. + """ + # Extracting `bravais_lattice` from `ModelSystem.symmetry` section and `ASE.cell` from `ModelSystem.cell` + lattice = None + for model_system in model_systems: + # General checks to proceed with normalization + if is_not_representative(model_system, logger): + continue + if model_system.symmetry is None: + logger.warning('Could not find `ModelSystem.symmetry`.') + continue + bravais_lattice = [symm.bravais_lattice for symm in model_system.symmetry] + if len(bravais_lattice) != 1: + logger.warning( + 'Could not uniquely determine `bravais_lattice` from `ModelSystem.symmetry`.' + ) + continue + bravais_lattice = bravais_lattice[0] + + if model_system.cell is None: + logger.warning('Could not find `ModelSystem.cell`.') + continue + prim_atomic_cell = None + for atomic_cell in model_system.cell: + if atomic_cell.type == 'primitive': + prim_atomic_cell = atomic_cell + break + if prim_atomic_cell is None: + logger.warning( + 'Could not find the primitive `AtomicCell` under `ModelSystem.cell`.' + ) + continue + # function defined in AtomicCell + atoms = prim_atomic_cell.to_ase_atoms(logger) + cell = atoms.get_cell() + lattice = cell.get_bravais_lattice(eps) + break # only cover the first representative `ModelSystem` + + # Checking if `bravais_lattice` and `lattice` are defined + if lattice is None: + logger.warning( + 'Could not resolve `bravais_lattice` and `lattice` ASE object from the `ModelSystem`.' + ) + return None + + # Non-conventional ordering testing for certain lattices: + if bravais_lattice in ['oP', 'oF', 'oI', 'oS']: + a, b, c = lattice.a, lattice.b, lattice.c + assert a < b + if bravais_lattice != 'oS': + assert b < c + elif bravais_lattice in ['mP', 'mS']: + a, b, c = lattice.a, lattice.b, lattice.c + alpha = lattice.alpha * np.pi / 180 + assert a <= c and b <= c # ordering of the conventional lattice + assert alpha < np.pi / 2 + + # Extracting the `high_symmetry_points` from the `lattice` object + special_points = lattice.get_special_points() + if special_points is None: + logger.warning( + 'Could not find `lattice.get_special_points()` from the ASE package.' + ) + return None + high_symmetry_points = {} + for key, value in lattice.get_special_points().items(): + if key == 'G': + key = 'Gamma' + if bravais_lattice == 'tI': + if key == 'S': + key = 'Sigma' + elif key == 'S1': + key = 'Sigma1' + high_symmetry_points[key] = list(value) + return high_symmetry_points + + def normalize(self, archive, logger) -> None: + super().normalize(archive, logger) + + class KMesh(Mesh): """ A base section used to specify the settings of a sampling mesh in reciprocal space. The `points` and other @@ -219,31 +347,6 @@ class KMesh(Mesh): # TODO add extraction of `high_symmetry_points` using BandStructureNormalizer idea (left for later when defining outputs.py) - def _check_reciprocal_lattice_vectors( - self, reciprocal_lattice_vectors: Optional[pint.Quantity], logger: BoundLogger - ) -> bool: - """ - Check if the `reciprocal_lattice_vectors` exist and if they have the same dimensionality as `grid`. - - Args: - reciprocal_lattice_vectors (Optional[pint.Quantity]): The reciprocal lattice vectors of the atomic cell. - logger (BoundLogger): The logger to log messages. - - Returns: - (bool): True if the `reciprocal_lattice_vectors` exist and have the same dimensionality as `grid`, False otherwise. - """ - if reciprocal_lattice_vectors is None or self.grid is None: - logger.warning( - 'Could not find `reciprocal_lattice_vectors` from parent `KSpace` or could not find `KMesh.grid`.' - ) - return False - if len(reciprocal_lattice_vectors) != 3 or len(self.grid) != 3: - logger.warning( - 'The `reciprocal_lattice_vectors` and the `grid` should have the same dimensionality.' - ) - return False - return True - def resolve_points_and_offset( self, logger: BoundLogger ) -> Tuple[Optional[List[np.ndarray]], Optional[np.ndarray]]: @@ -344,99 +447,6 @@ def resolve_k_line_density( return k_line_density return None - def resolve_high_symmetry_points( - self, - model_systems: List[ModelSystem], - logger: BoundLogger, - eps: float = 3e-3, - ) -> Optional[dict]: - """ - Resolves the `high_symmetry_points` of the `KMesh` from the list of `ModelSystem`. This method - relies on using the `ModelSystem` information in the sub-sections `Symmetry` and `AtomicCell`, and uses - the ASE package to extract the special (high symmetry) points information. - - Args: - model_systems (List[ModelSystem]): The list of `ModelSystem` sections. - logger (BoundLogger): The logger to log messages. - eps (float, optional): Tolerance factor to define the `lattice` ASE object. Defaults to 3e-3. - - Returns: - (Optional[dict]): The resolved `high_symmetry_points` of the `KMesh`. - """ - # Extracting `bravais_lattice` from `ModelSystem.symmetry` section and `ASE.cell` from `ModelSystem.cell` - lattice = None - for model_system in model_systems: - # General checks to proceed with normalization - if is_not_representative(model_system, logger): - continue - if model_system.symmetry is None: - logger.warning('Could not find `ModelSystem.symmetry`.') - continue - bravais_lattice = [symm.bravais_lattice for symm in model_system.symmetry] - if len(bravais_lattice) != 1: - logger.warning( - 'Could not uniquely determine `bravais_lattice` from `ModelSystem.symmetry`.' - ) - continue - bravais_lattice = bravais_lattice[0] - - if model_system.cell is None: - logger.warning('Could not find `ModelSystem.cell`.') - continue - prim_atomic_cell = None - for atomic_cell in model_system.cell: - if atomic_cell.type == 'primitive': - prim_atomic_cell = atomic_cell - break - if prim_atomic_cell is None: - logger.warning( - 'Could not find the primitive `AtomicCell` under `ModelSystem.cell`.' - ) - continue - # function defined in AtomicCell - atoms = prim_atomic_cell.to_ase_atoms(logger) - cell = atoms.get_cell() - lattice = cell.get_bravais_lattice(eps) - break # only cover the first representative `ModelSystem` - - # Checking if `bravais_lattice` and `lattice` are defined - if lattice is None: - logger.warning( - 'Could not resolve `bravais_lattice` and `lattice` ASE object from the `ModelSystem`.' - ) - return None - - # Non-conventional ordering testing for certain lattices: - if bravais_lattice in ['oP', 'oF', 'oI', 'oS']: - a, b, c = lattice.a, lattice.b, lattice.c - assert a < b - if bravais_lattice != 'oS': - assert b < c - elif bravais_lattice in ['mP', 'mS']: - a, b, c = lattice.a, lattice.b, lattice.c - alpha = lattice.alpha * np.pi / 180 - assert a <= c and b <= c # ordering of the conventional lattice - assert alpha < np.pi / 2 - - # Extracting the `high_symmetry_points` from the `lattice` object - special_points = lattice.get_special_points() - if special_points is None: - logger.warning( - 'Could not find `lattice.get_special_points()` from the ASE package.' - ) - return None - high_symmetry_points = {} - for key, value in lattice.get_special_points().items(): - if key == 'G': - key = 'Gamma' - if bravais_lattice == 'tI': - if key == 'S': - key = 'Sigma' - elif key == 'S1': - key = 'Sigma1' - high_symmetry_points[key] = list(value) - return high_symmetry_points - def normalize(self, archive, logger) -> None: super().normalize(archive, logger) @@ -551,6 +561,43 @@ def _check_high_symmetry_path(self, logger: BoundLogger) -> bool: return False return True + def resolve_high_symmetry_path_values( + self, + model_systems: List[ModelSystem], + reciprocal_lattice_vectors: pint.Quantity, + logger: BoundLogger, + ) -> Optional[List[float]]: + """ + Resolves the `high_symmetry_path_values` of the `KLinePath` from the `high_symmetry_path_names`. + + Args: + model_systems (List[ModelSystem]): The list of `ModelSystem` sections. + reciprocal_lattice_vectors (pint.Quantity): The reciprocal lattice vectors of the atomic cell. + logger (BoundLogger): The logger to log messages. + + Returns: + (Optional[List[float]]): The resolved `high_symmetry_path_values`. + """ + # Initial check on the `reciprocal_lattice_vectors` + if not self._check_reciprocal_lattice_vectors( + reciprocal_lattice_vectors, logger + ): + return [] + + # Resolving the dictionary containing the `high_symmetry_points` for the given ModelSystem symmetry + high_symmetry_points = self.resolve_high_symmetry_points(model_systems, logger) + if high_symmetry_points is None: + return [] + + # Appending into a list which is stored in the `high_symmetry_path_values`. There is a check in the `normalize()` + # function to ensure that the length of the `high_symmetry_path_names` and `high_symmetry_path_values` coincide. + high_symmetry_path_values = [ + high_symmetry_points[name] + for name in self.high_symmetry_path_names + if name in high_symmetry_points.keys() + ] + return high_symmetry_path_values + def get_high_symmetry_path_norms( self, reciprocal_lattice_vectors: Optional[pint.Quantity], @@ -686,6 +733,21 @@ def linspace_segments( def normalize(self, archive, logger) -> None: super().normalize(archive, logger) + # Resolves `high_symmetry_path_values` from `high_symmetry_path_names` + model_systems = self.m_xpath( + 'm_parent.m_parent.m_parent.model_system', dict=False + ) + reciprocal_lattice_vectors = self.m_xpath( + 'm_parent.reciprocal_lattice_vectors', dict=False + ) + if ( + self.high_symmetry_path_values is None + or len(self.high_symmetry_path_values) == 0 + ): + self.high_symmetry_path_values = self.resolve_high_symmetry_path_values( + model_systems, reciprocal_lattice_vectors, logger + ) + # If `high_symmetry_path` is not defined, we do not normalize the KLinePath if not self._check_high_symmetry_path(logger): return From c75baebaa9e909532075ff2763a9787a507e43a2 Mon Sep 17 00:00:00 2001 From: JosePizarro3 Date: Mon, 27 May 2024 10:14:22 +0200 Subject: [PATCH 12/16] Fix KMeshBase and changed name to KSpaceFunctionalities --- src/nomad_simulations/numerical_settings.py | 135 ++++++++++---------- tests/test_numerical_settings.py | 129 +++++++++++++++---- 2 files changed, 174 insertions(+), 90 deletions(-) diff --git a/src/nomad_simulations/numerical_settings.py b/src/nomad_simulations/numerical_settings.py index 7f0bc0c7..de0912e1 100644 --- a/src/nomad_simulations/numerical_settings.py +++ b/src/nomad_simulations/numerical_settings.py @@ -160,14 +160,17 @@ def normalize(self, archive, logger) -> None: super().normalize(archive, logger) -class KMeshBase(Mesh): +class KSpaceFunctionalities: """ - A base section used for abstraction for `KMesh` and `KLinePath` sections. It contains the methods - `_check_reciprocal_lattice_vectors` and `resolve_high_symmetry_points` that are used in both sections. + A functionality class useful for defining methods shared between `KSpace`, `KMesh`, and `KLinePath`. """ def _check_reciprocal_lattice_vectors( - self, reciprocal_lattice_vectors: Optional[pint.Quantity], logger: BoundLogger + self, + reciprocal_lattice_vectors: Optional[pint.Quantity], + logger: BoundLogger, + check_grid: bool = False, + grid: Optional[List[int]] = [], ) -> bool: """ Check if the `reciprocal_lattice_vectors` exist and if they have the same dimensionality as `grid`. @@ -175,16 +178,25 @@ def _check_reciprocal_lattice_vectors( Args: reciprocal_lattice_vectors (Optional[pint.Quantity]): The reciprocal lattice vectors of the atomic cell. logger (BoundLogger): The logger to log messages. + check_grid (bool, optional): Flag to check the `grid` is set to True. Defaults to False. + grid (Optional[List[int]], optional): The grid of the `KMesh`. Defaults to []. Returns: - (bool): True if the `reciprocal_lattice_vectors` exist and have the same dimensionality as `grid`, False otherwise. + (bool): True if the `reciprocal_lattice_vectors` exist. If `check_grid_too` is set to True, it also checks if the + `reciprocal_lattice_vectors` and the `grid` have the same dimensionality. False otherwise. """ - if reciprocal_lattice_vectors is None or self.grid is None: - logger.warning( - 'Could not find `reciprocal_lattice_vectors` from parent `KSpace` or could not find `KMesh.grid`.' - ) + if reciprocal_lattice_vectors is None: + logger.warning('Could not find `reciprocal_lattice_vectors`.') return False - if len(reciprocal_lattice_vectors) != 3 or len(self.grid) != 3: + # Only checking the `reciprocal_lattice_vectors` + if not check_grid: + return True + + # Checking the `grid` too + if grid is None: + logger.warning('Could not find `grid`.') + return False + if len(reciprocal_lattice_vectors) != 3 or len(grid) != 3: logger.warning( 'The `reciprocal_lattice_vectors` and the `grid` should have the same dimensionality.' ) @@ -284,9 +296,6 @@ def resolve_high_symmetry_points( high_symmetry_points[key] = list(value) return high_symmetry_points - def normalize(self, archive, logger) -> None: - super().normalize(archive, logger) - class KMesh(Mesh): """ @@ -395,8 +404,11 @@ def get_k_line_density( (np.float64): The k-line density of the `KMesh`. """ # Initial check - if not self._check_reciprocal_lattice_vectors( - reciprocal_lattice_vectors, logger + if not KSpaceFunctionalities._check_reciprocal_lattice_vectors( + reciprocal_lattice_vectors=reciprocal_lattice_vectors, + logger=logger, + check_grid=True, + grid=self.grid, ): return None @@ -426,8 +438,11 @@ def resolve_k_line_density( (Optional[pint.Quantity]): The resolved `k_line_density` of the `KMesh`. """ # Initial check - if not self._check_reciprocal_lattice_vectors( - reciprocal_lattice_vectors, logger + if not KSpaceFunctionalities._check_reciprocal_lattice_vectors( + reciprocal_lattice_vectors=reciprocal_lattice_vectors, + logger=logger, + check_grid=True, + grid=self.grid, ): return None @@ -473,8 +488,10 @@ def normalize(self, archive, logger) -> None: # Resolve `high_symmetry_points` if self.high_symmetry_points is None: - self.high_symmetry_points = self.resolve_high_symmetry_points( - model_systems, logger + self.high_symmetry_points = ( + KSpaceFunctionalities.resolve_high_symmetry_points( + model_systems=model_systems, logger=logger + ) ) @@ -485,21 +502,6 @@ class KLinePath(ArchiveSection): value, one should multiply them by the reciprocal lattice vectors (`points_cartesian = points @ reciprocal_lattice_vectors`). """ - # high_symmetry_path = Quantity( - # type=JSON, - # shape=['*'], - # description=""" - # List of dictionaries containing the high-symmetry path (in units of the `reciprocal_lattice_vectors`) followed in - # the k-line path. E.g., in a cubic lattice: - # high_symmetry_path = [ - # {'Gamma': [0, 0, 0]}, - # {'X': [0.5, 0, 0]}, - # {'Y': [0, 0.5, 0]}, - # {'Gamma': [0, 0, 0]}, - # ] - # """, - # ) - high_symmetry_path_names = Quantity( type=str, shape=['*'], @@ -533,34 +535,6 @@ class KLinePath(ArchiveSection): """, ) - def _check_high_symmetry_path(self, logger: BoundLogger) -> bool: - """ - Check if the `high_symmetry_path_names` and `high_symmetry_path_values` are defined and have the same length. - - Args: - logger (BoundLogger): The logger to log messages. - - Returns: - (bool): True if the `high_symmetry_path_names` and `high_symmetry_path_values` are defined and have the same length, False otherwise. - """ - if ( - self.high_symmetry_path_names is None - or self.high_symmetry_path_values is None - ) or ( - len(self.high_symmetry_path_names) == 0 - or len(self.high_symmetry_path_values) == 0 - ): - logger.warning( - 'Could not find `KLinePath.high_symmetry_path_names` or `KLinePath.high_symmetry_path_values`.' - ) - return False - if len(self.high_symmetry_path_names) != len(self.high_symmetry_path_values): - logger.warning( - 'The length of `KLinePath.high_symmetry_path_names` and `KLinePath.high_symmetry_path_values` should coincide.' - ) - return False - return True - def resolve_high_symmetry_path_values( self, model_systems: List[ModelSystem], @@ -579,13 +553,15 @@ def resolve_high_symmetry_path_values( (Optional[List[float]]): The resolved `high_symmetry_path_values`. """ # Initial check on the `reciprocal_lattice_vectors` - if not self._check_reciprocal_lattice_vectors( - reciprocal_lattice_vectors, logger + if not KSpaceFunctionalities._check_reciprocal_lattice_vectors( + reciprocal_lattice_vectors=reciprocal_lattice_vectors, logger=logger ): return [] # Resolving the dictionary containing the `high_symmetry_points` for the given ModelSystem symmetry - high_symmetry_points = self.resolve_high_symmetry_points(model_systems, logger) + high_symmetry_points = KSpaceFunctionalities.resolve_high_symmetry_points( + model_systems=model_systems, logger=logger + ) if high_symmetry_points is None: return [] @@ -598,6 +574,34 @@ def resolve_high_symmetry_path_values( ] return high_symmetry_path_values + def _check_high_symmetry_path(self, logger: BoundLogger) -> bool: + """ + Check if the `high_symmetry_path_names` and `high_symmetry_path_values` are defined and have the same length. + + Args: + logger (BoundLogger): The logger to log messages. + + Returns: + (bool): True if the `high_symmetry_path_names` and `high_symmetry_path_values` are defined and have the same length, False otherwise. + """ + if ( + self.high_symmetry_path_names is None + or self.high_symmetry_path_values is None + ) or ( + len(self.high_symmetry_path_names) == 0 + or len(self.high_symmetry_path_values) == 0 + ): + logger.warning( + 'Could not find `KLinePath.high_symmetry_path_names` or `KLinePath.high_symmetry_path_values`.' + ) + return False + if len(self.high_symmetry_path_names) != len(self.high_symmetry_path_values): + logger.warning( + 'The length of `KLinePath.high_symmetry_path_names` and `KLinePath.high_symmetry_path_values` should coincide.' + ) + return False + return True + def get_high_symmetry_path_norms( self, reciprocal_lattice_vectors: Optional[pint.Quantity], @@ -759,6 +763,7 @@ class KSpace(NumericalSettings): depending on the k-space sampling: `k_mesh` or `k_line_path`. """ + # ! This needs to be normalized first in order to extract the `reciprocal_lattice_vectors` from the `ModelSystem.cell` information reciprocal_lattice_vectors = Quantity( type=np.float64, shape=[3, 3], diff --git a/tests/test_numerical_settings.py b/tests/test_numerical_settings.py index fcb4c8ff..4cc7150b 100644 --- a/tests/test_numerical_settings.py +++ b/tests/test_numerical_settings.py @@ -18,12 +18,12 @@ import pytest import numpy as np -from typing import Optional, List, Dict +from typing import Optional, List from nomad.units import ureg from nomad.datamodel import EntryArchive -from nomad_simulations.numerical_settings import KMesh, KLinePath +from nomad_simulations.numerical_settings import KMesh, KLinePath, KSpaceFunctionalities from . import logger from .conftest import generate_k_line_path, generate_k_space_simulation @@ -75,7 +75,64 @@ def test_normalize( assert k_space.reciprocal_lattice_vectors == result -# TODO add testing for KMesh +class TestKSpaceFunctionalities: + """ + Test the `KSpaceFunctionalities` class defined in `numerical_settings.py`. + """ + + @pytest.mark.parametrize( + 'reciprocal_lattice_vectors, check_grid, grid, result', + [ + (None, None, None, False), + ([[1, 0, 0], [0, 1, 0], [0, 0, 1]], False, None, True), + ([[1, 0, 0], [0, 1, 0], [0, 0, 1]], True, None, False), + ([[1, 0, 0], [0, 1, 0], [0, 0, 1]], True, [6, 6, 6, 4], False), + ([[1, 0, 0], [0, 1, 0], [0, 0, 1]], True, [6, 6, 6], True), + ], + ) + def test_check_reciprocal_lattice_vectors( + self, + reciprocal_lattice_vectors: Optional[List[List[float]]], + check_grid: bool, + grid: Optional[List[int]], + result: bool, + ): + """ + Test the `_check_reciprocal_lattice_vectors` private method. + """ + check = KSpaceFunctionalities._check_reciprocal_lattice_vectors( + reciprocal_lattice_vectors=reciprocal_lattice_vectors, + logger=logger, + check_grd=check_grid, + grid=grid, + ) + assert check == result + + def test_resolve_high_symmetry_points(self): + """ + Test the `resolve_high_symmetry_points` method. Only testing the valid situation in which the `ModelSystem` normalization worked. + """ + # `ModelSystem.normalize()` need to extract `bulk` as a type. + simulation = generate_k_space_simulation( + pbc=[True, True, True], + ) + model_systems = simulation.model_system + # normalize to extract symmetry + simulation.model_system[0].normalize(EntryArchive(), logger) + + # Testing the functionality method + high_symmetry_points = KSpaceFunctionalities.resolve_high_symmetry_points( + model_systems=model_systems, logger=logger + ) + assert len(high_symmetry_points) == 4 + assert high_symmetry_points == { + 'Gamma': [0, 0, 0], + 'M': [0.5, 0.5, 0], + 'R': [0.5, 0.5, 0.5], + 'X': [0, 0.5, 0], + } + + class TestKMesh: """ Test the `KMesh` class defined in `numerical_settings.py`. @@ -231,28 +288,6 @@ def test_resolve_k_line_density( else: assert k_line_density == result_k_line_density - def test_resolve_high_symmetry_points(self): - """ - Test the `resolve_high_symmetry_points` method. Only testing the valid situation in which the `ModelSystem` normalization worked. - """ - # `ModelSystem.normalize()` need to extract `bulk` as a type. - simulation = generate_k_space_simulation( - pbc=[True, True, True], - ) - model_system = simulation.model_system[0] - model_system.normalize(EntryArchive(), logger) # normalize to extract symmetry - k_mesh = simulation.model_method[0].numerical_settings[0].k_mesh[0] - high_symmetry_points = k_mesh.resolve_high_symmetry_points( - simulation.model_system, logger - ) - assert len(high_symmetry_points) == 4 - assert high_symmetry_points == { - 'Gamma': [0, 0, 0], - 'M': [0.5, 0.5, 0], - 'R': [0.5, 0.5, 0.5], - 'X': [0, 0.5, 0], - } - class TestKLinePath: """ @@ -284,6 +319,50 @@ def test_check_high_symmetry_path( ) assert k_line_path._check_high_symmetry_path(logger) == result + @pytest.mark.parametrize( + 'high_symmetry_path_names, result', + [ + (['Gamma', 'X', 'R'], [[0, 0, 0], [0, 0.5, 0], [0.5, 0.5, 0.5]]), + ], + ) + def test_resolve_high_symmetry_path_values( + self, + high_symmetry_path_names: List[str], + result: bool, + ): + """ + Test the `resolve_high_symmetry_path_values` method. Only testing the valid situation in which the `ModelSystem` normalization worked. + """ + # `ModelSystem.normalize()` need to extract `bulk` as a type. + simulation = generate_k_space_simulation( + pbc=[True, True, True], + high_symmetry_path_names=high_symmetry_path_names, + high_symmetry_path_values=None, + ) + model_system = simulation.model_system[0] + model_system.normalize(EntryArchive(), logger) # normalize to extract symmetry + # getting `reciprocal_lattice_vectors` + reciprocal_lattice_vectors = ( + simulation.model_method[0].numerical_settings[0].reciprocal_lattice_vectors + ) + + # `KLinePath` can be understood as a `KMeshBase` section + k_line_path = simulation.model_method[0].numerical_settings[0].k_line_path + k_line_path.normalize(EntryArchive(), logger) + high_symmetry_points_values = k_line_path.resolve_high_symmetry_path_values( + simulation.model_system, reciprocal_lattice_vectors, logger + ) + # high_symmetry_points = k_mesh_base.resolve_high_symmetry_points( + # simulation.model_system, logger + # ) + # assert len(high_symmetry_points) == 4 + # assert high_symmetry_points == { + # 'Gamma': [0, 0, 0], + # 'M': [0.5, 0.5, 0], + # 'R': [0.5, 0.5, 0.5], + # 'X': [0, 0.5, 0], + # } + def test_get_high_symmetry_path_norm(self, k_line_path: KLinePath): """ Test the `get_high_symmetry_path_norm` method. From b271a92171055e598ac6e6068c53cd8003f490ca Mon Sep 17 00:00:00 2001 From: JosePizarro3 Date: Mon, 27 May 2024 11:17:12 +0200 Subject: [PATCH 13/16] Fixed test for KSpaceFunctionalities --- src/nomad_simulations/numerical_settings.py | 12 ++++++------ tests/test_numerical_settings.py | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/nomad_simulations/numerical_settings.py b/src/nomad_simulations/numerical_settings.py index de0912e1..bbbe101c 100644 --- a/src/nomad_simulations/numerical_settings.py +++ b/src/nomad_simulations/numerical_settings.py @@ -169,7 +169,7 @@ def _check_reciprocal_lattice_vectors( self, reciprocal_lattice_vectors: Optional[pint.Quantity], logger: BoundLogger, - check_grid: bool = False, + check_grid: Optional[bool] = False, grid: Optional[List[int]] = [], ) -> bool: """ @@ -404,7 +404,7 @@ def get_k_line_density( (np.float64): The k-line density of the `KMesh`. """ # Initial check - if not KSpaceFunctionalities._check_reciprocal_lattice_vectors( + if not KSpaceFunctionalities()._check_reciprocal_lattice_vectors( reciprocal_lattice_vectors=reciprocal_lattice_vectors, logger=logger, check_grid=True, @@ -438,7 +438,7 @@ def resolve_k_line_density( (Optional[pint.Quantity]): The resolved `k_line_density` of the `KMesh`. """ # Initial check - if not KSpaceFunctionalities._check_reciprocal_lattice_vectors( + if not KSpaceFunctionalities()._check_reciprocal_lattice_vectors( reciprocal_lattice_vectors=reciprocal_lattice_vectors, logger=logger, check_grid=True, @@ -489,7 +489,7 @@ def normalize(self, archive, logger) -> None: # Resolve `high_symmetry_points` if self.high_symmetry_points is None: self.high_symmetry_points = ( - KSpaceFunctionalities.resolve_high_symmetry_points( + KSpaceFunctionalities().resolve_high_symmetry_points( model_systems=model_systems, logger=logger ) ) @@ -553,13 +553,13 @@ def resolve_high_symmetry_path_values( (Optional[List[float]]): The resolved `high_symmetry_path_values`. """ # Initial check on the `reciprocal_lattice_vectors` - if not KSpaceFunctionalities._check_reciprocal_lattice_vectors( + if not KSpaceFunctionalities()._check_reciprocal_lattice_vectors( reciprocal_lattice_vectors=reciprocal_lattice_vectors, logger=logger ): return [] # Resolving the dictionary containing the `high_symmetry_points` for the given ModelSystem symmetry - high_symmetry_points = KSpaceFunctionalities.resolve_high_symmetry_points( + high_symmetry_points = KSpaceFunctionalities().resolve_high_symmetry_points( model_systems=model_systems, logger=logger ) if high_symmetry_points is None: diff --git a/tests/test_numerical_settings.py b/tests/test_numerical_settings.py index 4cc7150b..6cec3020 100644 --- a/tests/test_numerical_settings.py +++ b/tests/test_numerical_settings.py @@ -100,10 +100,10 @@ def test_check_reciprocal_lattice_vectors( """ Test the `_check_reciprocal_lattice_vectors` private method. """ - check = KSpaceFunctionalities._check_reciprocal_lattice_vectors( + check = KSpaceFunctionalities()._check_reciprocal_lattice_vectors( reciprocal_lattice_vectors=reciprocal_lattice_vectors, logger=logger, - check_grd=check_grid, + check_grid=check_grid, grid=grid, ) assert check == result @@ -121,11 +121,11 @@ def test_resolve_high_symmetry_points(self): simulation.model_system[0].normalize(EntryArchive(), logger) # Testing the functionality method - high_symmetry_points = KSpaceFunctionalities.resolve_high_symmetry_points( + high_symmetry_points = KSpaceFunctionalities().resolve_high_symmetry_points( model_systems=model_systems, logger=logger ) assert len(high_symmetry_points) == 4 - assert high_symmetry_points == { + assert high_symmetry_points == { 'Gamma': [0, 0, 0], 'M': [0.5, 0.5, 0], 'R': [0.5, 0.5, 0.5], From 53c934e179f744fa7b2ced4e5aee1d467f182ae3 Mon Sep 17 00:00:00 2001 From: JosePizarro3 Date: Mon, 27 May 2024 11:25:31 +0200 Subject: [PATCH 14/16] Fix testing for KMesh and KLinePath --- src/nomad_simulations/numerical_settings.py | 2 + tests/test_numerical_settings.py | 54 ++++++++------------- 2 files changed, 21 insertions(+), 35 deletions(-) diff --git a/src/nomad_simulations/numerical_settings.py b/src/nomad_simulations/numerical_settings.py index bbbe101c..163c0897 100644 --- a/src/nomad_simulations/numerical_settings.py +++ b/src/nomad_simulations/numerical_settings.py @@ -567,6 +567,8 @@ def resolve_high_symmetry_path_values( # Appending into a list which is stored in the `high_symmetry_path_values`. There is a check in the `normalize()` # function to ensure that the length of the `high_symmetry_path_names` and `high_symmetry_path_values` coincide. + if self.high_symmetry_path_names is None: + return [] high_symmetry_path_values = [ high_symmetry_points[name] for name in self.high_symmetry_path_names diff --git a/tests/test_numerical_settings.py b/tests/test_numerical_settings.py index 6cec3020..3b780da7 100644 --- a/tests/test_numerical_settings.py +++ b/tests/test_numerical_settings.py @@ -193,21 +193,20 @@ def test_resolve_points_and_offset( assert offset == result_offset @pytest.mark.parametrize( - 'system_type, is_representative, grid, reciprocal_lattice_vectors, result_check, result_get_k_line_density, result_k_line_density', + 'system_type, is_representative, grid, reciprocal_lattice_vectors, result_get_k_line_density, result_k_line_density', [ # No `grid` and `reciprocal_lattice_vectors` - ('bulk', False, None, None, False, None, None), + ('bulk', False, None, None, None, None), # No `reciprocal_lattice_vectors` - ('bulk', False, [6, 6, 6], None, False, None, None), + ('bulk', False, [6, 6, 6], None, None, None), # No `grid` - ('bulk', False, None, [[1, 0, 0], [0, 1, 0], [0, 0, 1]], False, None, None), + ('bulk', False, None, [[1, 0, 0], [0, 1, 0], [0, 0, 1]], None, None), # `is_representative` set to False ( 'bulk', False, [6, 6, 6], [[1, 0, 0], [0, 1, 0], [0, 0, 1]], - True, 0.954929658, None, ), @@ -217,7 +216,6 @@ def test_resolve_points_and_offset( True, [6, 6, 6], [[1, 0, 0], [0, 1, 0], [0, 0, 1]], - True, 0.954929658, None, ), @@ -227,7 +225,6 @@ def test_resolve_points_and_offset( True, [6, 6, 6], [[1, 0, 0], [0, 1, 0], [0, 0, 1]], - True, 0.954929658, 0.954929658, ), @@ -239,13 +236,11 @@ def test_resolve_k_line_density( is_representative: bool, grid: Optional[List[int]], reciprocal_lattice_vectors: Optional[List[List[float]]], - result_check: bool, result_get_k_line_density: Optional[float], result_k_line_density: Optional[float], ): """ - Test the `resolve_k_line_density` and `get_k_line_density` methods, as well as the `_check_reciprocal_lattice_vectors` - private method. + Test the `resolve_k_line_density` and `get_k_line_density` methods """ simulation = generate_k_space_simulation( system_type=system_type, @@ -257,13 +252,7 @@ def test_resolve_k_line_density( reciprocal_lattice_vectors = k_space.reciprocal_lattice_vectors k_mesh = k_space.k_mesh[0] model_systems = simulation.model_system - # Checking the reciprocal lattice vectors - assert ( - k_mesh._check_reciprocal_lattice_vectors( - reciprocal_lattice_vectors=reciprocal_lattice_vectors, logger=logger - ) - == result_check - ) + # Applying method `get_k_line_density` get_k_line_density_value = k_mesh.get_k_line_density( reciprocal_lattice_vectors=reciprocal_lattice_vectors, logger=logger @@ -275,6 +264,7 @@ def test_resolve_k_line_density( ) else: assert get_k_line_density_value == result_get_k_line_density + # Applying method `resolve_k_line_density` k_line_density = k_mesh.resolve_k_line_density( model_systems=model_systems, @@ -320,15 +310,22 @@ def test_check_high_symmetry_path( assert k_line_path._check_high_symmetry_path(logger) == result @pytest.mark.parametrize( - 'high_symmetry_path_names, result', + 'reciprocal_lattice_vectors, high_symmetry_path_names, result', [ - (['Gamma', 'X', 'R'], [[0, 0, 0], [0, 0.5, 0], [0.5, 0.5, 0.5]]), + (None, None, []), + ([[1, 0, 0], [0, 1, 0], [0, 0, 1]], None, []), + ( + [[1, 0, 0], [0, 1, 0], [0, 0, 1]], + ['Gamma', 'X', 'R'], + [[0, 0, 0], [0, 0.5, 0], [0.5, 0.5, 0.5]], + ), ], ) def test_resolve_high_symmetry_path_values( self, + reciprocal_lattice_vectors: Optional[List[List[float]]], high_symmetry_path_names: List[str], - result: bool, + result: List[float], ): """ Test the `resolve_high_symmetry_path_values` method. Only testing the valid situation in which the `ModelSystem` normalization worked. @@ -336,32 +333,19 @@ def test_resolve_high_symmetry_path_values( # `ModelSystem.normalize()` need to extract `bulk` as a type. simulation = generate_k_space_simulation( pbc=[True, True, True], + reciprocal_lattice_vectors=reciprocal_lattice_vectors, high_symmetry_path_names=high_symmetry_path_names, high_symmetry_path_values=None, ) model_system = simulation.model_system[0] model_system.normalize(EntryArchive(), logger) # normalize to extract symmetry - # getting `reciprocal_lattice_vectors` - reciprocal_lattice_vectors = ( - simulation.model_method[0].numerical_settings[0].reciprocal_lattice_vectors - ) # `KLinePath` can be understood as a `KMeshBase` section k_line_path = simulation.model_method[0].numerical_settings[0].k_line_path - k_line_path.normalize(EntryArchive(), logger) high_symmetry_points_values = k_line_path.resolve_high_symmetry_path_values( simulation.model_system, reciprocal_lattice_vectors, logger ) - # high_symmetry_points = k_mesh_base.resolve_high_symmetry_points( - # simulation.model_system, logger - # ) - # assert len(high_symmetry_points) == 4 - # assert high_symmetry_points == { - # 'Gamma': [0, 0, 0], - # 'M': [0.5, 0.5, 0], - # 'R': [0.5, 0.5, 0.5], - # 'X': [0, 0.5, 0], - # } + assert high_symmetry_points_values == result def test_get_high_symmetry_path_norm(self, k_line_path: KLinePath): """ From 21426feff215d94116c9f1cdcd3750e6c67ae24d Mon Sep 17 00:00:00 2001 From: JosePizarro3 Date: Mon, 27 May 2024 11:36:41 +0200 Subject: [PATCH 15/16] Changed points in KMesh and KLinePath variables to be references instead of copy the information --- src/nomad_simulations/variables.py | 48 ++++++++---------------------- 1 file changed, 12 insertions(+), 36 deletions(-) diff --git a/src/nomad_simulations/variables.py b/src/nomad_simulations/variables.py index 68304e9f..89c0e61b 100644 --- a/src/nomad_simulations/variables.py +++ b/src/nomad_simulations/variables.py @@ -172,10 +172,9 @@ class KMesh(Variables): ) points = Quantity( - type=np.float64, - shape=['n_points', 'dimensionality'], + type=KMeshSettings.points, description=""" - K-point mesh over which the physical property is calculated. These are 3D arrays stored in fractional coordinates. + Reference to the `KMesh.points` over which the physical property is calculated. These are 3D arrays stored in fractional coordinates. """, ) @@ -185,25 +184,13 @@ def __init__( super().__init__(m_def, m_context, **kwargs) self.name = self.m_def.name - def extract_points(self, logger: BoundLogger) -> Optional[list]: - """ - Extract the `points` list from the `k_mesh_settings_ref` pointing to the `KMesh` section. - Args: - logger (BoundLogger): The logger to log messages. - Returns: - (Optional[list]): The `points` list. - """ - if self.k_mesh_settings_ref is not None: - if self.k_mesh_settings_ref.points is not None: - return self.k_mesh_settings_ref.points - points, _ = self.k_mesh_settings_ref.resolve_points_and_offset(logger) - return points - logger.error('`k_mesh_settings_ref` is not defined.') - return None - def normalize(self, archive, logger) -> None: # Extracting `points` from the `k_mesh_settings_ref` BEFORE doing `super().normalize()` self.points = self.extract_points(logger) + if self.k_mesh_settings_ref is None: + logger.error('`k_mesh_settings_ref` is not defined.') + return + self.points = self.k_mesh_settings_ref # ref to `points` super().normalize(archive, logger) @@ -220,10 +207,9 @@ class KLinePath(Variables): ) points = Quantity( - type=np.float64, - shape=['n_points', 3], + type=KLinePathSettings.points, description=""" - Points along the k-line path in which the physical property is calculated. These are 3D arrays stored in fractional coordinates. + Reference to the `KLinePath.points` in which the physical property is calculated. These are 3D arrays stored in fractional coordinates. """, ) @@ -233,21 +219,11 @@ def __init__( super().__init__(m_def, m_context, **kwargs) self.name = self.m_def.name - def extract_points(self, logger: BoundLogger) -> Optional[list]: - """ - Extract the `points` list from the `k_line_path_settings_ref` pointing to the `KLinePath` section. - Args: - logger (BoundLogger): The logger to log messages. - Returns: - (Optional[list]): The `points` list. - """ - if self.k_line_path_settings_ref is not None: - return self.k_line_path_settings_ref.points - logger.error('`k_line_path_settings_ref` is not defined.') - return None - def normalize(self, archive, logger) -> None: # Extracting `points` from the `k_line_path_settings_ref` BEFORE doing `super().normalize()` - self.points = self.extract_points(logger) + if self.k_line_path_settings_ref is None: + logger.error('`k_line_path_settings_ref` is not defined.') + return + self.points = self.k_line_path_settings_ref # ref to `points` super().normalize(archive, logger) From 4b83e2ee5077893e7c876ec5da85e186dad630b7 Mon Sep 17 00:00:00 2001 From: JosePizarro3 Date: Mon, 27 May 2024 11:55:13 +0200 Subject: [PATCH 16/16] Deleted line in KMesh(Variables) --- src/nomad_simulations/variables.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/nomad_simulations/variables.py b/src/nomad_simulations/variables.py index 89c0e61b..0fe97d75 100644 --- a/src/nomad_simulations/variables.py +++ b/src/nomad_simulations/variables.py @@ -186,7 +186,6 @@ def __init__( def normalize(self, archive, logger) -> None: # Extracting `points` from the `k_mesh_settings_ref` BEFORE doing `super().normalize()` - self.points = self.extract_points(logger) if self.k_mesh_settings_ref is None: logger.error('`k_mesh_settings_ref` is not defined.') return