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..163c0897 100644 --- a/src/nomad_simulations/numerical_settings.py +++ b/src/nomad_simulations/numerical_settings.py @@ -18,23 +18,17 @@ import numpy as np import pint +from itertools import accumulate, tee, chain from structlog.stdlib import BoundLogger -from typing import Optional, List, Tuple +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 .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 +50,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 +160,156 @@ def normalize(self, archive, logger) -> None: super().normalize(archive, logger) -class LinePathSegment(ArchiveSection): +class KSpaceFunctionalities: """ - A base section used to define the settings of a single line path segment within a multidimensional mesh. + A functionality class useful for defining methods shared between `KSpace`, `KMesh`, and `KLinePath`. """ - 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. - """, - ) + def _check_reciprocal_lattice_vectors( + self, + reciprocal_lattice_vectors: Optional[pint.Quantity], + logger: BoundLogger, + check_grid: Optional[bool] = False, + grid: Optional[List[int]] = [], + ) -> bool: + """ + Check if the `reciprocal_lattice_vectors` exist and if they have the same dimensionality as `grid`. - n_line_points = Quantity( - type=np.int32, - description=""" - Number of points in the line path segment. - """, - ) + 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 []. - 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`. - """, - ) + Returns: + (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: + logger.warning('Could not find `reciprocal_lattice_vectors`.') + return False + # 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.' + ) + return False + return True - def resolve_points( + def resolve_high_symmetry_points( self, - high_symmetry_path: List[str], - n_line_points: int, + model_systems: List[ModelSystem], logger: BoundLogger, - ) -> Optional[np.ndarray]: + eps: float = 3e-3, + ) -> Optional[dict]: """ - Resolves the `points` of the `LinePathSegment` from the `high_symmetry_path` and the `n_line_points`. + 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: - 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. + 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[List[np.ndarray]]): The resolved `points` of the `LinePathSegment`. + (Optional[dict]): The resolved `high_symmetry_points`. """ - if high_symmetry_path is None or n_line_points is None: + # 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 `LinePathSegment.points` from `LinePathSegment.high_symmetry_path` and `LinePathSegment.n_line_points`.' + 'Could not resolve `bravais_lattice` and `lattice` ASE object from the `ModelSystem`.' ) return None - if self.m_parent.high_symmetry_points is 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 resolve the parent of `LinePathSegment` to extract `LinePathSegment.m_parent.high_symmetry_points`.' + 'Could not find `lattice.get_special_points()` from the ASE package.' ) 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 - ) + 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 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`). """ - 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,21 +325,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 = { + high_symmetry_points ={ 'Gamma': [0, 0, 0], 'X': [0.5, 0, 0], - } + 'Y': [0, 0.5, 0], + ... + ] """, ) @@ -285,21 +349,13 @@ 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. """, ) - 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 resolve_points_and_offset( self, logger: BoundLogger ) -> Tuple[Optional[List[np.ndarray]], Optional[np.ndarray]]: @@ -312,9 +368,13 @@ 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': + 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]) @@ -326,11 +386,12 @@ 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( - 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,25 +403,29 @@ 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 not KSpaceFunctionalities()._check_reciprocal_lattice_vectors( + reciprocal_lattice_vectors=reciprocal_lattice_vectors, + logger=logger, + check_grid=True, + grid=self.grid, + ): 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, 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,32 +437,29 @@ def resolve_k_line_density( Returns: (Optional[pint.Quantity]): The resolved `k_line_density` of the `KMesh`. """ + # Initial check + if not KSpaceFunctionalities()._check_reciprocal_lattice_vectors( + reciprocal_lattice_vectors=reciprocal_lattice_vectors, + logger=logger, + check_grid=True, + grid=self.grid, + ): + return None + 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 - - 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 - ) + continue # 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 k_line_density return None def normalize(self, archive, logger) -> None: @@ -413,33 +475,359 @@ 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 + ) + # Resolve `high_symmetry_points` + if self.high_symmetry_points is None: + self.high_symmetry_points = ( + KSpaceFunctionalities().resolve_high_symmetry_points( + model_systems=model_systems, logger=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. 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_names = Quantity( + type=str, + shape=['*'], + description=""" + 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]]`. + """, + ) + + 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 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 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( + model_systems=model_systems, logger=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. + 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 + if name in high_symmetry_points.keys() + ] + 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], + 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 + 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. + logger (BoundLogger): The logger to log messages. + + Returns: + (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 + + 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 + + # 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) + + # Skip the first element in the second iterator + 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) + + 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 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. + 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 not self._check_high_symmetry_path(logger): + 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 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 = 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], + ) + 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.') + self.points = new_points + + 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 + + +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`. + """ + + # ! 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], + 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): + continue + # TODO extend this for other dimensions (@ndaelman-hu) + if model_system.type != 'bulk': + logger.warning('`ModelSystem.type` is not describing a bulk system.') + continue + + atomic_cell = model_system.cell + if atomic_cell is None: + logger.warning('`ModelSystem.cell` was not found.') + continue + + # 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..0fe97d75 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,74 @@ 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_settings_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=KMeshSettings.points, + description=""" + Reference to the `KMesh.points` 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 normalize(self, archive, logger) -> None: + # Extracting `points` from the `k_mesh_settings_ref` BEFORE doing `super().normalize()` + 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) + + +class KLinePath(Variables): + """ """ + + k_line_path_settings_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=KLinePathSettings.points, + description=""" + Reference to the `KLinePath.points` 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 normalize(self, archive, logger) -> None: + # Extracting `points` from the `k_line_path_settings_ref` BEFORE doing `super().normalize()` + 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) diff --git a/tests/conftest.py b/tests/conftest.py index bde50e79..39107094 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,8 +17,9 @@ # 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 @@ -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,13 @@ 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, + pbc: List[bool] = [False, False, False], ) -> Optional[ModelSystem]: """ Generate a `ModelSystem` section with the given parameters. @@ -81,8 +91,13 @@ 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, + periodic_boundary_conditions=pbc, + ) model_system.cell.append(atomic_cell) # Add atoms_state to the model_system @@ -208,6 +223,73 @@ def generate_simulation_electronic_dos( return simulation +def generate_k_line_path( + 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_names=high_symmetry_path_names, + high_symmetry_path_values=high_symmetry_path_values, + ) + + +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_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: + model_system = generate_model_system( + 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` + 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_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() + 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 +308,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..3b780da7 --- /dev/null +++ b/tests/test_numerical_settings.py @@ -0,0 +1,396 @@ +# +# 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 KMesh, KLinePath, KSpaceFunctionalities + +from . import logger +from .conftest import generate_k_line_path, 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 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_grid=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`. + """ + + @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_get_k_line_density, result_k_line_density', + [ + # No `grid` and `reciprocal_lattice_vectors` + ('bulk', False, None, None, None, None), + # No `reciprocal_lattice_vectors` + ('bulk', False, [6, 6, 6], None, None, None), + # No `grid` + ('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]], + 0.954929658, + None, + ), + # `system_type` is not 'bulk' + ( + 'atom', + True, + [6, 6, 6], + [[1, 0, 0], [0, 1, 0], [0, 0, 1]], + 0.954929658, + None, + ), + # All parameters are set + ( + 'bulk', + True, + [6, 6, 6], + [[1, 0, 0], [0, 1, 0], [0, 0, 1]], + 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_get_k_line_density: Optional[float], + result_k_line_density: Optional[float], + ): + """ + Test the `resolve_k_line_density` and `get_k_line_density` methods + """ + 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 + + # 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: + """ + 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 + + @pytest.mark.parametrize( + 'reciprocal_lattice_vectors, high_symmetry_path_names, result', + [ + (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: List[float], + ): + """ + 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], + 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 + + # `KLinePath` can be understood as a `KMeshBase` section + k_line_path = simulation.model_method[0].numerical_settings[0].k_line_path + high_symmetry_points_values = k_line_path.resolve_high_symmetry_path_values( + simulation.model_system, reciprocal_lattice_vectors, logger + ) + assert high_symmetry_points_values == result + + 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_norms( + reciprocal_lattice_vectors=rlv, logger=logger + ) + 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): + """ + Test the `resolve_points` method. + """ + rlv = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) * ureg('1/meter') + hs_points = [0, 0.5, 0.5 + 1 / np.sqrt(2), 1 + 1 / np.sqrt(2)] + # Define paths + 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 + ) + 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)