Coverage for /builds/ase/ase/ase/spacegroup/symmetrize.py: 92.06%
126 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
1# fmt: off
3"""
4Provides utility functions for FixSymmetry class
5"""
6from collections.abc import MutableMapping
7from typing import Optional
9import numpy as np
11from ase.utils import atoms_to_spglib_cell
13__all__ = ['refine_symmetry', 'check_symmetry']
16def spglib_get_symmetry_dataset(*args, **kwargs):
17 """Temporary compatibility adapter around spglib dataset.
19 Return an object that allows attribute-based access
20 in line with recent spglib. This allows ASE code to not care about
21 older spglib versions.
22 """
23 import spglib
24 dataset = spglib.get_symmetry_dataset(*args, **kwargs)
25 if dataset is None:
26 return None
27 if isinstance(dataset, dict): # spglib < 2.5.0
28 return SpglibDatasetWrapper(dataset)
29 return dataset # spglib >= 2.5.0
32class SpglibDatasetWrapper(MutableMapping):
33 # Spglib 2.5.0 returns SpglibDataset with deprecated __getitem__.
34 # Spglib 2.4.0 and earlier return dict.
35 #
36 # We use this object to wrap dictionaries such that both types of access
37 # work correctly.
38 def __init__(self, spglib_dct):
39 self._spglib_dct = spglib_dct
41 def __getattr__(self, attr):
42 return self[attr]
44 def __getitem__(self, key):
45 return self._spglib_dct[key]
47 def __len__(self):
48 return len(self._spglib_dct)
50 def __iter__(self):
51 return iter(self._spglib_dct)
53 def __setitem__(self, key, value):
54 self._spglib_dct[key] = value
56 def __delitem__(self, item):
57 del self._spglib_dct[item]
60def print_symmetry(symprec, dataset):
61 print("ase.spacegroup.symmetrize: prec", symprec,
62 "got symmetry group number", dataset.number,
63 ", international (Hermann-Mauguin)", dataset.international,
64 ", Hall ", dataset.hall)
67def refine_symmetry(atoms, symprec=0.01, verbose=False):
68 """
69 Refine symmetry of an Atoms object
71 Parameters
72 ----------
73 atoms - input Atoms object
74 symprec - symmetry precicion
75 verbose - if True, print out symmetry information before and after
77 Returns
78 -------
80 spglib dataset
82 """
83 _check_and_symmetrize_cell(atoms, symprec=symprec, verbose=verbose)
84 _check_and_symmetrize_positions(atoms, symprec=symprec, verbose=verbose)
85 return check_symmetry(atoms, symprec=1e-4, verbose=verbose)
88class IntermediateDatasetError(Exception):
89 """The symmetry dataset in `_check_and_symmetrize_positions` can be at odds
90 with the original symmetry dataset in `_check_and_symmetrize_cell`.
91 This implies a faulty partial symmetrization if not handled by exception."""
94def get_symmetrized_atoms(atoms,
95 symprec: float = 0.01,
96 final_symprec: Optional[float] = None):
97 """Get new Atoms object with refined symmetries.
99 Checks internal consistency of the found symmetries.
101 Parameters
102 ----------
103 atoms : Atoms
104 Input atoms object.
105 symprec : float
106 Symmetry precision used to identify symmetries with spglib.
107 final_symprec : float
108 Symmetry precision used for testing the symmetrization.
110 Returns
111 -------
112 symatoms : Atoms
113 New atoms object symmetrized according to the input symprec.
114 """
115 atoms = atoms.copy()
116 original_dataset = _check_and_symmetrize_cell(atoms, symprec=symprec)
117 intermediate_dataset = _check_and_symmetrize_positions(
118 atoms, symprec=symprec)
119 if intermediate_dataset.number != original_dataset.number:
120 raise IntermediateDatasetError()
121 final_symprec = final_symprec or symprec
122 final_dataset = check_symmetry(atoms, symprec=final_symprec)
123 assert final_dataset.number == original_dataset.number
124 return atoms, final_dataset
127def _check_and_symmetrize_cell(atoms, **kwargs):
128 dataset = check_symmetry(atoms, **kwargs)
129 _symmetrize_cell(atoms, dataset)
130 return dataset
133def _symmetrize_cell(atoms, dataset):
134 # set actual cell to symmetrized cell vectors by copying
135 # transformed and rotated standard cell
136 std_cell = dataset.std_lattice
137 trans_std_cell = dataset.transformation_matrix.T @ std_cell
138 rot_trans_std_cell = trans_std_cell @ dataset.std_rotation_matrix
139 atoms.set_cell(rot_trans_std_cell, True)
142def _check_and_symmetrize_positions(atoms, *, symprec, **kwargs):
143 import spglib
144 dataset = check_symmetry(atoms, symprec=symprec, **kwargs)
145 # here we are assuming that primitive vectors returned by find_primitive
146 # are compatible with std_lattice returned by get_symmetry_dataset
147 res = spglib.find_primitive(atoms_to_spglib_cell(atoms), symprec=symprec)
148 _symmetrize_positions(atoms, dataset, res)
149 return dataset
152def _symmetrize_positions(atoms, dataset, primitive_spglib_cell):
153 prim_cell, _prim_scaled_pos, _prim_types = primitive_spglib_cell
155 # calculate offset between standard cell and actual cell
156 std_cell = dataset.std_lattice
157 rot_std_cell = std_cell @ dataset.std_rotation_matrix
158 rot_std_pos = dataset.std_positions @ rot_std_cell
159 pos = atoms.get_positions()
160 dp0 = (pos[list(dataset.mapping_to_primitive).index(0)] - rot_std_pos[
161 list(dataset.std_mapping_to_primitive).index(0)])
163 # create aligned set of standard cell positions to figure out mapping
164 rot_prim_cell = prim_cell @ dataset.std_rotation_matrix
165 inv_rot_prim_cell = np.linalg.inv(rot_prim_cell)
166 aligned_std_pos = rot_std_pos + dp0
168 # find ideal positions from position of corresponding std cell atom +
169 # integer_vec . primitive cell vectors
170 mapping_to_primitive = list(dataset.mapping_to_primitive)
171 std_mapping_to_primitive = list(dataset.std_mapping_to_primitive)
172 pos = atoms.get_positions()
173 for i_at in range(len(atoms)):
174 std_i_at = std_mapping_to_primitive.index(mapping_to_primitive[i_at])
175 dp = aligned_std_pos[std_i_at] - pos[i_at]
176 dp_s = dp @ inv_rot_prim_cell
177 pos[i_at] = (aligned_std_pos[std_i_at] - np.round(dp_s) @ rot_prim_cell)
178 atoms.set_positions(pos)
181def check_symmetry(atoms, symprec=1.0e-6, verbose=False):
182 """
183 Check symmetry of `atoms` with precision `symprec` using `spglib`
185 Prints a summary and returns result of `spglib.get_symmetry_dataset()`
186 """
187 dataset = spglib_get_symmetry_dataset(atoms_to_spglib_cell(atoms),
188 symprec=symprec)
189 if verbose:
190 print_symmetry(symprec, dataset)
191 return dataset
194def is_subgroup(sup_data, sub_data, tol=1e-10):
195 """
196 Test if spglib dataset `sub_data` is a subgroup of dataset `sup_data`
197 """
198 for rot1, trns1 in zip(sub_data.rotations, sub_data.translations):
199 for rot2, trns2 in zip(sup_data.rotations, sup_data.translations):
200 if np.all(rot1 == rot2) and np.linalg.norm(trns1 - trns2) < tol:
201 break
202 else:
203 return False
204 return True
207def prep_symmetry(atoms, symprec=1.0e-6, verbose=False):
208 """
209 Prepare `at` for symmetry-preserving minimisation at precision `symprec`
211 Returns a tuple `(rotations, translations, symm_map)`
212 """
213 dataset = spglib_get_symmetry_dataset(atoms_to_spglib_cell(atoms),
214 symprec=symprec)
215 if verbose:
216 print_symmetry(symprec, dataset)
217 rotations = dataset.rotations.copy()
218 translations = dataset.translations.copy()
219 symm_map = []
220 scaled_pos = atoms.get_scaled_positions()
221 for (rot, trans) in zip(rotations, translations):
222 this_op_map = [-1] * len(atoms)
223 for i_at in range(len(atoms)):
224 new_p = rot @ scaled_pos[i_at, :] + trans
225 dp = scaled_pos - new_p
226 dp -= np.round(dp)
227 i_at_map = np.argmin(np.linalg.norm(dp, axis=1))
228 this_op_map[i_at] = i_at_map
229 symm_map.append(this_op_map)
230 return (rotations, translations, symm_map)
233def symmetrize_rank1(lattice, inv_lattice, forces, rot, trans, symm_map):
234 """
235 Return symmetrized forces
237 lattice vectors expected as row vectors (same as ASE get_cell() convention),
238 inv_lattice is its matrix inverse (reciprocal().T)
239 """
240 scaled_symmetrized_forces_T = np.zeros(forces.T.shape)
242 scaled_forces_T = np.dot(inv_lattice.T, forces.T)
243 for (r, t, this_op_map) in zip(rot, trans, symm_map):
244 transformed_forces_T = np.dot(r, scaled_forces_T)
245 scaled_symmetrized_forces_T[:, this_op_map] += transformed_forces_T
246 scaled_symmetrized_forces_T /= len(rot)
247 symmetrized_forces = (lattice.T @ scaled_symmetrized_forces_T).T
249 return symmetrized_forces
252def symmetrize_rank2(lattice, lattice_inv, stress_3_3, rot):
253 """
254 Return symmetrized stress
256 lattice vectors expected as row vectors (same as ASE get_cell() convention),
257 inv_lattice is its matrix inverse (reciprocal().T)
258 """
259 scaled_stress = np.dot(np.dot(lattice, stress_3_3), lattice.T)
261 symmetrized_scaled_stress = np.zeros((3, 3))
262 for r in rot:
263 symmetrized_scaled_stress += np.dot(np.dot(r.T, scaled_stress), r)
264 symmetrized_scaled_stress /= len(rot)
266 sym = np.dot(np.dot(lattice_inv, symmetrized_scaled_stress), lattice_inv.T)
267 return sym