├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── evgraf ├── __init__.py ├── crystal_comparator.py ├── crystal_reducer.py ├── crystal_reduction.py ├── inversion_symmetry.py ├── subgroup_enumeration.py └── utils │ ├── __init__.py │ ├── axis_permutation.py │ ├── minkowski_reduction.py │ ├── pbc.py │ ├── rotation_matrix.py │ └── standardization.py ├── setup.py ├── src ├── crystalline.cpp ├── crystalline.h ├── evgrafcpp_module.cpp ├── lup_decomposition.cpp ├── lup_decomposition.h ├── matrix_vector.cpp ├── matrix_vector.h ├── rectangular_lsap.cpp ├── rectangular_lsap.h ├── wrap_positions.cpp └── wrap_positions.h └── tests ├── crystal_reduction.py ├── inversion_symmetry.py ├── minkowski_reduce.py ├── pbc.py ├── permute_axes.py ├── standardization.py ├── standardize_cell.py ├── subgroup_enumeration.py └── wrap_positions.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | include: 3 | - language: python 4 | python: 5 | - "3.6" # current default Python on Travis CI 6 | # command to install dependencies 7 | install: 8 | - export BRANCH=$(if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then echo $TRAVIS_BRANCH; else echo $TRAVIS_PULL_REQUEST_BRANCH; fi) 9 | - python3 -m pip install git+https://github.com/pmla/evgraf@$BRANCH 10 | - python3 -m pip install scipy 11 | # command to run tests 12 | script: 13 | - python3 -m pytest tests/* 14 | 15 | - language: sh 16 | os: osx 17 | env: 18 | - TOXENV=py3 19 | - HOMEBREW_NO_INSTALL_CLEANUP=1 20 | - HOMEBREW_NO_ANALYTICS=1 21 | before_cache: 22 | # - brew cleanup 23 | - rm -f "$HOME/Library/Caches/pip/log/debug.log" 24 | cache: 25 | directories: 26 | # - "$HOME/Library/Caches/Homebrew" 27 | - "$HOME/Library/Caches/pip" 28 | addons: 29 | homebrew: 30 | # update: true 31 | packages: python3 32 | before_install: 33 | - python3 -m pip install scipy 34 | - python3 -m pip install --upgrade virtualenv 35 | - virtualenv -p python3 --system-site-packages "$HOME/venv" 36 | - source "$HOME/venv/bin/activate" 37 | - python3 -m pip install --upgrade pytest 38 | # command to install dependencies 39 | install: 40 | - export BRANCH=$(if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then echo $TRAVIS_BRANCH; else echo $TRAVIS_PULL_REQUEST_BRANCH; fi) 41 | - python3 -m pip install git+https://github.com/pmla/evgraf@$BRANCH 42 | # command to run tests 43 | script: 44 | - python3 -m pytest tests/* 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 P. M. Larsen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | include setup.py 4 | recursive-include evgraf *.py 5 | recursive-include src *.cpp .*h 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # evgraf 2 | Symmetrization of crystal structures 3 | 4 | 5 | Currently supports translational and inversion symmetries only. 6 | 7 | 8 | ### Installation: 9 | 10 | To install the module with pip (recommended): 11 | ``` 12 | pip install --user evgraf 13 | ``` 14 | 15 | To install directly from the git repository: 16 | ``` 17 | pip install --user git+https://github.com/pmla/evgraf 18 | ``` 19 | 20 | To do a manual build and installation: 21 | ``` 22 | python3 setup.py build 23 | python3 setup.py install --user 24 | ``` 25 | 26 | ### Usage: 27 | We can quantify the breaking of inversion symmetry in BaTiO3. 28 | First we create the crystal structure: 29 | ``` 30 | >>> from ase import Atoms 31 | >>> atoms = Atoms(symbols='BaTiO3', pbc=True, cell=[4.002, 4.002, 4.216], 32 | ... positions=[[0.000, 0.000, 0.085], 33 | ... [2.001, 2.001, 2.272], 34 | ... [2.001, 2.001, 4.092], 35 | ... [0.000, 2.001, 2.074], 36 | ... [2.001, 0.000, 2.074]]) 37 | ``` 38 | We call the inversion symmetry analysis function: 39 | ``` 40 | >>> from evgraf import find_inversion_symmetry 41 | >>> result = find_inversion_symmetry(atoms) 42 | ``` 43 | We can view the inversion-symmetrized structure: 44 | ``` 45 | >>> from ase.visualize import view 46 | >>> view(result.atoms) 47 | ``` 48 | The degree of symmetry breaking is given by the root-mean-square distance between the input structure and the symmetrized structure: 49 | ``` 50 | >>> result.rmsd 51 | 0.10115255804971046 52 | ``` 53 | We also obtain the inversion axis: 54 | ``` 55 | >>> result.axis 56 | array([2.001 , 2.001 , 2.1194]) 57 | ``` 58 | 59 | 60 | ### Information 61 | `evgraf` is written by Peter M. Larsen. The software is provided under the MIT license. 62 | -------------------------------------------------------------------------------- /evgraf/__init__.py: -------------------------------------------------------------------------------- 1 | from .crystal_reduction import find_crystal_reductions 2 | from .inversion_symmetry import find_inversion_symmetry 3 | 4 | __all__ = ['find_crystal_reductions', 'find_inversion_symmetry'] 5 | -------------------------------------------------------------------------------- /evgraf/crystal_comparator.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import numpy as np 3 | from evgrafcpp import wrap_positions, calculate_rmsd 4 | from evgraf.utils import standardize 5 | 6 | 7 | class CrystalComparator: 8 | 9 | def __init__(self, _atoms, subtract_barycenter=False): 10 | std = standardize(_atoms, subtract_barycenter) 11 | self.atoms = std.atoms 12 | self.op = std.op 13 | self.invop = std.invop 14 | self.zpermutation = std.zpermutation 15 | self.dim = sum(self.atoms.pbc) 16 | self.positions = wrap_positions(self.atoms.get_positions(), 17 | self.atoms.cell, self.atoms.pbc) 18 | 19 | self.nbr_cells = self._get_neighboring_cells() 20 | self.offsets = self.nbr_cells @ self.atoms.cell 21 | if subtract_barycenter: 22 | self.barycenter = std.barycenter 23 | 24 | def _get_neighboring_cells(self): 25 | pbc = self.atoms.pbc.astype(int) 26 | return np.array(list(itertools.product(*[range(-p, p + 1) 27 | for p in pbc]))) 28 | 29 | def expand_coordinates(self, c): 30 | """Expands a 1D or 2D coordinate to 3D by filling in zeros where 31 | pbc=False. For input c=(1, 3) and pbc=[True, False, True] this returns 32 | array([1, 0, 3])""" 33 | count = 0 34 | expanded = [] 35 | for i in range(3): 36 | if self.atoms.pbc[i]: 37 | expanded.append(c[count]) 38 | count += 1 39 | else: 40 | expanded.append(0) 41 | return np.array(expanded) 42 | 43 | def calculate_rmsd(self, positions): 44 | positions = wrap_positions(positions, self.atoms.cell, self.atoms.pbc) 45 | return calculate_rmsd(positions, self.positions, self.offsets, 46 | self.atoms.numbers.astype(np.int32)) 47 | -------------------------------------------------------------------------------- /evgraf/crystal_reducer.py: -------------------------------------------------------------------------------- 1 | import math 2 | import functools 3 | import numpy as np 4 | from .crystal_comparator import CrystalComparator 5 | from .subgroup_enumeration import (enumerate_subgroup_bases, 6 | get_subgroup_elements) 7 | 8 | 9 | def reduce_gcd(x): 10 | return functools.reduce(math.gcd, x) 11 | 12 | 13 | class CrystalReducer: 14 | 15 | def __init__(self, atoms): 16 | self.distances = {} 17 | self.permutations = {} 18 | self.n = reduce_gcd(atoms.symbols.formula.count().values()) 19 | self.comparator = CrystalComparator(atoms) 20 | 21 | def get_point(self, c): 22 | """Calculates the minimum-cost permutation at a desired translation. 23 | The translation is specified by `c` which describes the coordinates of 24 | the subgroup element.""" 25 | key = tuple(c) 26 | if key in self.permutations: 27 | return self.distances[key], self.permutations[key] 28 | 29 | comparator = self.comparator 30 | c = comparator.expand_coordinates(c) 31 | positions = comparator.positions + c @ comparator.atoms.cell / self.n 32 | rmsd, permutation = self.comparator.calculate_rmsd(positions) 33 | 34 | self.distances[key] = rmsd 35 | self.permutations[key] = permutation 36 | return rmsd, permutation 37 | 38 | def is_consistent(self, H): 39 | """Callback function which tests whether the minimum-cost permutations 40 | of the subgroup elements are consistent with each other. H describes 41 | the subgroup basis.""" 42 | n = self.n 43 | dims = [n] * self.comparator.dim 44 | num_atoms = len(self.comparator.atoms) 45 | 46 | seen = -np.ones((3, num_atoms), dtype=int) 47 | elements = get_subgroup_elements(dims, H) 48 | 49 | for c1 in elements: 50 | _, perm1 = self.get_point(c1) 51 | invperm1 = np.argsort(perm1) 52 | for i, c2 in enumerate((c1 + H) % n): 53 | _, perm2 = self.get_point(c2) 54 | val = perm2[invperm1] 55 | if seen[i][0] == -1: 56 | seen[i] = val 57 | elif (seen[i] != val).any(): 58 | return False 59 | return True 60 | 61 | def find_consistent_reductions(self): 62 | n = self.n 63 | dims = [n] * self.comparator.dim 64 | for H in enumerate_subgroup_bases(dims, self.is_consistent, 65 | min_index=1, max_index=n): 66 | group_index = np.prod(dims // np.diag(H)) 67 | elements = get_subgroup_elements(dims, H) 68 | distances = np.array([self.distances[tuple(c)] for c in elements]) 69 | permutations = np.array([self.permutations[tuple(c)] 70 | for c in elements]) 71 | rmsd = np.sqrt(np.sum(distances**2) / (2 * group_index)) 72 | yield (rmsd, group_index, H, permutations) 73 | -------------------------------------------------------------------------------- /evgraf/crystal_reduction.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from collections import namedtuple 3 | from ase import Atoms 4 | from ase.geometry import find_mic 5 | from ase.geometry.dimensionality.disjoint_set import DisjointSet 6 | from .crystal_reducer import CrystalReducer 7 | 8 | 9 | Reduced = namedtuple('ReducedCrystal', 'rmsd factor atoms components map') 10 | 11 | 12 | def assign_atoms_to_clusters(num_atoms, permutations): 13 | uf = DisjointSet(num_atoms) 14 | for p in permutations: 15 | for i, e in enumerate(p): 16 | uf.union(i, e) 17 | return uf.find_all(relabel=True) 18 | 19 | 20 | def reduction_basis(n, H, pbc): 21 | dim = sum(pbc) 22 | # Extend the subgroup basis to 3D (if not already) 23 | R = np.diag([n, n, n]) 24 | indices = np.where(pbc)[0] 25 | for i in range(dim): 26 | for j in range(dim): 27 | R[indices[i], indices[j]] = H[i, j] 28 | return R / n 29 | 30 | 31 | def reduced_layout(reducer, rmsd, group_index, R, permutations, atoms): 32 | 33 | num_atoms = len(reducer.comparator.atoms) 34 | components = assign_atoms_to_clusters(num_atoms, permutations) 35 | if num_atoms // group_index != len(np.bincount(components)): 36 | return None 37 | 38 | if len(np.unique(np.bincount(components))) > 1: 39 | return None 40 | 41 | # Collect atoms in contracted unit cell 42 | indices = np.argsort(components) 43 | collected = reducer.comparator.atoms[indices] 44 | collected.set_cell(R @ atoms.cell, scale_atoms=False) 45 | collected.wrap(eps=0) 46 | 47 | clusters = components[indices] 48 | ps = collected.get_positions() 49 | parents = clusters * group_index 50 | vmin, _ = find_mic(ps - ps[parents], collected.cell, pbc=collected.pbc) 51 | positions = ps[parents] + vmin 52 | 53 | m = num_atoms // group_index 54 | numbers = collected.numbers.reshape((m, group_index))[:, 0] 55 | meanpos = np.mean(positions.reshape((m, group_index, 3)), axis=1) 56 | deltas = positions - meanpos[clusters] 57 | rmsd_check = np.sqrt(np.sum(deltas**2) / num_atoms) 58 | if abs(rmsd - rmsd_check) > 1E-12: 59 | return None 60 | 61 | reduced = Atoms(positions=meanpos, numbers=numbers, 62 | cell=collected.cell, pbc=collected.pbc) 63 | reduced.wrap(eps=0) 64 | return reduced, components 65 | 66 | 67 | def find_crystal_reductions(atoms): 68 | """Finds reductions of a crystal using the root-mean-square (RMS) distance. 69 | 70 | A crystal reduction is defined by a translational symmetry in the input 71 | structure. Each translational symmetry has an associated cost, which is the 72 | RMS distance from the input structure to its symmetrized (i.e. reduced) 73 | structure. The atomic coordinates in the reduced crystal are given by the 74 | Euclidean average of the those in the input structure (after being wrapped 75 | into the reduced unit cell). 76 | 77 | If the crystal structure is perfect, the reduced crystal is the textbook 78 | primitive unit cell and has a RMS distance of zero. As the atomic 79 | coordinates in the input structure deviate from perfect translational 80 | symmetry the RMS distance increases correspondingly. Similarly, the RMS 81 | distance cannot decrease as the reduction factor increases. 82 | 83 | See the tutorial for an example with illustrations. 84 | 85 | Parameters: 86 | 87 | atoms: ASE atoms object 88 | The system to reduce. 89 | 90 | Returns: 91 | 92 | reduced: list 93 | List of ReducedCrystal objects for reduction found. A ReducedCrystal 94 | is a namedtuple with the following field names: 95 | 96 | rmsd: float 97 | RMS distance from input structure to reduced structure 98 | factor: integer 99 | The reduction factor 100 | atoms: Atoms object 101 | The reduced structure 102 | components: integer ndarray 103 | Describes how atoms in the input structure are combined in the 104 | reduced structure 105 | map: ndarray 106 | Map from input cell to reduced cell 107 | """ 108 | reducer = CrystalReducer(atoms) 109 | reductions = reducer.find_consistent_reductions() 110 | invzperm = np.argsort(reducer.comparator.zpermutation) 111 | 112 | reduced = {} 113 | for rmsd, group_index, H, permutations in reductions: 114 | R = reduction_basis(reducer.n, H, atoms.pbc) 115 | R = reducer.comparator.invop @ R @ reducer.comparator.op 116 | result = reduced_layout(reducer, rmsd, group_index, R, permutations, 117 | atoms) 118 | if result is not None: 119 | reduced_atoms, components = result 120 | key = group_index 121 | entry = Reduced(rmsd=rmsd, factor=group_index, atoms=reduced_atoms, 122 | components=components[invzperm], 123 | map=R) 124 | if key not in reduced: 125 | reduced[key] = entry 126 | else: 127 | reduced[key] = min(reduced[key], entry, key=lambda x: x.rmsd) 128 | 129 | return sorted(reduced.values(), key=lambda x: x.factor) 130 | -------------------------------------------------------------------------------- /evgraf/inversion_symmetry.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import numpy as np 3 | from collections import namedtuple 4 | from ase import Atoms 5 | from ase.geometry import find_mic 6 | from .crystal_comparator import CrystalComparator 7 | 8 | 9 | InversionSymmetry = namedtuple('InversionSymmetry', 10 | 'rmsd axis atoms permutation') 11 | 12 | 13 | class CrystalInverter: 14 | 15 | def __init__(self, atoms): 16 | self.n = len(atoms) 17 | self.comparator = CrystalComparator(atoms, subtract_barycenter=True) 18 | 19 | def get_point(self, c): 20 | """Calculates the minimum-cost permutation at a desired translation. 21 | The translation is specified by `c` which describes the coordinates of 22 | the subgroup element.""" 23 | c = self.comparator.expand_coordinates(c) 24 | shift = c / self.n @ self.comparator.atoms.cell 25 | positions = -self.comparator.positions + shift 26 | return self.comparator.calculate_rmsd(positions) 27 | 28 | 29 | def find_inversion_axis(inverter): 30 | n = inverter.n 31 | r = list(range(n)) 32 | dim = inverter.comparator.dim 33 | best = (float("inf"), None, None) 34 | for c in itertools.product(*([r] * dim)): 35 | rmsd, permutation = inverter.get_point(c) 36 | best = min(best, (rmsd, permutation, c), key=lambda x: x[0]) 37 | rmsd, permutation, c = best 38 | c = inverter.comparator.expand_coordinates(c) 39 | return rmsd, permutation, c 40 | 41 | 42 | def symmetrized_layout(rmsd, atoms, inverted): 43 | ps = atoms.get_positions() 44 | v, _ = find_mic(inverted.get_positions() - ps, atoms.cell) 45 | meanpos = ps + v / 2 46 | component_rmsd = np.sqrt(np.sum((ps - meanpos)**2) / len(atoms)) 47 | assert abs(rmsd - component_rmsd) < 1E-12 48 | 49 | symmetrized = Atoms(positions=meanpos, numbers=atoms.numbers, 50 | cell=atoms.cell, pbc=atoms.pbc) 51 | symmetrized.set_cell(symmetrized.cell, scale_atoms=False) 52 | symmetrized.wrap(eps=0) 53 | return symmetrized 54 | 55 | 56 | def find_inversion_symmetry(atoms): 57 | """Finds and quantifies inversion symmetry in a crystal using the 58 | root-mean-square (RMS) distance. 59 | 60 | The function finds the inversion axis and permutation which minimizes the 61 | RMS distance from the input structure to the symmetrized structure. 62 | 63 | Parameters: 64 | 65 | atoms: ASE atoms object 66 | The system to analyze. 67 | 68 | Returns: 69 | 70 | inversion: namedtuple with the following field names: 71 | rmsd: float 72 | RMS distance from input structure to reduced structure 73 | axis: ndarray of shape (3,) 74 | The inversion axis 75 | atoms: Atoms object 76 | The symmetrized structure 77 | permutation: integer ndarray 78 | Describes how atoms are paired to create the symmetrized structure 79 | """ 80 | n = len(atoms) 81 | inverter = CrystalInverter(atoms) 82 | rmsd, permutation, c = find_inversion_axis(inverter) 83 | 84 | comparator = inverter.comparator 85 | permutation = permutation[np.argsort(comparator.zpermutation)] 86 | permutation = comparator.zpermutation[permutation] 87 | 88 | axis = (c / n) @ comparator.atoms.cell / 2 + comparator.barycenter 89 | inverted = atoms[permutation] 90 | inverted.positions = -inverted.positions + 2 * axis 91 | inverted.wrap(eps=0) 92 | assert (inverted.numbers == atoms.numbers).all() 93 | 94 | symmetrized = symmetrized_layout(rmsd / 2, atoms, inverted) 95 | return InversionSymmetry(rmsd=rmsd / 2, axis=axis, atoms=symmetrized, 96 | permutation=permutation) 97 | -------------------------------------------------------------------------------- /evgraf/subgroup_enumeration.py: -------------------------------------------------------------------------------- 1 | # Subgroup enumeration for cyclic, dicyclic, and tricyclic integer groups. 2 | # PM Larsen, 2019 3 | # 4 | # The theory implemented here is described for two-dimensional groups in: 5 | # Representing and counting the subgroups of the group Z_m x Z_n 6 | # Mario Hampejs, Nicki Holighaus, László Tóth, and Christoph Wiesmeyr 7 | # Journal of Numbers, vol. 2014, Article ID 491428 8 | # http://dx.doi.org./10.1155/2014/491428 9 | # https://arxiv.org/abs/1211.1797 10 | # 11 | # and for three-dimensional groups in: 12 | # On the subgroups of finite Abelian groups of rank three 13 | # Mario Hampejs and László Tóth 14 | # Annales Univ. Sci. Budapest., Sect. Comp. 39 (2013), 111–124 15 | # https://arxiv.org/abs/1304.2961 16 | 17 | import itertools 18 | import numpy as np 19 | from math import gcd 20 | 21 | 22 | def get_divisors(n): 23 | return [i for i in range(1, n + 1) if n % i == 0] 24 | 25 | 26 | def get_subgroup_elements(orders, H): 27 | 28 | size = 1 29 | for e, x in zip(np.diag(H), orders): 30 | if e != 0: 31 | size *= x // e 32 | 33 | dimension = len(orders) 34 | indices = np.zeros((size, dimension), dtype=int) 35 | indices[:, 0] = H[0, 0] * np.arange(size) 36 | 37 | for i, order in enumerate(orders): 38 | if i > 0 and H[i, i] != 0: 39 | k = np.prod(orders[:i]) // np.prod(np.diag(H)[:i]) 40 | p = np.arange(size) // k 41 | for j in range(i + 1): 42 | indices[:, j] += H[i, j] * p 43 | 44 | return indices % orders 45 | 46 | 47 | def consistent_first_rows(dimension, dm, ffilter): 48 | for a in dm: 49 | H = np.zeros((dimension, dimension), dtype=int) 50 | H[0, 0] = a 51 | if ffilter is None or ffilter(H): 52 | yield a 53 | 54 | 55 | def solve_linear_congruence(r, a, b, c, s, v): 56 | for u in range(a + 1): 57 | if (r // c * u) % a == (r * v * s // (b * c)) % a: 58 | return u 59 | raise Exception("u not found") 60 | 61 | 62 | def enumerate_subgroup_bases(orders, ffilter=None, 63 | min_index=1, max_index=float("inf")): 64 | """Get the subgroup bases of a cyclic/dicyclic/tricyclic integer group. 65 | 66 | Parameters: 67 | 68 | orders: list-like integer object 69 | Orders of the constituent groups. 70 | [m] if the group is a cyclic group Zm 71 | [m, n] if the group is a dicyclic group Zm x Zn 72 | [m, n, r] if the group is a tricyclic group Zm x Zn x Zr 73 | 74 | ffilter: function, optional 75 | A boolean filter function. Avoids generation of unwanted subgroups by 76 | rejecting partial bases. 77 | 78 | Returns iterator object yielding: 79 | 80 | H: integer ndarray 81 | Subgroup basis. 82 | """ 83 | dimension = len(orders) 84 | assert dimension in [1, 2, 3] 85 | if dimension == 1: 86 | m = orders[0] 87 | elif dimension == 2: 88 | m, n = orders 89 | else: 90 | m, n, r = orders 91 | dm = get_divisors(m) 92 | 93 | if dimension == 1: 94 | for d in consistent_first_rows(dimension, dm, ffilter): 95 | group_index = m // d 96 | if group_index >= min_index and group_index <= max_index: 97 | yield np.array([[d]]) 98 | 99 | elif dimension == 2: 100 | dn = get_divisors(n) 101 | 102 | for a in consistent_first_rows(dimension, dm, ffilter): 103 | for b in dn: 104 | group_index = m * n // (a * b) 105 | if group_index < min_index or group_index > max_index: 106 | continue 107 | 108 | for t in range(gcd(a, n // b)): 109 | s = t * a // gcd(a, n // b) 110 | 111 | H = np.array([[a, 0], [s, b]]) 112 | if ffilter is None or ffilter(H): 113 | yield H 114 | 115 | elif dimension == 3: 116 | dn = get_divisors(n) 117 | dr = get_divisors(r) 118 | 119 | for a in consistent_first_rows(dimension, dm, ffilter): 120 | for b, c in itertools.product(dn, dr): 121 | group_index = m * n * r // (a * b * c) 122 | if group_index < min_index or group_index > max_index: 123 | continue 124 | 125 | A = gcd(a, n // b) 126 | B = gcd(b, r // c) 127 | C = gcd(a, r // c) 128 | ABC = A * B * C 129 | X = ABC // gcd(a * r // c, ABC) 130 | 131 | for t in range(A): 132 | s = a * t // A 133 | 134 | H = np.zeros((dimension, dimension), dtype=int) 135 | H[0] = [a, 0, 0] 136 | H[1] = [s, b, 0] 137 | H[2, 2] = r 138 | if ffilter is not None and not ffilter(H): 139 | continue 140 | 141 | for w in range(B * gcd(t, X) // X): 142 | v = b * X * w // (B * gcd(t, X)) 143 | u0 = solve_linear_congruence(r, a, b, c, s, v) 144 | 145 | for z in range(C): 146 | u = u0 + a * z // C 147 | H = np.array([[a, 0, 0], [s, b, 0], [u, v, c]]) 148 | if ffilter is None or ffilter(H): 149 | yield H 150 | 151 | 152 | def count_subgroups(orders): 153 | """Count the number of subgroups of a cyclic/dicyclic/tricyclic integer 154 | group. 155 | 156 | Parameters: 157 | 158 | orders: list-like integer object 159 | Orders of the constituent groups. 160 | [m] if the group is a cyclic group Zm 161 | [m, n] if the group is a dicyclic group Zm x Zn 162 | [m, n, r] if the group is a tricyclic group Zm x Zn x Zr 163 | 164 | Returns: 165 | 166 | n: integer 167 | Subgroup basis. 168 | """ 169 | def P(n): 170 | return sum([gcd(k, n) for k in range(1, n + 1)]) 171 | 172 | dimension = len(orders) 173 | assert dimension in [1, 2, 3] 174 | if dimension == 1: 175 | m = orders[0] 176 | elif dimension == 2: 177 | m, n = orders 178 | else: 179 | m, n, r = orders 180 | dm = get_divisors(m) 181 | 182 | if dimension == 1: 183 | return len(dm) 184 | elif dimension == 2: 185 | dn = get_divisors(n) 186 | return sum([gcd(a, b) for a in dm for b in dn]) 187 | else: 188 | dn = get_divisors(n) 189 | dr = get_divisors(r) 190 | 191 | total = 0 192 | for a, b, c in itertools.product(dm, dn, dr): 193 | A = gcd(a, n // b) 194 | B = gcd(b, r // c) 195 | C = gcd(a, r // c) 196 | ABC = A * B * C 197 | X = ABC // gcd(a * r // c, ABC) 198 | total += ABC // X**2 * P(X) 199 | return total 200 | -------------------------------------------------------------------------------- /evgraf/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .minkowski_reduction import minkowski_reduce 2 | from .pbc import pbc2pbc 3 | from .axis_permutation import permute_axes 4 | from .standardization import standardize, standardize_cell 5 | from .rotation_matrix import rotation_matrix 6 | 7 | __all__ = ['minkowski_reduce', 'pbc2pbc', 'permute_axes', 'standardize', 8 | 'rotation_matrix', 'standardize_cell'] 9 | -------------------------------------------------------------------------------- /evgraf/utils/axis_permutation.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def permute_axes(atoms, permutation): 5 | """Permute axes of unit cell and atom positions. Considers only cell and 6 | atomic positions. Other vector quantities such as momenta are not 7 | modified.""" 8 | assert (np.sort(permutation) == np.arange(3)).all() 9 | 10 | permuted = atoms.copy() 11 | scaled = permuted.get_scaled_positions() 12 | 13 | cell = permuted.cell[permutation][:, permutation] 14 | permuted.set_cell(cell, scale_atoms=False) 15 | permuted.set_scaled_positions(scaled[:, permutation]) 16 | permuted.set_pbc(permuted.pbc[permutation]) 17 | return permuted 18 | -------------------------------------------------------------------------------- /evgraf/utils/minkowski_reduction.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import numpy as np 3 | from .pbc import pbc2pbc 4 | 5 | 6 | def reduction_gauss(B, hu, hv): 7 | """Calculate a Gauss-reduced lattice basis (2D reduction).""" 8 | u = hu @ B 9 | v = hv @ B 10 | 11 | max_it = 100000 # in practice this is not exceeded 12 | for it in range(max_it): 13 | 14 | x = int(round(np.dot(u, v) / np.dot(u, u))) 15 | hu, hv = hv - x * hu, hu 16 | 17 | u = hu @ B 18 | v = hv @ B 19 | if np.dot(u, u) >= np.dot(v, v): 20 | return hv, hu 21 | 22 | raise RuntimeError("Gaussian basis not found after %d iterations" % max_it) 23 | 24 | 25 | def relevant_vectors_2D(u, v): 26 | cs = np.array([e for e in itertools.product([-1, 0, 1], repeat=2)]) 27 | vs = np.dot(cs, [u, v]) 28 | indices = np.argsort(np.linalg.norm(vs, axis=1))[:7] 29 | return vs[indices], cs[indices] 30 | 31 | 32 | def closest_vector(t0, u, v): 33 | t = t0 34 | rs, cs = relevant_vectors_2D(u, v) 35 | a = np.array([0, 0]) 36 | 37 | dprev = float("inf") 38 | max_it = 100000 # in practice this is not exceeded 39 | for it in range(max_it): 40 | 41 | ds = np.linalg.norm(rs + t, axis=1) 42 | index = np.argmin(ds) 43 | if index == 0 or ds[index] >= dprev: 44 | return a 45 | 46 | dprev = ds[index] 47 | r = rs[index] 48 | kopt = int(round(-np.dot(t, r) / np.dot(r, r))) 49 | a += kopt * cs[index] 50 | t = t0 + a[0] * u + a[1] * v 51 | 52 | raise RuntimeError("Closest vector not found after %d iterations" % max_it) 53 | 54 | 55 | def reduction_full(B): 56 | """Calculate a Minkowski-reduced lattice basis (3D reduction).""" 57 | H = np.eye(3, dtype=int) 58 | norms = np.linalg.norm(B, axis=1) 59 | 60 | max_it = 100000 # in practice this is not exceeded 61 | for it in range(max_it): 62 | 63 | # Sort vectors by norm 64 | indices = np.argsort(norms) 65 | H = H[indices] 66 | 67 | # Gauss-reduce smallest two vectors 68 | hw = H[2] 69 | hu, hv = reduction_gauss(B, H[0], H[1]) 70 | 71 | H = np.array([hu, hv, hw]) 72 | R = H @ B 73 | u, v, w = R 74 | 75 | X = u / np.linalg.norm(u) 76 | Y = v - X * np.dot(v, X) 77 | Y /= np.linalg.norm(Y) 78 | 79 | # Find closest vector to last element of R 80 | pu, pv, pw = np.dot(R, np.array([X, Y]).T) 81 | nb = closest_vector(pw, pu, pv) 82 | hw = np.dot([nb[0], nb[1], 1], H) 83 | 84 | # Update basis 85 | H = np.array([hu, hv, hw]) 86 | R = H @ B 87 | 88 | norms = np.diag(np.dot(R, R.T)) 89 | if norms[2] >= norms[1] or (nb == 0).all(): 90 | return R, H 91 | 92 | raise RuntimeError("Reduced basis not found after %d iterations" % max_it) 93 | 94 | 95 | def minkowski_reduce(cell, pbc=True): 96 | """Calculate a Minkowski-reduced lattice basis. The reduced basis 97 | has the shortest possible vector lengths and has 98 | norm(a) <= norm(b) <= norm(c). 99 | 100 | Implements the method described in: 101 | 102 | Low-dimensional Lattice Basis Reduction Revisited 103 | Nguyen, Phong Q. and Stehlé, Damien, 104 | ACM Trans. Algorithms 5(4) 46:1--46:48, 2009 105 | https://doi.org/10.1145/1597036.1597050 106 | 107 | Parameters: 108 | 109 | cell: array 110 | The lattice basis to reduce (in row-vector format). 111 | pbc: array, optional 112 | The periodic boundary conditions of the cell (Default `True`). 113 | If `pbc` is provided, only periodic cell vectors are reduced. 114 | 115 | Returns: 116 | 117 | rcell: array 118 | The reduced lattice basis. 119 | op: array 120 | The unimodular matrix transformation (rcell = op @ cell). 121 | """ 122 | pbc = pbc2pbc(pbc) 123 | dim = pbc.sum() 124 | 125 | op = np.eye(3, dtype=int) 126 | if dim == 2: 127 | perm = np.argsort(pbc, kind='merge')[::-1] # stable sort 128 | pcell = cell[perm][:, perm] 129 | 130 | norms = np.linalg.norm(pcell, axis=1) 131 | norms[2] = float("inf") 132 | indices = np.argsort(norms) 133 | op = op[indices] 134 | 135 | hu, hv = reduction_gauss(pcell, op[0], op[1]) 136 | 137 | op[0] = hu 138 | op[1] = hv 139 | invperm = np.argsort(perm) 140 | op = op[invperm][:, invperm] 141 | 142 | elif dim == 3: 143 | _, op = reduction_full(cell) 144 | 145 | # maintain cell handedness 146 | if dim == 3: 147 | if np.sign(np.linalg.det(cell)) != np.sign(np.linalg.det(op @ cell)): 148 | op = -op 149 | elif dim == 2: 150 | index = np.argmin(pbc) 151 | _cell = cell.copy() 152 | _cell[index] = (1, 1, 1) 153 | _rcell = op @ cell 154 | _rcell[index] = (1, 1, 1) 155 | 156 | if np.sign(np.linalg.det(_cell)) != np.sign(np.linalg.det(_rcell)): 157 | index = np.argmax(pbc) 158 | op[index] *= -1 159 | 160 | norms1 = np.sort(np.linalg.norm(cell, axis=1)) 161 | norms2 = np.sort(np.linalg.norm(op @ cell, axis=1)) 162 | if not (norms2 <= norms1 + 1E-12).all(): 163 | raise RuntimeError("Minkowski reduction failed") 164 | return op @ cell, op 165 | -------------------------------------------------------------------------------- /evgraf/utils/pbc.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def pbc2pbc(pbc): 5 | newpbc = np.empty(3, bool) 6 | newpbc[:] = pbc 7 | return newpbc 8 | -------------------------------------------------------------------------------- /evgraf/utils/rotation_matrix.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def rotation_matrix(theta): 5 | return np.array([[np.cos(theta), -np.sin(theta), 0], 6 | [np.sin(theta), +np.cos(theta), 0], 7 | [ 0, 0, 1]]) 8 | -------------------------------------------------------------------------------- /evgraf/utils/standardization.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from collections import namedtuple 3 | from .minkowski_reduction import minkowski_reduce 4 | from evgraf.utils import pbc2pbc 5 | 6 | 7 | StandardizedAtoms = namedtuple('StandardizedAtoms', 8 | 'atoms op invop barycenter zpermutation') 9 | 10 | 11 | def standardize_cell(cell): 12 | Q, R = np.linalg.qr(cell.T) 13 | indices = np.where(np.diag(R) < 0)[0] 14 | Q[:, indices] *= -1 15 | R[indices] *= -1 16 | if np.sign(np.linalg.det(R)) != np.sign(np.linalg.det(cell)): 17 | R = -R 18 | Q = -Q 19 | return Q, R.T 20 | 21 | 22 | def standardize(atoms, subtract_barycenter=False): 23 | zpermutation = np.argsort(atoms.numbers, kind='merge') 24 | atoms = atoms[zpermutation] 25 | atoms.set_pbc(pbc2pbc(atoms.pbc)) 26 | atoms.wrap(eps=0) 27 | 28 | barycenter = np.mean(atoms.get_positions(), axis=0) 29 | if subtract_barycenter: 30 | atoms.positions -= barycenter 31 | 32 | atoms.set_cell(atoms.cell.complete(), scale_atoms=False) 33 | rcell, op = minkowski_reduce(atoms.cell, atoms.pbc) 34 | invop = np.linalg.inv(op) 35 | atoms.set_cell(rcell, scale_atoms=False) 36 | 37 | atoms.wrap(eps=0) 38 | return StandardizedAtoms(atoms=atoms, op = op, invop=invop, 39 | barycenter=barycenter, zpermutation=zpermutation) 40 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import numpy 4 | from setuptools import Extension, find_packages, setup 5 | from distutils.sysconfig import get_config_vars 6 | from distutils.version import LooseVersion 7 | import platform 8 | 9 | 10 | major_version = 0 11 | minor_version = 1 12 | subminor_version = 6 13 | version = f'{major_version}.{minor_version}.{subminor_version}' 14 | extra_compile_args = [] 15 | 16 | 17 | def is_platform_mac(): 18 | return sys.platform == "darwin" 19 | 20 | 21 | # Build for at least macOS 10.9 when compiling on a 10.9 system or above, 22 | # overriding CPython distuitls behaviour which is to target the version that 23 | # python was built for. This may be overridden by setting 24 | # MACOSX_DEPLOYMENT_TARGET before calling setup.py 25 | if is_platform_mac(): 26 | if "MACOSX_DEPLOYMENT_TARGET" not in os.environ: 27 | current_system = platform.mac_ver()[0] 28 | python_target = get_config_vars().get( 29 | "MACOSX_DEPLOYMENT_TARGET", current_system 30 | ) 31 | if ( 32 | LooseVersion(python_target) < "10.9" 33 | and LooseVersion(current_system) >= "10.9" 34 | ): 35 | os.environ["MACOSX_DEPLOYMENT_TARGET"] = "10.9" 36 | 37 | if sys.version_info[:2] == (3, 8): 38 | extra_compile_args.append("-Wno-error=deprecated-declarations") 39 | 40 | 41 | # read the contents of README.md 42 | this_directory = os.path.abspath(os.path.dirname(__file__)) 43 | with open(os.path.join(this_directory, 'README.md'), encoding='utf-8') as f: 44 | long_description = f.read() 45 | 46 | evgrafcpp_module = Extension( 47 | 'evgrafcpp', 48 | sources=['src/crystalline.cpp', 49 | 'src/evgrafcpp_module.cpp', 50 | 'src/lup_decomposition.cpp', 51 | 'src/matrix_vector.cpp', 52 | 'src/rectangular_lsap.cpp', 53 | 'src/wrap_positions.cpp'], 54 | include_dirs=[os.path.join(numpy.get_include(), 'numpy'), 55 | 'src'], 56 | extra_compile_args=extra_compile_args, 57 | language='c++' 58 | ) 59 | 60 | setup(name='evgraf', 61 | python_requires='>=3.6', 62 | ext_modules=[evgrafcpp_module], 63 | version=version, 64 | description='Geometric analysis of crystal structures', 65 | author='Peter M. Larsen', 66 | author_email='peter.mahler.larsen@gmail.com', 67 | url='https://github.com/pmla/evgraf', 68 | long_description_content_type='text/markdown', 69 | long_description=long_description, 70 | install_requires=['numpy', 'ase>=3.19'], 71 | packages=find_packages() 72 | ) 73 | -------------------------------------------------------------------------------- /src/crystalline.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "rectangular_lsap.h" 7 | 8 | 9 | static double calculate_dsquared(double (*a)[3], double (*b)[3], int num_cells, double (*nbr_cells)[3]) { 10 | 11 | double best = INFINITY; 12 | for (int j=0;j _permutation(num_atoms, 0); 59 | std::vector< double > distances(num_atoms * num_atoms, INFINITY); 60 | 61 | 62 | std::map< int, int > start; 63 | std::map< int, int > count; 64 | int prev = -1; 65 | for (int i=0;i::iterator it = start.begin(); 78 | while (it != start.end()) { 79 | int z = it->first; 80 | int offset = start[z]; 81 | int num = count[z]; 82 | 83 | res = monoatomic_bipartite_matching(num, num_cells, &P[offset], &Q[offset], nbr_cells, 84 | &cost, &_permutation.data()[offset], distances.data()); 85 | if (res != 0) { 86 | break; 87 | } 88 | 89 | it++; 90 | } 91 | 92 | for (int i=0;i 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "crystalline.h" 8 | #include "wrap_positions.h" 9 | #include "rectangular_lsap.h" 10 | 11 | 12 | static PyObject* error(PyObject* type, const char* msg) 13 | { 14 | PyErr_SetString(type, msg); 15 | return NULL; 16 | } 17 | 18 | static PyObject* calculate_rmsd(PyObject* self, PyObject* args, PyObject* kwargs) 19 | { 20 | (void)self; 21 | PyObject* obj_p = NULL; 22 | PyObject* obj_q = NULL; 23 | PyObject* obj_nbrcell = NULL; 24 | PyObject* obj_numbers = NULL; 25 | PyObject* obj_pcont = NULL; 26 | PyObject* obj_qcont = NULL; 27 | PyObject* obj_nbrcellcont = NULL; 28 | PyObject* obj_numberscont = NULL; 29 | if (!PyArg_ParseTuple(args, "OOOO", &obj_p, &obj_q, &obj_nbrcell, &obj_numbers)) 30 | return NULL; 31 | 32 | // get numpy arrays in contiguous form 33 | obj_pcont = PyArray_ContiguousFromAny(obj_p, NPY_DOUBLE, 1, 2); 34 | if (obj_pcont == NULL) 35 | return error(PyExc_TypeError, "Invalid input data: P"); 36 | 37 | obj_qcont = PyArray_ContiguousFromAny(obj_q, NPY_DOUBLE, 1, 2); 38 | if (obj_qcont == NULL) 39 | return error(PyExc_TypeError, "Invalid input data: Q"); 40 | 41 | obj_nbrcellcont = PyArray_ContiguousFromAny(obj_nbrcell, NPY_DOUBLE, 1, 3); 42 | if (obj_nbrcellcont == NULL) 43 | return error(PyExc_TypeError, "Invalid input data: nbrcell"); 44 | 45 | obj_numberscont = PyArray_ContiguousFromAny(obj_numbers, NPY_INT, 1, 1); 46 | if (obj_numberscont == NULL) 47 | return error(PyExc_TypeError, "Invalid input data: numbers"); 48 | 49 | // validate numpy arrays 50 | if (PyArray_NDIM(obj_pcont) != 2) 51 | return error(PyExc_TypeError, "P must have shape N x 3"); 52 | 53 | if (PyArray_NDIM(obj_qcont) != 2) 54 | return error(PyExc_TypeError, "Q must have shape N x 3"); 55 | 56 | if (PyArray_DIM(obj_pcont, 1) != 3) 57 | return error(PyExc_TypeError, "P must contain three-dimensional coordinates"); 58 | 59 | if (PyArray_DIM(obj_qcont, 1) != 3) 60 | return error(PyExc_TypeError, "Q must contain three-dimensional coordinates"); 61 | 62 | if (PyArray_DIM(obj_pcont, 0) != PyArray_DIM(obj_qcont, 0)) 63 | return error(PyExc_TypeError, "P and Q must contain same number of entries"); 64 | 65 | if (PyArray_NDIM(obj_nbrcellcont) != 2 66 | || PyArray_DIM(obj_nbrcellcont, 1) != 3) 67 | return error(PyExc_TypeError, "nbrcell must have shape m x 3 x 3"); 68 | 69 | if (PyArray_NDIM(obj_numberscont) != 1) 70 | return error(PyExc_TypeError, "numbers array must be 1-dimensional"); 71 | 72 | if (PyArray_DIM(obj_numberscont, 0) != PyArray_DIM(obj_pcont, 0)) 73 | return error(PyExc_TypeError, "numbers array must contain N entries"); 74 | 75 | int num_atoms = PyArray_DIM(obj_pcont, 0); 76 | double* P = (double*)PyArray_DATA((PyArrayObject*)obj_p); 77 | double* Q = (double*)PyArray_DATA((PyArrayObject*)obj_q); 78 | int num_cells = PyArray_DIM(obj_nbrcellcont, 0); 79 | double* nbrcells = (double*)PyArray_DATA((PyArrayObject*)obj_nbrcellcont); 80 | int* numbers = (int*)PyArray_DATA((PyArrayObject*)obj_numbers); 81 | 82 | 83 | npy_intp dim[1] = {num_atoms}; 84 | PyObject* obj_permutation = PyArray_SimpleNew(1, dim, NPY_INT); 85 | int* permutation = (int*)PyArray_DATA((PyArrayObject*)obj_permutation); 86 | 87 | double cost = INFINITY; 88 | int res = crystalline_bipartite_matching(num_atoms, num_cells, (double (*)[3])P, (double (*)[3])Q, 89 | (double (*)[3])nbrcells, 90 | numbers, &cost, permutation); 91 | if (res != 0) 92 | return error(PyExc_RuntimeError, "bipartite matching failed"); 93 | 94 | PyObject* result = Py_BuildValue("dO", cost, obj_permutation); 95 | Py_DECREF(obj_pcont); 96 | Py_DECREF(obj_qcont); 97 | Py_DECREF(obj_nbrcellcont); 98 | Py_DECREF(obj_numberscont); 99 | Py_DECREF(obj_permutation); 100 | return result; 101 | } 102 | 103 | static PyObject* linear_sum_assignment(PyObject* self, PyObject* args, PyObject* kwargs) 104 | { 105 | (void)self; 106 | PyObject* obj_cost = NULL; 107 | PyObject* obj_costcont = NULL; 108 | if (!PyArg_ParseTuple(args, "O", &obj_cost)) 109 | return NULL; 110 | 111 | // get numpy arrays in contiguous form 112 | obj_costcont = PyArray_ContiguousFromAny(obj_cost, NPY_DOUBLE, 1, 2); 113 | if (obj_costcont == NULL) 114 | return error(PyExc_TypeError, "Invalid input data: cost"); 115 | 116 | // validate numpy arrays 117 | if (PyArray_NDIM(obj_costcont) != 2) 118 | return error(PyExc_TypeError, "cost matrix must be two-dimensional"); 119 | 120 | int m = PyArray_DIM(obj_costcont, 0); 121 | int n = PyArray_DIM(obj_costcont, 1); 122 | int p = std::min(m, n); 123 | double* cost = (double*)PyArray_DATA((PyArrayObject*)obj_cost); 124 | 125 | npy_intp dim[1] = {p}; 126 | PyObject* obj_permutation = PyArray_SimpleNew(1, dim, NPY_INT64); 127 | int64_t* permutation = (int64_t*)PyArray_DATA((PyArrayObject*)obj_permutation); 128 | 129 | PyObject* obj_range = PyArray_SimpleNew(1, dim, NPY_INT64); 130 | int64_t* range = (int64_t*)PyArray_DATA((PyArrayObject*)obj_range); 131 | for (int i=0;i 8 | * All rights reserved. 9 | * 10 | * Modifided for use in "auguste" module by P. M. Larsen, 2019 11 | * 12 | * Redistribution and use in source and binary forms, with or without 13 | * modification, are permitted provided that the following conditions 14 | * are met: 15 | * 1. Redistributions of source code must retain the above copyright 16 | * notice, this list of conditions and the following disclaimer. 17 | * 2. Redistributions in binary form must reproduce the above copyright 18 | * notice, this list of conditions and the following disclaimer in the 19 | * documentation and/or other materials provided with the distribution. 20 | * 21 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS `AS IS' 22 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 24 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 25 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 26 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 27 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 28 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 29 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 30 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 | * POSSIBILITY OF SUCH DAMAGE. 32 | */ 33 | 34 | #include 35 | #include 36 | 37 | #define MAX_LEN 10 38 | bool lup_decompose(int n, double* A, int* P) 39 | { 40 | double swap_area[MAX_LEN]; 41 | 42 | int* tmpPermutations = P; 43 | int* dstPermutations; 44 | int k2 = 0, t; 45 | double temp; 46 | double p; 47 | int lLastColum = n - 1; 48 | int lSwapSize = n * (int)sizeof(double); 49 | double* lTmpMatrix = A; 50 | double* lColumnMatrix, * lDestMatrix; 51 | int offset = 1; 52 | int lStride = n - 1; 53 | 54 | // Initialize P 55 | for (int i = 0; i < n; i++) 56 | *tmpPermutations++ = i; 57 | 58 | // Now make a pivot with column switch 59 | tmpPermutations = P; 60 | for (int k = 0; k < lLastColum; k++) 61 | { 62 | p = 0.0; 63 | 64 | // Take the middle element 65 | lColumnMatrix = lTmpMatrix + k; 66 | 67 | // Make permutation with the biggest value in the column 68 | for (int i = k; i < n; i++) 69 | { 70 | temp = ((*lColumnMatrix > 0) ? *lColumnMatrix : -(*lColumnMatrix)); 71 | if (temp > p) 72 | { 73 | p = temp; 74 | k2 = i; 75 | } 76 | 77 | // Next line 78 | lColumnMatrix += n; 79 | } 80 | 81 | // A whole rest of 0 -> non singular 82 | if (p == 0.0) 83 | return false; 84 | 85 | // Should we permute? 86 | if (k2 != k) 87 | { 88 | // exchange of line 89 | // k2 > k 90 | dstPermutations = tmpPermutations + k2 - k; 91 | // swap indices 92 | t = *tmpPermutations; 93 | *tmpPermutations = *dstPermutations; 94 | *dstPermutations = t; 95 | 96 | // and swap entire line. 97 | lColumnMatrix = lTmpMatrix + (k2 - k) * n; 98 | memcpy(swap_area, lColumnMatrix, lSwapSize); 99 | memcpy(lColumnMatrix, lTmpMatrix, lSwapSize); 100 | memcpy(lTmpMatrix, swap_area, lSwapSize); 101 | } 102 | 103 | // now update data in the rest of the line and line after 104 | lDestMatrix = lTmpMatrix + k; 105 | lColumnMatrix = lDestMatrix + n; 106 | 107 | // take the middle element 108 | temp = *(lDestMatrix++); 109 | 110 | // now compute up data (i.e. coeff up of the diagonal). 111 | for (int i = offset; i < n; i++) 112 | { 113 | // lColumnMatrix; 114 | // divide the lower column elements by the diagonal value 115 | 116 | // A[i][k] /= A[k][k]; 117 | // p = A[i][k] 118 | p = *lColumnMatrix / temp; 119 | *(lColumnMatrix++) = p; 120 | 121 | for (int j = /* k + 1 */ offset; j < n; j++) 122 | { 123 | // A[i][j] -= A[i][k] * A[k][j]; 124 | *(lColumnMatrix++) -= p * (*(lDestMatrix++)); 125 | } 126 | 127 | // come back to the k+1th element 128 | lDestMatrix -= lStride; 129 | 130 | // go to kth element of the next line 131 | lColumnMatrix += k; 132 | } 133 | 134 | // offset is now k+2 135 | ++offset; 136 | 137 | // 1 element less for stride 138 | --lStride; 139 | 140 | // next line 141 | lTmpMatrix += n; 142 | 143 | // next permutation element 144 | ++tmpPermutations; 145 | } 146 | 147 | return true; 148 | } 149 | 150 | void lup_solve(int n, double* A, int* P, double* b, double* x) 151 | { 152 | double intermediate_data[MAX_LEN * MAX_LEN]; 153 | 154 | int lStride = n + 1; 155 | double* lLineMatrix = A; 156 | double* lBeginPtr = x + n - 1; 157 | int* lCurrentPermutationPtr = P; 158 | 159 | 160 | double* lIntermediatePtr = intermediate_data; 161 | double* lGeneratedData = intermediate_data + n - 1; 162 | 163 | for (int i = 0; i < n; i++) 164 | { 165 | double sum = 0; 166 | double* lCurrentPtr = intermediate_data; 167 | double* lTmpMatrix = lLineMatrix; 168 | 169 | for (int j = 1; j <= i; j++) 170 | { 171 | // sum += A[i][j-1] * y[j-1]; 172 | sum += (*(lTmpMatrix++)) * (*(lCurrentPtr++)); 173 | } 174 | 175 | // y[i] = b[P[i]] - sum; 176 | *(lIntermediatePtr++) = b[*(lCurrentPermutationPtr++)] - sum; 177 | lLineMatrix += n; 178 | } 179 | 180 | // we take the last point of the A 181 | lLineMatrix = A + n * n - 1; 182 | 183 | // and we take after the last point of the destination vector 184 | double* lDestPtr = x + n; 185 | 186 | 187 | assert(n != 0); 188 | for (int k = (int)n - 1; k != -1; k--) 189 | { 190 | double sum = 0; 191 | double* lTmpMatrix = lLineMatrix; 192 | double u = *(lTmpMatrix++); 193 | double* lCurrentPtr = lDestPtr--; 194 | 195 | for (int j = (int)(k + 1); j < n; ++j) 196 | { 197 | // sum += A[k][j] * x[j] 198 | sum += (*(lTmpMatrix++)) * (*(lCurrentPtr++)); 199 | } 200 | 201 | // x[k] = (y[k] - sum) / u; 202 | *(lBeginPtr--) = (*(lGeneratedData--) - sum) / u; 203 | lLineMatrix -= lStride; 204 | } 205 | } 206 | 207 | -------------------------------------------------------------------------------- /src/lup_decomposition.h: -------------------------------------------------------------------------------- 1 | /*MIT License 2 | 3 | Copyright (c) 2019 P. M. Larsen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE.*/ 22 | 23 | 24 | #ifndef LUP_DECOMPOSITION_H 25 | #define LUP_DECOMPOSITION_H 26 | 27 | bool lup_decompose(int n, double* A, int* P); 28 | void lup_solve(int n, double* A, int* P, double* b, double* x); 29 | 30 | #endif 31 | 32 | -------------------------------------------------------------------------------- /src/matrix_vector.cpp: -------------------------------------------------------------------------------- 1 | /*MIT License 2 | 3 | Copyright (c) 2019 P. M. Larsen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE.*/ 22 | 23 | 24 | #include 25 | #include 26 | #include 27 | 28 | 29 | double vector_dot(int n, double* a, double* b) 30 | { 31 | double dot = 0; 32 | for (int i=0;i 28 | 29 | 30 | void swap(double* a, double* b); 31 | double vector_dot(int n, double* a, double* b); 32 | double vector_norm(int n, double* x); 33 | void normalize_vector(int n, double* x); 34 | void matvec(int n, double* A, double* x, double* b); 35 | void matveci(int n, double* A, int* x, double* b); 36 | void transpose(int n, double* A); 37 | void transposei(int n, int* A); 38 | double frobenius_inner_product(double* A, double* B); 39 | void matmul(int n, double* A, double* x, double* b); 40 | void matmuli(int n, int* A, int* x, int* b); 41 | void matmul_int8(int n, int* A, int8_t* x, int* b); 42 | void matmul_di(int n, double* A, int* x, double* b); 43 | void matmul_id(int n, int* A, double* x, double* b); 44 | double determinant_3x3(double* m); 45 | void unimodular_inverse_3x3i(int* A, int* B); 46 | void flip_matrix(int n, double* m); 47 | void flip_matrix_i(int n, int* m); 48 | 49 | #endif 50 | 51 | -------------------------------------------------------------------------------- /src/rectangular_lsap.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Redistribution and use in source and binary forms, with or without 3 | modification, are permitted provided that the following conditions 4 | are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following 11 | disclaimer in the documentation and/or other materials provided 12 | with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | 31 | This code implements the shortest augmenting path algorithm for the 32 | rectangular assignment problem. This implementation is based on the 33 | pseudocode described in pages 1685-1686 of: 34 | 35 | DF Crouse. On implementing 2D rectangular assignment algorithms. 36 | IEEE Transactions on Aerospace and Electronic Systems 37 | 52(4):1679-1696, August 2016 38 | doi: 10.1109/TAES.2016.140952 39 | 40 | Author: PM Larsen 41 | */ 42 | 43 | #include 44 | #include 45 | #include 46 | 47 | static int 48 | augmenting_path(int nc, std::vector& cost, std::vector& u, 49 | std::vector& v, std::vector& path, 50 | std::vector& row4col, 51 | std::vector& shortestPathCosts, int i, 52 | std::vector& SR, std::vector& SC, double* p_minVal) 53 | { 54 | double minVal = 0; 55 | 56 | // Crouse's pseudocode uses set complements to keep track of remaining 57 | // nodes. Here we use a vector, as it is more efficient in C++. 58 | int num_remaining = nc; 59 | std::vector remaining(nc); 60 | for (int it = 0; it < nc; it++) { 61 | remaining[it] = it; 62 | } 63 | 64 | std::fill(SR.begin(), SR.end(), false); 65 | std::fill(SC.begin(), SC.end(), false); 66 | std::fill(shortestPathCosts.begin(), shortestPathCosts.end(), INFINITY); 67 | 68 | // find shortest augmenting path 69 | int sink = -1; 70 | while (sink == -1) { 71 | 72 | int index = -1; 73 | double lowest = INFINITY; 74 | SR[i] = true; 75 | 76 | for (int it = 0; it < num_remaining; it++) { 77 | int j = remaining[it]; 78 | 79 | double r = minVal + cost[i * nc + j] - u[i] - v[j]; 80 | if (r < shortestPathCosts[j]) { 81 | path[j] = i; 82 | shortestPathCosts[j] = r; 83 | } 84 | 85 | // When multiple nodes have the minimum cost, we select one which 86 | // gives us a new sink node. This is particularly important for 87 | // integer cost matrices with small co-efficients. 88 | if (shortestPathCosts[j] < lowest || 89 | (shortestPathCosts[j] == lowest && row4col[j] == -1)) { 90 | lowest = shortestPathCosts[j]; 91 | index = it; 92 | } 93 | } 94 | 95 | minVal = lowest; 96 | int j = remaining[index]; 97 | if (minVal == INFINITY) { // infeasible cost matrix 98 | return -1; 99 | } 100 | 101 | if (row4col[j] == -1) { 102 | sink = j; 103 | } else { 104 | i = row4col[j]; 105 | } 106 | 107 | SC[j] = true; 108 | remaining[index] = remaining[--num_remaining]; 109 | remaining.resize(num_remaining); 110 | } 111 | 112 | *p_minVal = minVal; 113 | return sink; 114 | } 115 | 116 | int 117 | solve_rectangular_linear_sum_assignment(int nr, int nc, double* input_cost, int64_t* output_col4row) 118 | { 119 | 120 | // build a non-negative cost matrix 121 | std::vector cost(nr * nc); 122 | double minval = *std::min_element(input_cost, input_cost + nr * nc); 123 | for (int i = 0; i < nr * nc; i++) { 124 | cost[i] = input_cost[i] - minval; 125 | } 126 | 127 | // initialize variables 128 | std::vector u(nr, 0); 129 | std::vector v(nc, 0); 130 | std::vector shortestPathCosts(nc); 131 | std::vector path(nc, -1); 132 | std::vector col4row(nr, -1); 133 | std::vector row4col(nc, -1); 134 | std::vector SR(nr); 135 | std::vector SC(nc); 136 | 137 | // iteratively build the solution 138 | for (int curRow = 0; curRow < nr; curRow++) { 139 | 140 | double minVal; 141 | int sink = augmenting_path(nc, cost, u, v, path, row4col, 142 | shortestPathCosts, curRow, SR, SC, &minVal); 143 | if (sink < 0) { 144 | return -1; 145 | } 146 | 147 | // update dual variables 148 | u[curRow] += minVal; 149 | for (int i = 0; i < nr; i++) { 150 | if (SR[i] && i != curRow) { 151 | u[i] += minVal - shortestPathCosts[col4row[i]]; 152 | } 153 | } 154 | 155 | for (int j = 0; j < nc; j++) { 156 | if (SC[j]) { 157 | v[j] -= minVal - shortestPathCosts[j]; 158 | } 159 | } 160 | 161 | // augment previous solution 162 | int j = sink; 163 | while (1) { 164 | int i = path[j]; 165 | row4col[j] = i; 166 | std::swap(col4row[i], j); 167 | if (i == curRow) { 168 | break; 169 | } 170 | } 171 | } 172 | 173 | for (int i = 0; i < nr; i++) { 174 | output_col4row[i] = col4row[i]; 175 | } 176 | 177 | return 0; 178 | } 179 | 180 | -------------------------------------------------------------------------------- /src/rectangular_lsap.h: -------------------------------------------------------------------------------- 1 | /* 2 | Redistribution and use in source and binary forms, with or without 3 | modification, are permitted provided that the following conditions 4 | are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following 11 | disclaimer in the documentation and/or other materials provided 12 | with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | #ifndef RECTANGULAR_LSAP_H 32 | #define RECTANGULAR_LSAP_H 33 | 34 | #include 35 | 36 | int solve_rectangular_linear_sum_assignment(int nr, int nc, double* input_cost, 37 | int64_t* col4row); 38 | 39 | #endif 40 | -------------------------------------------------------------------------------- /src/wrap_positions.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "lup_decomposition.h" 8 | #include "matrix_vector.h" 9 | 10 | 11 | static double wrap(double x) { 12 | x = fmod(x, 1); 13 | if (x < 0) { 14 | x += 1; 15 | } 16 | if (x == -0) x = 0; 17 | 18 | if (x == 1) { 19 | return 0; 20 | } 21 | else { 22 | return x; 23 | } 24 | } 25 | 26 | int _wrap_positions(int num_atoms, double (*P)[3], double (*cell)[3], int8_t* pbc, double (*wrapped)[3]) { 27 | 28 | double transposed[9]; 29 | memcpy(transposed, cell, 9 * sizeof(double)); 30 | transpose(3, transposed); 31 | 32 | int pivot[3]; 33 | double LU[9] = {0}; 34 | memcpy(LU, transposed, 9 * sizeof(double)); 35 | lup_decompose(3, LU, pivot); //todo: check return code 36 | 37 | for (int i=0;i 1 137 | 138 | 139 | # 1-dimensional: carbon nanotube 140 | @pytest.mark.parametrize("i", range(3)) 141 | @pytest.mark.parametrize("seed", range(3)) 142 | def test_nanotube(seed, i): 143 | size = 4 144 | atoms = nanotube(3, 3, length=size) 145 | permutation = np.roll(np.arange(3), i) 146 | atoms = permute_axes(atoms, permutation) 147 | 148 | rng = np.random.RandomState(seed=seed) 149 | atoms = randomize(rng, atoms) 150 | 151 | result = find_crystal_reductions(atoms)[:3] 152 | factors = [reduced.factor for reduced in result] 153 | assert tuple(factors) == (1, 2, 4) 154 | assert all([reduced.rmsd < TOL for reduced in result]) 155 | check_components(atoms, result) 156 | 157 | 158 | @pytest.mark.parametrize("n", np.arange(1, 16)) 159 | def test_line(n): 160 | positions = np.zeros((n, 3)) 161 | positions[:, 0] = np.arange(n) 162 | atoms = Atoms(positions=positions, cell=np.diag([n, 0, 0]), 163 | pbc=[1, 0, 0], numbers=10 * np.ones(n)) 164 | 165 | result = find_crystal_reductions(atoms) 166 | check_components(atoms, result) 167 | -------------------------------------------------------------------------------- /tests/inversion_symmetry.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import numpy as np 3 | from numpy.testing import assert_allclose 4 | from ase.build import bulk, mx2, nanotube 5 | from ase.geometry import find_mic 6 | from evgraf.utils import permute_axes 7 | from evgraf import find_inversion_symmetry 8 | 9 | 10 | TOL = 1E-10 11 | 12 | 13 | def check_result(atoms, result): 14 | # check that permutation maps species onto like species 15 | assert (atoms.numbers == atoms.numbers[result.permutation]).all() 16 | 17 | # check rmsd 18 | delta = result.atoms.get_positions() - atoms.get_positions() 19 | _, x = find_mic(delta, cell=atoms.cell) 20 | assert_allclose(np.sqrt(np.mean(x**2)), result.rmsd, atol=TOL) 21 | 22 | # check inversion manually 23 | inverted = atoms[result.permutation] 24 | inverted.positions = -inverted.positions + 2 * result.axis 25 | inverted.wrap(eps=0) 26 | 27 | delta = result.atoms.get_positions() - inverted.get_positions() 28 | _, x = find_mic(delta, cell=atoms.cell) 29 | assert_allclose(np.sqrt(np.mean(x**2)), result.rmsd, atol=TOL) 30 | 31 | 32 | # 3-dimensional: NaCl 33 | def test_nacl(): 34 | size = 2 35 | atoms = bulk("NaCl", "rocksalt", a=5.64) * size 36 | result = find_inversion_symmetry(atoms) 37 | assert result.rmsd < TOL 38 | check_result(atoms, result) 39 | 40 | 41 | def test_nacl_rattled(): 42 | size = 2 43 | atoms = bulk("NaCl", "rocksalt", a=5.64) * size 44 | atoms.rattle() 45 | result = find_inversion_symmetry(atoms) 46 | check_result(atoms, result) 47 | 48 | 49 | # 2-dimensional: MoS2 50 | @pytest.mark.parametrize("i", range(3)) 51 | def test_mos2(i): 52 | size = 2 53 | atoms = mx2(formula='MoS2', size=(size, size, 1)) 54 | permutation = np.roll(np.arange(3), i) 55 | atoms = permute_axes(atoms, permutation) 56 | result = find_inversion_symmetry(atoms) 57 | check_result(atoms, result) 58 | 59 | 60 | # 1-dimensional: carbon nanotube 61 | @pytest.mark.parametrize("i", range(3)) 62 | def test_nanotube(i): 63 | size = 4 64 | atoms = nanotube(3, 3, length=size) 65 | permutation = np.roll(np.arange(3), i) 66 | atoms = permute_axes(atoms, permutation) 67 | 68 | result = find_inversion_symmetry(atoms) 69 | assert result.rmsd < TOL 70 | check_result(atoms, result) 71 | 72 | atoms.rattle() 73 | result = find_inversion_symmetry(atoms) 74 | check_result(atoms, result) 75 | -------------------------------------------------------------------------------- /tests/minkowski_reduce.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import numpy as np 3 | from evgraf.utils import minkowski_reduce 4 | from ase.cell import Cell 5 | 6 | 7 | tol = 1E-14 8 | np.seterr(all='raise') 9 | 10 | 11 | def test_faulty_cell(): 12 | cell = Cell([[8.972058879514716, 0.0009788104586639142, 0.0005932485724084841], 13 | [4.485181755775297, 7.770520334862034, 0.00043663339838788054], 14 | [4.484671994095723, 2.5902066679984634, 16.25695615743613]]) 15 | cell, _ = minkowski_reduce(cell) 16 | 17 | 18 | @pytest.mark.parametrize("seed", range(40)) 19 | def test_random_3D(seed): 20 | rng = np.random.RandomState(seed) 21 | B = rng.uniform(-1, 1, (3, 3)) 22 | R, H = minkowski_reduce(B) 23 | assert np.allclose(H @ B, R, atol=tol) 24 | assert np.sign(np.linalg.det(B)) == np.sign(np.linalg.det(R)) 25 | 26 | norms = np.linalg.norm(R, axis=1) 27 | assert (np.argsort(norms) == range(3)).all() 28 | 29 | # Test idempotency 30 | _, _H = minkowski_reduce(R) 31 | assert (_H == np.eye(3, dtype=int)).all() 32 | 33 | rcell, _ = minkowski_reduce(B) 34 | assert np.allclose(rcell, R, atol=tol) 35 | 36 | 37 | cell = np.array([[1, 1, 2], [0, 1, 4], [0, 0, 1]]) 38 | unimodular = np.array([[1, 2, 2], [0, 1, 2], [0, 0, 1]]) 39 | assert np.linalg.det(unimodular) == 1 40 | lcell = unimodular.T @ cell 41 | 42 | 43 | def test_3D(): 44 | rcell, op = minkowski_reduce(lcell) 45 | assert np.linalg.det(rcell) == 1 46 | 47 | for pbc in [1, True, (1, 1, 1)]: 48 | rcell, op = minkowski_reduce(lcell, pbc=pbc) 49 | assert np.linalg.det(rcell) == 1 50 | assert np.sign(np.linalg.det(rcell)) == np.sign(np.linalg.det(lcell)) 51 | 52 | 53 | def test_0D(): 54 | rcell, op = minkowski_reduce(lcell, pbc=[0, 0, 0]) 55 | assert (rcell == lcell).all() # 0D reduction does nothing 56 | 57 | 58 | @pytest.mark.parametrize("i", range(3)) 59 | def test_1D(i): 60 | rcell, op = minkowski_reduce(lcell, pbc=np.roll([1, 0, 0], i)) 61 | assert (rcell == lcell).all() # 1D reduction does nothing 62 | 63 | 64 | def test_single_vector(): 65 | zcell = np.zeros((3, 3)) 66 | zcell[0] = lcell[0] 67 | rcell, _ = minkowski_reduce(zcell, zcell.any(1)) 68 | assert np.allclose(rcell, zcell, atol=tol) 69 | 70 | 71 | @pytest.mark.parametrize("i", range(3)) 72 | def test_2D(i): 73 | pbc = np.roll([0, 1, 1], i) 74 | rcell, op = minkowski_reduce(lcell.astype(float), pbc=pbc) 75 | assert (rcell[i] == lcell[i]).all() 76 | 77 | zcell = np.copy(lcell.astype(float)) 78 | zcell[i] = 0 79 | rzcell, _ = minkowski_reduce(zcell, zcell.any(1)) 80 | rcell[i] = 0 81 | assert np.allclose(rzcell, rcell, atol=tol) 82 | -------------------------------------------------------------------------------- /tests/pbc.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import itertools 3 | from evgraf.utils import pbc2pbc 4 | 5 | 6 | def test_true(): 7 | assert (pbc2pbc(True) == [True, True, True]).all() 8 | 9 | 10 | def test_false(): 11 | assert (pbc2pbc(False) == [False, False, False]).all() 12 | 13 | 14 | @pytest.mark.parametrize("pbc", itertools.product([False, True], repeat=3)) 15 | def test_array(pbc): 16 | assert (pbc2pbc(pbc) == pbc).all() 17 | -------------------------------------------------------------------------------- /tests/permute_axes.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import numpy as np 3 | from numpy.testing import assert_allclose 4 | from ase import Atoms 5 | from evgraf.utils import permute_axes 6 | 7 | 8 | TOL = 1E-10 9 | 10 | 11 | @pytest.mark.parametrize("seed", range(20)) 12 | def test_permute_axes(seed): 13 | rng = np.random.RandomState(seed) 14 | n = 10 15 | atoms = Atoms(numbers=[1] * n, 16 | scaled_positions=rng.uniform(0, 1, (n, 3)), 17 | pbc=rng.randint(0, 2, 3), 18 | cell=rng.uniform(-1, 1, (3, 3))) 19 | 20 | permutation = rng.permutation(3) 21 | permuted = permute_axes(atoms, permutation) 22 | invperm = np.argsort(permutation) 23 | original = permute_axes(permuted, invperm) 24 | 25 | assert (original.pbc == atoms.pbc).all() 26 | assert_allclose(original.cell, atoms.cell, atol=TOL) 27 | assert_allclose(original.get_positions(), atoms.get_positions(), atol=TOL) 28 | assert_allclose(atoms.get_positions()[:, permutation], 29 | permuted.get_positions(), atol=TOL) 30 | 31 | assert_allclose(atoms.cell.volume, permuted.cell.volume, atol=TOL) 32 | assert_allclose(atoms.cell.volume, original.cell.volume, atol=TOL) 33 | assert (permuted.pbc == atoms.pbc[permutation]).all() 34 | -------------------------------------------------------------------------------- /tests/standardization.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numpy.testing import assert_allclose 3 | from evgraf.utils import standardize 4 | from ase.build import bulk 5 | 6 | 7 | TOL = 1E-10 8 | 9 | 10 | def test_standarization(): 11 | 12 | # prepare input 13 | size = 3 14 | atoms = bulk("NaCl", "rocksalt", a=5.64) * size 15 | L = np.array([[1, 0, 0], [1, 1, 0], [2, 0, 1]]) 16 | assert np.linalg.det(L) == 1 17 | atoms.set_cell(L @ atoms.cell, scale_atoms=False) 18 | atoms.set_positions(atoms.get_positions() + 1.23456789) 19 | atoms.wrap(eps=0) 20 | 21 | # standardize 22 | std = standardize(atoms, subtract_barycenter=True) 23 | 24 | # check atomic numbers 25 | assert (std.atoms.numbers == np.sort(atoms.numbers)).all() 26 | assert (np.sort(atoms.numbers) == std.atoms.numbers).all() 27 | assert (atoms.numbers[std.zpermutation] == std.atoms.numbers).all() 28 | 29 | # check unit cell 30 | assert_allclose(np.linalg.det(std.invop), np.linalg.det(L), atol=TOL) 31 | assert_allclose(std.invop @ std.atoms.cell, atoms.cell, atol=TOL) 32 | 33 | # check atomic positions 34 | inverse_permutation = np.argsort(std.zpermutation) 35 | reverted = std.atoms[inverse_permutation] 36 | reverted.set_cell(std.invop @ reverted.cell, scale_atoms=False) 37 | reverted.set_cell(reverted.cell, scale_atoms=True) 38 | reverted.set_positions(reverted.get_positions() + std.barycenter) 39 | reverted.wrap(eps=0) 40 | p1 = reverted.get_positions() 41 | p0 = atoms.get_positions() 42 | assert_allclose(p0, p1, atol=TOL) 43 | -------------------------------------------------------------------------------- /tests/standardize_cell.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import numpy as np 3 | from numpy.testing import assert_allclose 4 | from evgraf.utils import standardize_cell 5 | 6 | 7 | TOL = 1E-10 8 | 9 | 10 | @pytest.mark.parametrize("seed", range(40)) 11 | def test_cell_standardization(seed): 12 | rng = np.random.RandomState(seed=seed) 13 | cell = rng.uniform(-1, 1, (3, 3)) 14 | Q, R = standardize_cell(cell) 15 | assert np.linalg.det(Q) > 0 16 | assert_allclose(cell, R @ Q.T, atol=TOL) 17 | -------------------------------------------------------------------------------- /tests/subgroup_enumeration.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import itertools 3 | import numpy as np 4 | from evgraf import subgroup_enumeration 5 | 6 | 7 | @pytest.mark.parametrize("dim", [1, 2, 3]) 8 | def test_subgroup_elements(dim): 9 | for dims in itertools.product(range(8), repeat=dim): 10 | dims = np.array(dims) 11 | bases = list(subgroup_enumeration.enumerate_subgroup_bases(dims)) 12 | assert len(bases) == subgroup_enumeration.count_subgroups(dims) 13 | 14 | for H in bases: 15 | elements = subgroup_enumeration.get_subgroup_elements(dims, H) 16 | assert len(set([tuple(e) for e in elements])) == len(elements) 17 | # when numpy dependency is at least 1.13 we can write: 18 | # assert elements.shape == np.unique(elements, axis=0).shape 19 | group_index = np.prod(dims // np.diag(H)) 20 | assert group_index == len(elements) 21 | -------------------------------------------------------------------------------- /tests/wrap_positions.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import numpy as np 3 | from numpy.testing import assert_allclose 4 | from ase.build import bulk, mx2, nanotube 5 | from evgraf.utils import permute_axes, standardize 6 | from evgrafcpp import wrap_positions 7 | 8 | 9 | TOL = 1E-10 10 | 11 | 12 | def check_result(atoms, positions, wrapped, translate): 13 | indices = np.where(atoms.pbc) 14 | scaled = atoms.cell.scaled_positions(wrapped) 15 | 16 | if translate: 17 | assert (scaled[:, indices] >= 0).all() 18 | assert (scaled[:, indices] < 1).all() 19 | assert_allclose(positions, wrapped, atol=TOL) 20 | else: 21 | assert (scaled[:, indices] >= -TOL).all() 22 | assert (scaled[:, indices] < 1 + TOL).all() 23 | 24 | 25 | def prepare(seed, i, translate, atoms): 26 | rng = np.random.RandomState(seed=seed) 27 | 28 | atoms = standardize(atoms).atoms 29 | permutation = np.roll(np.arange(3), i) 30 | atoms = permute_axes(atoms, permutation) 31 | if translate: 32 | atoms.positions += rng.uniform(-1, 1, atoms.positions.shape) 33 | atoms.wrap(eps=0) 34 | 35 | positions = atoms.get_positions(wrap=False) 36 | offset = rng.randint(-5, 6, positions.shape) 37 | for i in range(3): 38 | if not atoms.pbc[i]: 39 | offset[:, i] = 0 40 | scattered = positions.copy() + offset @ atoms.cell 41 | 42 | wrapped = wrap_positions(scattered, atoms.cell, atoms.pbc) 43 | check_result(atoms, positions, wrapped, translate) 44 | 45 | 46 | # 3-dimensional: NaCl 47 | @pytest.mark.parametrize("seed", range(3)) 48 | @pytest.mark.parametrize("translate", [False, True]) 49 | def test_nacl(seed, translate): 50 | size = 2 51 | atoms = bulk("NaCl", "rocksalt", a=5.64) * size 52 | prepare(seed, 0, translate, atoms) 53 | 54 | 55 | # 2-dimensional: MoS2 56 | @pytest.mark.parametrize("seed", range(3)) 57 | @pytest.mark.parametrize("i", range(3)) 58 | @pytest.mark.parametrize("translate", [False, True]) 59 | def test_mos2(seed, i, translate): 60 | size = 2 61 | atoms = mx2(formula='MoS2', size=(size, size, 1)) 62 | prepare(seed, i, translate, atoms) 63 | 64 | 65 | # 1-dimensional: carbon nanotube 66 | @pytest.mark.parametrize("seed", range(3)) 67 | @pytest.mark.parametrize("i", range(3)) 68 | @pytest.mark.parametrize("translate", [False, True]) 69 | def test_nanotube(seed, i, translate): 70 | size = 4 71 | atoms = nanotube(3, 3, length=size) 72 | prepare(seed, i, translate, atoms) 73 | --------------------------------------------------------------------------------