Coverage for icet/core/matrix_of_equivalent_positions.py: 94%
72 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-12-26 04:12 +0000
« prev ^ index » next coverage.py v7.5.0, created at 2024-12-26 04:12 +0000
1"""
2This module provides a Python interface to the MatrixOfEquivalentPositions
3class with supplementary functions.
4"""
6from typing import List, Tuple
8import numpy as np
9import spglib
11from ase import Atoms
12from icet.core.lattice_site import LatticeSite
13from icet.core.neighbor_list import get_neighbor_lists
14from icet.core.structure import Structure
15from icet.input_output.logging_tools import logger
16from icet.tools.geometry import (ase_atoms_to_spglib_cell,
17 get_fractional_positions_from_neighbor_list,
18 get_primitive_structure)
20logger = logger.getChild('matrix_of_equivalent_positions')
23class MatrixOfEquivalentPositions:
24 """
25 This class handles a matrix of equivalent positions given the symmetry
26 elements of an atomic structure.
28 Note
29 ----
30 As a user you will usually not interact directly with objects of this type.
32 Parameters
33 ----------
34 translations
35 Translational symmetry elements.
36 rotations
37 Rotational symmetry elements.
38 """
40 def __init__(self, translations: np.ndarray, rotations: np.ndarray):
41 if len(translations) != len(rotations):
42 raise ValueError(f'The number of translations ({len(translations)})'
43 f' must equal the number of rotations ({len(rotations)}).')
44 self.n_symmetries = len(rotations)
45 self.translations = np.array(translations)
46 self.rotations = np.array(rotations)
48 def build(self, fractional_positions: np.ndarray) -> None:
49 """
50 Builds a matrix of symmetry equivalent positions given a set of input
51 coordinates using the rotational and translational symmetries provided upon
52 initialization of the object.
54 Parameters
55 ----------
56 fractional_positions
57 Atomic positions in fractional coordinates.
58 Dimensions: (number of atoms, 3 fractional coordinates).
59 """
60 positions = np.dot(self.rotations, fractional_positions.transpose())
61 positions = np.moveaxis(positions, 2, 0)
62 translations = self.translations[np.newaxis, :].repeat(len(fractional_positions), axis=0)
63 positions += translations
64 self.positions = positions
66 def get_equivalent_positions(self) -> np.ndarray:
67 """
68 Returns the matrix of equivalent positions. Each row corresponds
69 to a set of symmetry equivalent positions. The entry in the
70 first column is commonly treated as the representative position.
71 Dimensions: (number of atoms, number of symmetries, 3 fractional coordinates)
72 """
73 return self.positions
76def matrix_of_equivalent_positions_from_structure(structure: Atoms,
77 cutoff: float,
78 position_tolerance: float,
79 symprec: float,
80 find_primitive: bool = True) \
81 -> Tuple[np.ndarray, Structure, List]:
82 """Sets up a matrix of equivalent positions from an :class:`Atoms <ase.Atoms>` object.
84 Parameters
85 ----------
86 structure
87 Input structure.
88 cutoff
89 Cutoff radius.
90 find_primitive
91 If ``True`` the symmetries of the primitive structure will be employed.
92 symprec
93 Tolerance imposed when analyzing the symmetry using spglib.
94 position_tolerance
95 Tolerance applied when comparing positions in Cartesian coordinates.
97 Returns
98 -------
99 The tuple that is returned comprises the matrix of equivalent positions,
100 the primitive structure, and the neighbor list.
101 """
103 structure = structure.copy()
104 structure_prim = structure
105 if find_primitive:
106 structure_prim = get_primitive_structure(structure, symprec=symprec)
107 logger.debug(f'Size of primitive structure: {len(structure_prim)}')
109 # get symmetry information
110 structure_as_tuple = ase_atoms_to_spglib_cell(structure_prim)
111 symmetry = spglib.get_symmetry(structure_as_tuple, symprec=symprec)
112 translations = symmetry['translations']
113 rotations = symmetry['rotations']
115 # set up a MatrixOfEquivalentPositions object
116 matrix_of_equivalent_positions = MatrixOfEquivalentPositions(translations, rotations)
118 # create neighbor lists
119 prim_icet_structure = Structure.from_atoms(structure_prim)
121 neighbor_list = get_neighbor_lists(prim_icet_structure,
122 [cutoff],
123 position_tolerance=position_tolerance)[0]
125 # get fractional positions for neighbor_list
126 frac_positions = get_fractional_positions_from_neighbor_list(
127 prim_icet_structure, neighbor_list)
129 logger.debug(f'Number of fractional positions: {len(frac_positions)}')
130 if frac_positions is not None: 130 ↛ 133line 130 didn't jump to line 133, because the condition on line 130 was never false
131 matrix_of_equivalent_positions.build(frac_positions)
133 return matrix_of_equivalent_positions, prim_icet_structure, neighbor_list
136def _get_lattice_site_matrix_of_equivalent_positions(
137 structure: Structure,
138 matrix_of_equivalent_positions: MatrixOfEquivalentPositions,
139 fractional_position_tolerance: float,
140 prune: bool = True) -> np.ndarray:
141 """
142 Returns a transformed matrix of equivalent positions with lattice sites as
143 entries instead of fractional coordinates.
145 Parameters
146 ----------
147 structure
148 Primitive structure.
149 matrix_of_equivalent_positions
150 Matrix of equivalent positions with fractional coordinates format entries.
151 fractional_position_tolerance
152 Tolerance applied when evaluating distances in fractional coordinates.
153 prune
154 If ``True`` the matrix of equivalent positions will be pruned.
156 Returns
157 -------
158 Matrix of equivalent positions in row major order with entries in lattice site format.
159 """
160 eqpos_frac = matrix_of_equivalent_positions.get_equivalent_positions()
162 eqpos_lattice_sites = []
163 for row in eqpos_frac:
164 positions = _fractional_to_cartesian(row, structure.cell)
165 lattice_sites = []
166 if np.all(structure.pbc): 166 ↛ 170line 166 didn't jump to line 170, because the condition on line 166 was never false
167 lattice_sites = structure.find_lattice_sites_by_positions(
168 positions=positions, fractional_position_tolerance=fractional_position_tolerance)
169 else:
170 raise ValueError('Input structure must have periodic boundary conditions.')
171 if lattice_sites is not None: 171 ↛ 174line 171 didn't jump to line 174, because the condition on line 171 was never false
172 eqpos_lattice_sites.append(lattice_sites)
173 else:
174 logger.warning('Unable to transform any element in a column of the'
175 ' fractional matrix of equivalent positions to lattice site')
176 if prune: 176 ↛ 185line 176 didn't jump to line 185, because the condition on line 176 was never false
177 logger.debug('Size of columns of the matrix of equivalent positions before'
178 ' pruning {}'.format(len(eqpos_lattice_sites)))
180 eqpos_lattice_sites = _prune_matrix_of_equivalent_positions(eqpos_lattice_sites)
182 logger.debug('Size of columns of the matrix of equivalent positions after'
183 ' pruning {}'.format(len(eqpos_lattice_sites)))
185 return eqpos_lattice_sites
188def _prune_matrix_of_equivalent_positions(matrix_of_equivalent_positions: List[List[LatticeSite]]):
189 """
190 Prunes the matrix so that the first column only contains unique elements.
192 Parameters
193 ----------
194 matrix_of_equivalent_positions
195 Permutation matrix with :class:`LatticeSite` type entries.
196 """
198 for i in range(len(matrix_of_equivalent_positions)):
199 for j in reversed(range(len(matrix_of_equivalent_positions))):
200 if j <= i:
201 continue
202 if matrix_of_equivalent_positions[i][0] == matrix_of_equivalent_positions[j][0]:
203 matrix_of_equivalent_positions.pop(j)
204 logger.debug('Removing duplicate in matrix of equivalent positions'
205 'i: {} j: {}'.format(i, j))
206 return matrix_of_equivalent_positions
209def _fractional_to_cartesian(fractional_coordinates: List[List[float]],
210 cell: np.ndarray) -> List[float]:
211 """
212 Converts cell metrics from fractional to Cartesian coordinates.
214 Parameters
215 ----------
216 fractional_coordinates
217 List of fractional coordinates.
218 cell
219 Cell metric.
220 """
221 cartesian_coordinates = [np.dot(frac, cell)
222 for frac in fractional_coordinates]
223 return cartesian_coordinates