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