diff --git a/src/structuretoolkit/analyse/symmetry.py b/src/structuretoolkit/analyse/symmetry.py index 7af40bc40..83f89f2ce 100644 --- a/src/structuretoolkit/analyse/symmetry.py +++ b/src/structuretoolkit/analyse/symmetry.py @@ -4,6 +4,7 @@ import ast import dataclasses import string +import warnings import numpy as np import spglib @@ -400,26 +401,40 @@ def get_primitive_cell( >>> symmetry = Symmetry(structure) >>> len(symmetry.get_primitive_cell()) == len(basis) True + + .. warning:: + Custom arrays defined in the base structures + :attr:`ase.atoms.Atoms.arrays` and other state (.info, .calc, etc.) are not copied to the new structure! """ + if not all(self._structure.pbc): + raise ValueError("Can only symmetrize periodic structures.") ret = spglib.standardize_cell( self._get_spglib_cell(use_elements=use_elements, use_magmoms=use_magmoms), to_primitive=not standardize, ) if ret is None: raise SymmetryError(spglib.error.get_error_message()) - cell, positions, indices = ret - positions = (cell.T @ positions.T).T - new_structure = self._structure.copy() - new_structure.cell = cell - new_structure = new_structure[: len(indices)] + cell, scaled_positions, indices = ret indices_dict = { v: k for k, v in structuretoolkit.common.helper.get_species_indices_dict( structure=self._structure ).items() } - new_structure.symbols = [indices_dict[i] for i in indices] - new_structure.positions = positions + symbols = [indices_dict[i] for i in indices] + new_structure = type(self._structure)( + symbols=symbols, + scaled_positions=scaled_positions, + cell=cell, + pbc=[True, True, True], + ) + keys = set(self._structure.arrays) - {"numbers", "positions"} + if len(keys) > 0: + warnings.warn( + f"Custom arrays {keys} do not carry over to new structure!", + stacklevel=2, + ) + return new_structure def get_ir_reciprocal_mesh( diff --git a/tests/test_analyse_symmetry.py b/tests/test_analyse_symmetry.py index 7f956e91d..aa09acd59 100644 --- a/tests/test_analyse_symmetry.py +++ b/tests/test_analyse_symmetry.py @@ -3,6 +3,7 @@ # Distributed under the terms of "New BSD License", see the LICENSE file. import unittest +import warnings import numpy as np from ase.atoms import Atoms @@ -20,6 +21,7 @@ try: import spglib + from spglib.error import SpglibError skip_spglib_test = False except ImportError: @@ -31,6 +33,39 @@ ) class TestSymmetry(unittest.TestCase): def test_get_arg_equivalent_sites(self): + a_0 = 4.0 + structure = bulk("Al", cubic=True, a=a_0).repeat(2) + sites = stk.common.get_wrapped_coordinates( + structure=structure, + positions=structure.positions + np.array([0, 0, 0.5 * a_0]), + ) + v_position = structure.positions[0] + del structure[0] + pairs = np.stack( + ( + stk.analyse.get_symmetry(structure=structure).get_arg_equivalent_sites( + sites + ), + np.unique( + np.round( + stk.analyse.get_distances_array( + structure=structure, p1=v_position, p2=sites + ), + decimals=2, + ), + return_inverse=True, + )[1], + ), + axis=-1, + ) + unique_pairs = np.unique(pairs, axis=0) + self.assertEqual(len(unique_pairs), len(np.unique(unique_pairs[:, 0]))) + with self.assertRaises(ValueError): + stk.analyse.get_symmetry(structure=structure).get_arg_equivalent_sites( + [0, 0, 0] + ) + + def test_group_points_by_symmetry(self): a_0 = 4.0 structure = bulk("Al", cubic=True, a=a_0).repeat(2) sites = stk.common.get_wrapped_coordinates( @@ -65,15 +100,115 @@ def test_get_arg_equivalent_sites(self): points=[0, 0, 0], ) + def test_generate_equivalent_points(self): + a_0 = 4 + structure = bulk("Al", cubic=True, a=a_0) + sym = stk.analyse.get_symmetry(structure) + self.assertEqual( + len(structure), len(sym.generate_equivalent_points([0, 0, 0.5 * a_0])) + ) + x = np.array([[0, 0, 0.5 * a_0], 3 * [0.25 * a_0]]) + y = np.random.randn(2) + sym_x = sym.generate_equivalent_points(x, return_unique=False) + y = np.tile(y, len(sym_x)) + sym_x = sym_x.reshape(-1, 3) + xy = np.round( + [ + stk.analyse.get_neighborhood( + structure, sym_x, num_neighbors=1 + ).distances.flatten(), + y, + ], + decimals=8, + ) + self.assertEqual( + np.unique(xy, axis=1).shape, + (2, 2), + msg="order of generated points does not match the original order", + ) + + def test_get_symmetry(self): + cell = 2.2 * np.identity(3) + Al = Atoms( + "AlAl", positions=[(0, 0, 0), (0.5, 0.5, 0.5)], cell=cell, pbc=True + ).repeat(2) + self.assertEqual( + len(set(stk.analyse.get_symmetry(structure=Al)["equivalent_atoms"])), 1 + ) + self.assertEqual( + len(stk.analyse.get_symmetry(structure=Al)["translations"]), 96 + ) + self.assertEqual( + len(stk.analyse.get_symmetry(structure=Al)["translations"]), + len(stk.analyse.get_symmetry(structure=Al)["rotations"]), + ) + cell = 2.2 * np.identity(3) + Al = Atoms( + "AlAl", scaled_positions=[(0, 0, 0), (0.5, 0.5, 0.5)], cell=cell, pbc=True + ) + v = np.random.rand(6).reshape(-1, 3) + sym = stk.analyse.get_symmetry(structure=Al) + self.assertAlmostEqual( + np.linalg.norm(sym.symmetrize_vectors(v)), + 0, + ) + vv = np.random.rand(12).reshape(2, 2, 3) + for vvv in sym.symmetrize_vectors(vv): + self.assertAlmostEqual(np.linalg.norm(vvv), 0) + Al.positions[0, 0] += 0.01 + w = sym.symmetrize_vectors(v) + self.assertAlmostEqual( + np.absolute(w[:, 0]).sum(), np.linalg.norm(w, axis=-1).sum() + ) + self.assertAlmostEqual( + np.linalg.norm(sym.symmetrize_vectors(v) - sym.symmetrize_tensor(v)), 0 + ) + + def test_symmetrize_tensor(self): + structure = Atoms( + "AlAlAlAl", + positions=[(0, 0, 0), (0, 0.5, 0.5), (0.5, 0, 0.5), (0.5, 0.5, 0)], + cell=np.identity(3), + pbc=True, + ).repeat(2) + structure.symbols[0] = "Ni" + symmetry = stk.analyse.get_symmetry(structure=structure) + self.assertLess(np.ptp(symmetry.symmetrize_tensor(np.random.randn(3))), 1.0e-8) + sym_tensor = symmetry.symmetrize_tensor(np.random.randn(3, 3)) + self.assertLess(np.ptp(sym_tensor.diagonal()), 1.0e-8) + self.assertLess(np.ptp(sym_tensor[np.triu_indices(3, k=1)]), 1.0e-8) + i = np.all(structure.positions == [0.5, 0, 0.5], axis=-1) + j = np.all(structure.positions == [0, 0.5, 0.5], axis=-1) + s_tensor = symmetry.symmetrize_tensor(np.random.randn(len(structure))) + self.assertAlmostEqual(s_tensor[i][0], s_tensor[j][0]) + s_tensor = symmetry.symmetrize_tensor( + np.random.randn(4, len(structure), 3, len(structure), 3) + ) + self.assertEqual(s_tensor.shape, (4, len(structure), 3, len(structure), 3)) + s_tensor = symmetry.symmetrize_tensor( + np.random.randn(4, len(structure), 3, 3, len(structure)) + ) + self.assertEqual(s_tensor.shape, (4, len(structure), 3, 3, len(structure))) + structure_displaced = structure.copy() + structure_displaced.positions[0, 0] += 0.01 + sym = stk.analyse.get_symmetry(structure=structure_displaced) + tensor = np.zeros((len(structure_displaced), 3, len(structure_displaced), 3)) + tensor[0, 0, 0, 0] = 1 + self.assertAlmostEqual(sym.symmetrize_tensor(tensor)[0, 0, 0, 0], 1) + def test_get_symmetry_dataset(self): cell = 2.2 * np.identity(3) - Al_sc = Atoms("AlAl", scaled_positions=[(0, 0, 0), (0.5, 0.5, 0.5)], cell=cell) + Al_sc = Atoms( + "AlAl", scaled_positions=[(0, 0, 0), (0.5, 0.5, 0.5)], cell=cell, pbc=True + ) Al_sc = Al_sc.repeat([2, 2, 2]) self.assertEqual(stk.analyse.get_symmetry(structure=Al_sc).info["number"], 229) def test_get_ir_reciprocal_mesh(self): cell = 2.2 * np.identity(3) - Al_sc = Atoms("AlAl", scaled_positions=[(0, 0, 0), (0.5, 0.5, 0.5)], cell=cell) + Al_sc = Atoms( + "AlAl", scaled_positions=[(0, 0, 0), (0.5, 0.5, 0.5)], cell=cell, pbc=True + ) self.assertEqual( len( stk.analyse.get_symmetry(structure=Al_sc).get_ir_reciprocal_mesh( @@ -85,7 +220,24 @@ def test_get_ir_reciprocal_mesh(self): def test_get_primitive_cell(self): cell = 2.2 * np.identity(3) - basis = Atoms("AlFe", scaled_positions=[(0, 0, 0), (0.5, 0.5, 0.5)], cell=cell) + basis = Atoms( + "AlFe", scaled_positions=[(0, 0, 0), (0.5, 0.5, 0.5)], cell=cell, pbc=True + ) + structure = basis.repeat([2, 2, 2]) + sym = stk.analyse.get_symmetry(structure=structure) + self.assertEqual(len(basis), len(sym.get_primitive_cell(standardize=True))) + self.assertEqual( + stk.analyse.get_symmetry(structure=sym.get_primitive_cell()).spacegroup[ + "Number" + ], + 221, + ) + + def test_get_primitive_cell_functional(self): + cell = 2.2 * np.identity(3) + basis = Atoms( + "AlFe", scaled_positions=[(0, 0, 0), (0.5, 0.5, 0.5)], cell=cell, pbc=True + ) structure = basis.repeat([2, 2, 2]) self.assertEqual( len(basis), len(stk.analyse.get_primitive_cell(structure=structure)) @@ -97,6 +249,20 @@ def test_get_primitive_cell(self): 221, ) + def test_get_primitive_cell_standardize_fcc(self): + # primitive FCC cell has 1 atom; standardize=True should return the + # conventional cubic cell with 4 atoms + a_0 = 4.05 + structure = bulk("Al", crystalstructure="fcc", a=a_0) + self.assertEqual(len(structure), 1) + sym = stk.analyse.get_symmetry(structure=structure) + std = sym.get_primitive_cell(standardize=True) + self.assertEqual(len(std), 4) + # conventional cell should be approximately cubic + cell = std.get_cell() + lengths = np.linalg.norm(cell, axis=1) + self.assertTrue(np.allclose(lengths, a_0, atol=1e-3)) + def test_get_primitive_cell_hex(self): elements = ["Fe", "Fe", "Fe", "Fe", "O", "O", "O", "O", "O", "O"] positions = [ @@ -112,7 +278,30 @@ def test_get_primitive_cell_hex(self): [0.77, 1.57, 5.74], ] cell = [[2.519, 1.454, 4.590], [-2.519, 1.454, 4.590], [0.0, -2.909, 4.590]] - structure = Atoms(symbols=elements, positions=positions, cell=cell) + structure = Atoms(symbols=elements, positions=positions, cell=cell, pbc=True) + structure_repeat = structure.repeat([2, 2, 2]) + sym = stk.analyse.get_symmetry(structure=structure_repeat) + structure_prim_base = sym.get_primitive_cell() + self.assertEqual( + structure_prim_base.get_chemical_symbols(), structure.get_chemical_symbols() + ) + + def test_get_primitive_cell_hex_functional(self): + elements = ["Fe", "Fe", "Fe", "Fe", "O", "O", "O", "O", "O", "O"] + positions = [ + [0.0, 0.0, 4.89], + [0.0, 0.0, 11.78], + [0.0, 0.0, 1.99], + [0.0, 0.0, 8.87], + [-0.98, 1.45, 8.0], + [-1.74, -0.1, 5.74], + [-0.77, -1.57, 8.0], + [0.98, -1.45, 5.74], + [1.74, 0.12, 8.0], + [0.77, 1.57, 5.74], + ] + cell = [[2.519, 1.454, 4.590], [-2.519, 1.454, 4.590], [0.0, -2.909, 4.590]] + structure = Atoms(symbols=elements, positions=positions, cell=cell, pbc=True) structure_repeat = structure.repeat([2, 2, 2]) structure_prim_base = stk.analyse.get_primitive_cell(structure=structure_repeat) self.assertEqual( @@ -121,7 +310,24 @@ def test_get_primitive_cell_hex(self): def test_get_equivalent_points(self): basis = Atoms( - "FeFe", positions=[[0.01, 0, 0], [0.5, 0.5, 0.5]], cell=np.identity(3) + "FeFe", + positions=[[0.01, 0, 0], [0.5, 0.5, 0.5]], + cell=np.identity(3), + pbc=True, + ) + arr = stk.analyse.get_symmetry(structure=basis).generate_equivalent_points( + [0, 0, 0.5] + ) + self.assertAlmostEqual( + np.linalg.norm(arr - np.array([0.51, 0.5, 0]), axis=-1).min(), 0 + ) + + def test_get_equivalent_points_functional(self): + basis = Atoms( + "FeFe", + positions=[[0.01, 0, 0], [0.5, 0.5, 0.5]], + cell=np.identity(3), + pbc=True, ) arr = stk.analyse.get_equivalent_points( structure=basis, @@ -132,6 +338,124 @@ def test_get_equivalent_points(self): 0.7142128534267638, ) + def test_get_space_group(self): + cell = 2.2 * np.identity(3) + Al_sc = Atoms( + "AlAl", scaled_positions=[(0, 0, 0), (0.5, 0.5, 0.5)], cell=cell, pbc=True + ) + self.assertEqual( + stk.analyse.get_symmetry(structure=Al_sc).spacegroup[ + "InternationalTableSymbol" + ], + "Im-3m", + ) + self.assertEqual( + stk.analyse.get_symmetry(structure=Al_sc).spacegroup["Number"], 229 + ) + cell = 4.2 * (0.5 * np.ones((3, 3)) - 0.5 * np.eye(3)) + Al_fcc = Atoms("Al", scaled_positions=[(0, 0, 0)], cell=cell, pbc=True) + self.assertEqual( + stk.analyse.get_symmetry(structure=Al_fcc).spacegroup[ + "InternationalTableSymbol" + ], + "Fm-3m", + ) + self.assertEqual( + stk.analyse.get_symmetry(structure=Al_fcc).spacegroup["Number"], 225 + ) + a = 3.18 + c = 1.623 * a + cell = np.eye(3) + cell[0, 0] = a + cell[2, 2] = c + cell[1, 0] = -a / 2.0 + cell[1, 1] = np.sqrt(3) * a / 2.0 + pos = np.array([[0.0, 0.0, 0.0], [1.0 / 3.0, 2.0 / 3.0, 1.0 / 2.0]]) + Mg_hcp = Atoms("Mg2", scaled_positions=pos, cell=cell, pbc=True) + self.assertEqual( + stk.analyse.get_symmetry(structure=Mg_hcp).spacegroup["Number"], 194 + ) + cell = np.eye(3) + cell[0, 0] = a + cell[2, 2] = c + cell[1, 1] = np.sqrt(3) * a + pos = np.array( + [ + [0.0, 0.0, 0.0], + [0.5, 0.5, 0.0], + [0.5, 1 / 6, 0.5], + [0.0, 2 / 3, 0.5], + ] + ) + Mg_hcp = Atoms("Mg4", scaled_positions=pos, cell=cell, pbc=True) + self.assertEqual( + stk.analyse.get_symmetry(structure=Mg_hcp).spacegroup["Number"], 194 + ) + + def test_permutations(self): + structure = bulk("Al", cubic=True).repeat(2) + x_vacancy = structure.positions[0] + del structure[0] + neigh = stk.analyse.get_neighborhood(structure=structure, positions=x_vacancy) + vec = np.zeros_like(structure.positions) + vec[neigh.indices[0]] = neigh.vecs[0] + sym = stk.analyse.get_symmetry(structure=structure) + all_vectors = np.einsum("ijk,ink->inj", sym.rotations, vec[sym.permutations]) + for i, v in zip(neigh.indices, neigh.vecs, strict=True): + vec = np.zeros_like(structure.positions) + vec[i] = v + self.assertAlmostEqual( + np.linalg.norm(all_vectors - vec, axis=(-1, -2)).min(), + 0, + ) + + def test_arg_equivalent_vectors(self): + structure = bulk("Al", cubic=True).repeat(2) + self.assertEqual( + np.unique( + stk.analyse.get_symmetry(structure=structure).arg_equivalent_vectors + ).squeeze(), + 0, + ) + x_v = structure.positions[0] + del structure[0] + arg_v = stk.analyse.get_symmetry(structure=structure).arg_equivalent_vectors + dx = stk.analyse.get_distances_array( + structure=structure, p1=structure.positions, p2=x_v, vectors=True + ) + dx_round = np.round(np.absolute(dx), decimals=3) + self.assertEqual(len(np.unique(dx_round + arg_v)), len(np.unique(arg_v))) + + def test_get_primitive_cell_pbc_error(self): + structure = bulk("Al", cubic=True) + structure.pbc = [True, True, False] + sym = stk.analyse.get_symmetry(structure=structure) + with self.assertRaisesRegex(ValueError, "Can only symmetrize periodic structures."): + sym.get_primitive_cell() + + def test_get_primitive_cell_arrays_warning(self): + structure = bulk("Al", cubic=True) + structure.set_array("test_array", np.zeros(len(structure))) + sym = stk.analyse.get_symmetry(structure=structure) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + sym.get_primitive_cell() + self.assertTrue( + any( + "Custom arrays {'test_array'} do not carry over to new structure!" + in str(warning.message) + for warning in w + ) + ) + + def test_error(self): + """spglib errors should be wrapped in a SymmetryError.""" + + structure = bulk("Al") + structure += structure[-1] + with self.assertRaises((stk.common.SymmetryError, SpglibError)): + stk.analyse.get_symmetry(structure=structure) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_symmetry.py b/tests/test_symmetry.py deleted file mode 100644 index e32ae6aff..000000000 --- a/tests/test_symmetry.py +++ /dev/null @@ -1,324 +0,0 @@ -# coding: utf-8 -# Copyright (c) Max-Planck-Institut für Eisenforschung GmbH - Computational Materials Design (CM) Department -# Distributed under the terms of "New BSD License", see the LICENSE file. - -import unittest - -import numpy as np -from ase.atoms import Atoms -from ase.build import bulk - -import structuretoolkit as stk - -try: - import pyscal - - skip_pyscal_test = False -except ImportError: - skip_pyscal_test = True - - -try: - import spglib - from spglib.error import SpglibError - - skip_spglib_test = False -except ImportError: - skip_spglib_test = True - - -@unittest.skipIf( - skip_spglib_test, "spglib is not installed, so the spglib tests are skipped." -) -class TestSymmetry(unittest.TestCase): - def test_get_arg_equivalent_sites(self): - a_0 = 4.0 - structure = bulk("Al", cubic=True, a=a_0).repeat(2) - sites = stk.common.get_wrapped_coordinates( - structure=structure, - positions=structure.positions + np.array([0, 0, 0.5 * a_0]), - ) - v_position = structure.positions[0] - del structure[0] - pairs = np.stack( - ( - stk.analyse.get_symmetry(structure=structure).get_arg_equivalent_sites( - sites - ), - np.unique( - np.round( - stk.analyse.get_distances_array( - structure=structure, p1=v_position, p2=sites - ), - decimals=2, - ), - return_inverse=True, - )[1], - ), - axis=-1, - ) - unique_pairs = np.unique(pairs, axis=0) - self.assertEqual(len(unique_pairs), len(np.unique(unique_pairs[:, 0]))) - with self.assertRaises(ValueError): - stk.analyse.get_symmetry(structure=structure).get_arg_equivalent_sites( - [0, 0, 0] - ) - - def test_generate_equivalent_points(self): - a_0 = 4 - structure = bulk("Al", cubic=True, a=a_0) - sym = stk.analyse.get_symmetry(structure) - self.assertEqual( - len(structure), len(sym.generate_equivalent_points([0, 0, 0.5 * a_0])) - ) - x = np.array([[0, 0, 0.5 * a_0], 3 * [0.25 * a_0]]) - y = np.random.randn(2) - sym_x = sym.generate_equivalent_points(x, return_unique=False) - y = np.tile(y, len(sym_x)) - sym_x = sym_x.reshape(-1, 3) - xy = np.round( - [ - stk.analyse.get_neighborhood( - structure, sym_x, num_neighbors=1 - ).distances.flatten(), - y, - ], - decimals=8, - ) - self.assertEqual( - np.unique(xy, axis=1).shape, - (2, 2), - msg="order of generated points does not match the original order", - ) - - def test_get_symmetry(self): - cell = 2.2 * np.identity(3) - Al = Atoms( - "AlAl", positions=[(0, 0, 0), (0.5, 0.5, 0.5)], cell=cell, pbc=True - ).repeat(2) - self.assertEqual( - len(set(stk.analyse.get_symmetry(structure=Al)["equivalent_atoms"])), 1 - ) - self.assertEqual( - len(stk.analyse.get_symmetry(structure=Al)["translations"]), 96 - ) - self.assertEqual( - len(stk.analyse.get_symmetry(structure=Al)["translations"]), - len(stk.analyse.get_symmetry(structure=Al)["rotations"]), - ) - cell = 2.2 * np.identity(3) - Al = Atoms( - "AlAl", scaled_positions=[(0, 0, 0), (0.5, 0.5, 0.5)], cell=cell, pbc=True - ) - v = np.random.rand(6).reshape(-1, 3) - sym = stk.analyse.get_symmetry(structure=Al) - self.assertAlmostEqual( - np.linalg.norm(sym.symmetrize_vectors(v)), - 0, - ) - vv = np.random.rand(12).reshape(2, 2, 3) - for vvv in sym.symmetrize_vectors(vv): - self.assertAlmostEqual(np.linalg.norm(vvv), 0) - Al.positions[0, 0] += 0.01 - w = sym.symmetrize_vectors(v) - self.assertAlmostEqual( - np.absolute(w[:, 0]).sum(), np.linalg.norm(w, axis=-1).sum() - ) - self.assertAlmostEqual( - np.linalg.norm(sym.symmetrize_vectors(v) - sym.symmetrize_tensor(v)), 0 - ) - - def test_symmetrize_tensor(self): - structure = Atoms( - "AlAlAlAl", - positions=[(0, 0, 0), (0, 0.5, 0.5), (0.5, 0, 0.5), (0.5, 0.5, 0)], - cell=np.identity(3), - pbc=True, - ).repeat(2) - structure.symbols[0] = "Ni" - symmetry = stk.analyse.get_symmetry(structure=structure) - self.assertLess(np.ptp(symmetry.symmetrize_tensor(np.random.randn(3))), 1.0e-8) - sym_tensor = symmetry.symmetrize_tensor(np.random.randn(3, 3)) - self.assertLess(np.ptp(sym_tensor.diagonal()), 1.0e-8) - self.assertLess(np.ptp(sym_tensor[np.triu_indices(3, k=1)]), 1.0e-8) - i = np.all(structure.positions == [0.5, 0, 0.5], axis=-1) - j = np.all(structure.positions == [0, 0.5, 0.5], axis=-1) - s_tensor = symmetry.symmetrize_tensor(np.random.randn(len(structure))) - self.assertAlmostEqual(s_tensor[i][0], s_tensor[j][0]) - s_tensor = symmetry.symmetrize_tensor( - np.random.randn(4, len(structure), 3, len(structure), 3) - ) - self.assertEqual(s_tensor.shape, (4, len(structure), 3, len(structure), 3)) - s_tensor = symmetry.symmetrize_tensor( - np.random.randn(4, len(structure), 3, 3, len(structure)) - ) - self.assertEqual(s_tensor.shape, (4, len(structure), 3, 3, len(structure))) - structure_displaced = structure.copy() - structure_displaced.positions[0, 0] += 0.01 - sym = stk.analyse.get_symmetry(structure=structure_displaced) - tensor = np.zeros((len(structure_displaced), 3, len(structure_displaced), 3)) - tensor[0, 0, 0, 0] = 1 - self.assertAlmostEqual(sym.symmetrize_tensor(tensor)[0, 0, 0, 0], 1) - - def test_get_symmetry_dataset(self): - cell = 2.2 * np.identity(3) - Al_sc = Atoms("AlAl", scaled_positions=[(0, 0, 0), (0.5, 0.5, 0.5)], cell=cell) - Al_sc = Al_sc.repeat([2, 2, 2]) - self.assertEqual(stk.analyse.get_symmetry(structure=Al_sc).info["number"], 229) - - def test_get_ir_reciprocal_mesh(self): - cell = 2.2 * np.identity(3) - Al_sc = Atoms("AlAl", scaled_positions=[(0, 0, 0), (0.5, 0.5, 0.5)], cell=cell) - self.assertEqual( - len( - stk.analyse.get_symmetry(structure=Al_sc).get_ir_reciprocal_mesh( - [3, 3, 3] - )[0] - ), - 27, - ) - - def test_get_primitive_cell(self): - cell = 2.2 * np.identity(3) - basis = Atoms("AlFe", scaled_positions=[(0, 0, 0), (0.5, 0.5, 0.5)], cell=cell) - structure = basis.repeat([2, 2, 2]) - sym = stk.analyse.get_symmetry(structure=structure) - self.assertEqual(len(basis), len(sym.get_primitive_cell(standardize=True))) - self.assertEqual( - stk.analyse.get_symmetry(structure=sym.get_primitive_cell()).spacegroup[ - "Number" - ], - 221, - ) - - def test_get_primitive_cell_hex(self): - elements = ["Fe", "Fe", "Fe", "Fe", "O", "O", "O", "O", "O", "O"] - positions = [ - [0.0, 0.0, 4.89], - [0.0, 0.0, 11.78], - [0.0, 0.0, 1.99], - [0.0, 0.0, 8.87], - [-0.98, 1.45, 8.0], - [-1.74, -0.1, 5.74], - [-0.77, -1.57, 8.0], - [0.98, -1.45, 5.74], - [1.74, 0.12, 8.0], - [0.77, 1.57, 5.74], - ] - cell = [[2.519, 1.454, 4.590], [-2.519, 1.454, 4.590], [0.0, -2.909, 4.590]] - structure = Atoms(symbols=elements, positions=positions, cell=cell) - structure_repeat = structure.repeat([2, 2, 2]) - sym = stk.analyse.get_symmetry(structure=structure_repeat) - structure_prim_base = sym.get_primitive_cell() - self.assertEqual( - structure_prim_base.get_chemical_symbols(), structure.get_chemical_symbols() - ) - - def test_get_equivalent_points(self): - basis = Atoms( - "FeFe", positions=[[0.01, 0, 0], [0.5, 0.5, 0.5]], cell=np.identity(3) - ) - arr = stk.analyse.get_symmetry(structure=basis).generate_equivalent_points( - [0, 0, 0.5] - ) - self.assertAlmostEqual( - np.linalg.norm(arr - np.array([0.51, 0.5, 0]), axis=-1).min(), 0 - ) - - def test_get_space_group(self): - cell = 2.2 * np.identity(3) - Al_sc = Atoms("AlAl", scaled_positions=[(0, 0, 0), (0.5, 0.5, 0.5)], cell=cell) - self.assertEqual( - stk.analyse.get_symmetry(structure=Al_sc).spacegroup[ - "InternationalTableSymbol" - ], - "Im-3m", - ) - self.assertEqual( - stk.analyse.get_symmetry(structure=Al_sc).spacegroup["Number"], 229 - ) - cell = 4.2 * (0.5 * np.ones((3, 3)) - 0.5 * np.eye(3)) - Al_fcc = Atoms("Al", scaled_positions=[(0, 0, 0)], cell=cell) - self.assertEqual( - stk.analyse.get_symmetry(structure=Al_fcc).spacegroup[ - "InternationalTableSymbol" - ], - "Fm-3m", - ) - self.assertEqual( - stk.analyse.get_symmetry(structure=Al_fcc).spacegroup["Number"], 225 - ) - a = 3.18 - c = 1.623 * a - cell = np.eye(3) - cell[0, 0] = a - cell[2, 2] = c - cell[1, 0] = -a / 2.0 - cell[1, 1] = np.sqrt(3) * a / 2.0 - pos = np.array([[0.0, 0.0, 0.0], [1.0 / 3.0, 2.0 / 3.0, 1.0 / 2.0]]) - Mg_hcp = Atoms("Mg2", scaled_positions=pos, cell=cell) - self.assertEqual( - stk.analyse.get_symmetry(structure=Mg_hcp).spacegroup["Number"], 194 - ) - cell = np.eye(3) - cell[0, 0] = a - cell[2, 2] = c - cell[1, 1] = np.sqrt(3) * a - pos = np.array( - [ - [0.0, 0.0, 0.0], - [0.5, 0.5, 0.0], - [0.5, 1 / 6, 0.5], - [0.0, 2 / 3, 0.5], - ] - ) - Mg_hcp = Atoms("Mg4", scaled_positions=pos, cell=cell) - self.assertEqual( - stk.analyse.get_symmetry(structure=Mg_hcp).spacegroup["Number"], 194 - ) - - def test_permutations(self): - structure = bulk("Al", cubic=True).repeat(2) - x_vacancy = structure.positions[0] - del structure[0] - neigh = stk.analyse.get_neighborhood(structure=structure, positions=x_vacancy) - vec = np.zeros_like(structure.positions) - vec[neigh.indices[0]] = neigh.vecs[0] - sym = stk.analyse.get_symmetry(structure=structure) - all_vectors = np.einsum("ijk,ink->inj", sym.rotations, vec[sym.permutations]) - for i, v in zip(neigh.indices, neigh.vecs, strict=True): - vec = np.zeros_like(structure.positions) - vec[i] = v - self.assertAlmostEqual( - np.linalg.norm(all_vectors - vec, axis=(-1, -2)).min(), - 0, - ) - - def test_arg_equivalent_vectors(self): - structure = bulk("Al", cubic=True).repeat(2) - self.assertEqual( - np.unique( - stk.analyse.get_symmetry(structure=structure).arg_equivalent_vectors - ).squeeze(), - 0, - ) - x_v = structure.positions[0] - del structure[0] - arg_v = stk.analyse.get_symmetry(structure=structure).arg_equivalent_vectors - dx = stk.analyse.get_distances_array( - structure=structure, p1=structure.positions, p2=x_v, vectors=True - ) - dx_round = np.round(np.absolute(dx), decimals=3) - self.assertEqual(len(np.unique(dx_round + arg_v)), len(np.unique(arg_v))) - - def test_error(self): - """spglib errors should be wrapped in a SymmetryError.""" - - structure = bulk("Al") - structure += structure[-1] - with self.assertRaises((stk.common.SymmetryError, SpglibError)): - stk.analyse.get_symmetry(structure=structure) - - -if __name__ == "__main__": - unittest.main()