def find_mic(v, cell, pbc=True): """Finds the minimum-image representation of vector(s) v using either one of two find mic algorithms depending on the given cell, v and pbc.""" pbc = cell.any(1) & pbc2pbc(pbc) dim = np.sum(pbc) v = np.asarray(v) single = v.ndim == 1 v = np.atleast_2d(v) if dim > 0: naive_find_mic_is_safe = False if dim == 3: vmin, vlen = naive_find_mic(v, cell) # naive find mic is safe only for the following condition if (vlen < 0.5 * min(cell.lengths())).all(): naive_find_mic_is_safe = True # hence skip Minkowski reduction if not naive_find_mic_is_safe: vmin, vlen = general_find_mic(v, cell, pbc=pbc) else: vmin = v.copy() vlen = np.linalg.norm(vmin, axis=1) if single: return vmin[0], vlen[0] else: return vmin, vlen
def find_mic(v, cell, pbc=True): """Finds the minimum-image representation of vector(s) v""" pbc = cell.any(1) & pbc2pbc(pbc) v = np.array(v) single = len(v.shape) == 1 v = np.atleast_2d(v) if np.sum(pbc) > 0: cell = complete_cell(cell) rcell, _ = minkowski_reduce(cell, pbc=pbc) # in a Minkowski-reduced cell we only need to test nearest neighbors cs = [np.arange(-1 * p, p + 1) for p in pbc] neighbor_cells = list(itertools.product(*cs)) positions = wrap_positions(v, rcell, pbc=pbc, eps=0) vmin = positions.copy() vlen = np.linalg.norm(positions, axis=1) for nbr in neighbor_cells: trial = positions + np.dot(rcell.T, nbr) trial_len = np.linalg.norm(trial, axis=1) indices = np.where(trial_len < vlen) vmin[indices] = trial[indices] vlen[indices] = trial_len[indices] else: vmin = v.copy() vlen = np.linalg.norm(vmin, axis=1) if single: return vmin[0], vlen[0] else: return vmin, vlen
def __init__(self, n_top=None, dE=1.0, cos_dist_max=5e-3, rcut=20., binwidth=0.05, sigma=0.02, nsigma=4, pbc=True, maxdims=None, recalculate=False): self.n_top = n_top or 0 self.dE = dE self.cos_dist_max = cos_dist_max self.rcut = rcut self.binwidth = binwidth self.pbc = pbc2pbc(pbc) if maxdims is None: self.maxdims = [None] * 3 else: self.maxdims = maxdims self.sigma = sigma self.nsigma = nsigma self.recalculate = recalculate self.dimensions = self.pbc.sum() if self.dimensions == 1 or self.dimensions == 2: for direction in range(3): if not self.pbc[direction]: if self.maxdims[direction] is not None: if self.maxdims[direction] <= 0: e = '''If a max thickness is specificed in maxdims for a non-periodic direction, it has to be strictly positive.''' raise ValueError(e)
def minkowski_reduce(self): """Minkowski-reduce this cell, returning new cell and mapping. See also :func:`ase.geometry.minkowski_reduction.minkowski_reduce`.""" from ase.geometry.minkowski_reduction import minkowski_reduce cell, op = minkowski_reduce(self, self.any(1) & pbc2pbc(self._pbc)) result = Cell(cell) result._pbc = self._pbc.copy() return result, op
def get_bravais_lattice(self, eps=2e-4, *, pbc=True): """Return :class:`~ase.lattice.BravaisLattice` for this cell: >>> cell = Cell.fromcellpar([4, 4, 4, 60, 60, 60]) >>> print(cell.get_bravais_lattice()) FCC(a=5.65685) .. note:: The Bravais lattice object follows the AFlow conventions. ``cell.get_bravais_lattice().tocell()`` may differ from the original cell by a permutation or other operation which maps it to the AFlow convention. For example, the orthorhombic lattice enforces a < b < c. To build a bandpath for a particular cell, use :meth:`ase.cell.Cell.bandpath` instead of this method. This maps the kpoints back to the original input cell. """ from ase.lattice import identify_lattice pbc = self.any(1) & pbc2pbc(pbc) lat, op = identify_lattice(self, eps=eps, pbc=pbc) return lat
def wrap_positions(positions, cell, pbc=True, center=(0.5, 0.5, 0.5), pretty_translation=False, eps=1e-7): """Wrap positions to unit cell. Returns positions changed by a multiple of the unit cell vectors to fit inside the space spanned by these vectors. See also the :meth:`ase.Atoms.wrap` method. Parameters: positions: float ndarray of shape (n, 3) Positions of the atoms cell: float ndarray of shape (3, 3) Unit cell vectors. pbc: one or 3 bool For each axis in the unit cell decides whether the positions will be moved along this axis. center: three float The positons in fractional coordinates that the new positions will be nearest possible to. pretty_translation: bool Translates atoms such that fractional coordinates are minimized. eps: float Small number to prevent slightly negative coordinates from being wrapped. Example: >>> from ase.geometry import wrap_positions >>> wrap_positions([[-0.1, 1.01, -0.5]], ... [[1, 0, 0], [0, 1, 0], [0, 0, 4]], ... pbc=[1, 1, 0]) array([[ 0.9 , 0.01, -0.5 ]]) """ if not hasattr(center, '__len__'): center = (center, ) * 3 pbc = pbc2pbc(pbc) shift = np.asarray(center) - 0.5 - eps # Don't change coordinates when pbc is False shift[np.logical_not(pbc)] = 0.0 assert np.asarray(cell)[np.asarray(pbc)].any(axis=1).all(), (cell, pbc) cell = complete_cell(cell) fractional = np.linalg.solve(cell.T, np.asarray(positions).T).T - shift if pretty_translation: fractional = translate_pretty(fractional, pbc) shift = np.asarray(center) - 0.5 shift[np.logical_not(pbc)] = 0.0 fractional += shift else: for i, periodic in enumerate(pbc): if periodic: fractional[:, i] %= 1.0 fractional[:, i] += shift[i] return np.dot(fractional, cell)
def get_2d_bravais_lattice(origcell, eps=2e-4, *, pbc=True): pbc = origcell.any(1) & pbc2pbc(pbc) if list(pbc) != [1, 1, 0]: raise UnsupportedLattice( 'Can only get 2D Bravais lattice of cell with ' 'pbc==[1, 1, 0]; but we have {}'.format(pbc)) nonperiodic = pbc.argmin() # Start with op = I ops = [np.eye(3)] for i in range(-1, 1): for j in range(-1, 1): op = [[1, j], [i, 1]] if np.abs(np.linalg.det(op)) > 1e-5: # Only touch periodic dirs: op = np.insert(op, nonperiodic, [0, 0], 0) op = np.insert(op, nonperiodic, ~pbc, 1) ops.append(np.array(op)) def allclose(a, b): return np.allclose(a, b, atol=eps) symrank = 0 for op in ops: cell = Cell(op.dot(origcell)) cellpar = cell.cellpar() angles = cellpar[3:] # Find a, b and gamma gamma = angles[~pbc][0] a, b = cellpar[:3][pbc] anglesm90 = np.abs(angles - 90) # Maximum one angle different from 90 deg in 2d please if np.sum(anglesm90 > eps) > 1: continue all_lengths_equal = abs(a - b) < eps if all_lengths_equal: if allclose(gamma, 90): lat = SQR(a) rank = 5 elif allclose(gamma, 120): lat = HEX2D(a) rank = 4 else: lat = CRECT(a, gamma) rank = 3 else: if allclose(gamma, 90): lat = RECT(a, b) rank = 2 else: lat = OBL(a, b, gamma) rank = 1 op = lat.get_transformation(origcell, eps=eps) if not allclose( np.dot(op, lat.tocell())[pbc][:, pbc], origcell.array[pbc][:, pbc]): msg = ('Cannot recognize cell at all somehow! {}, {}, {}'.format( a, b, gamma)) raise RuntimeError(msg) if rank > symrank: finalop = op symrank = rank finallat = lat return finallat, finalop.T
def identify_lattice(cell, eps=2e-4, *, pbc=True): """Find Bravais lattice representing this cell. Returns Bravais lattice object representing the cell along with an operation that, applied to the cell, yields the same lengths and angles as the Bravais lattice object.""" pbc = cell.any(1) & pbc2pbc(pbc) npbc = sum(pbc) if npbc == 1: i = np.argmax(pbc) # index of periodic axis a = cell[i, i] if a < 0 or cell[i, [i - 1, i - 2]].any(): raise ValueError( 'Not a 1-d cell ASE can handle: {cell}.'.format(cell=cell)) if i == 0: op = np.eye(3) elif i == 1: op = np.array([[0, 1, 0], [1, 0, 0], [0, 0, 1]]) else: op = np.array([[0, 0, 1], [0, 1, 0], [1, 0, 0]]) return LINE(a), op if npbc == 2: lat, op = get_2d_bravais_lattice(cell, eps, pbc=pbc) return lat, op if npbc != 3: raise ValueError('System must be periodic either ' 'along all three axes, ' 'along two first axes or, ' 'along the third axis. ' 'Got pbc={}'.format(pbc)) from ase.geometry.bravais_type_engine import niggli_op_table if cell.rank < 3: raise ValueError('Expected 3 linearly independent cell vectors') rcell, reduction_op = cell.niggli_reduce(eps=eps) # We tabulate the cell's Niggli-mapped versions so we don't need to # redo any work when the same Niggli-operation appears multiple times # in the table: memory = {} # We loop through the most symmetric kinds (CUB etc.) and return # the first one we find: for latname in LatticeChecker.check_order: # There may be multiple Niggli operations that produce valid # lattices, at least for MCL. In that case we will pick the # one whose angle is closest to 90, but it means we cannot # just return the first one we find so we must remember then: matching_lattices = [] for op_key in niggli_op_table[latname]: checker_and_op = memory.get(op_key) if checker_and_op is None: normalization_op = np.array(op_key).reshape(3, 3) candidate = Cell(np.linalg.inv(normalization_op.T) @ rcell) checker = LatticeChecker(candidate, eps=eps) memory[op_key] = (checker, normalization_op) else: checker, normalization_op = checker_and_op lat = checker.query(latname) if lat is not None: op = normalization_op @ np.linalg.inv(reduction_op) matching_lattices.append((lat, op)) # Among any matching lattices, return the one with lowest # orthogonality defect: best = None best_defect = np.inf for lat, op in matching_lattices: cell = lat.tocell() lengths = cell.lengths() defect = np.prod(lengths) / cell.volume if defect < best_defect: best = lat, op best_defect = defect if best is not None: return best
def minkowski_reduce(cell, pbc=True): """Calculate a Minkowski-reduced lattice basis. The reduced basis has the shortest possible vector lengths and has norm(a) <= norm(b) <= norm(c). Implements the method described in: Low-dimensional Lattice Basis Reduction Revisited Nguyen, Phong Q. and Stehlé, Damien, ACM Trans. Algorithms 5(4) 46:1--46:48, 2009 https://doi.org/10.1145/1597036.1597050 Parameters: cell: array The lattice basis to reduce (in row-vector format). pbc: array, optional The periodic boundary conditions of the cell (Default `True`). If `pbc` is provided, only periodic cell vectors are reduced. Returns: rcell: array The reduced lattice basis. op: array The unimodular matrix transformation (rcell = op @ cell). """ pbc = pbc2pbc(pbc) dim = pbc.sum() op = np.eye(3).astype(np.int) if dim == 2: perm = np.argsort(pbc, kind='merge')[::-1] # stable sort pcell = cell[perm][:, perm] norms = np.linalg.norm(pcell, axis=1) norms[2] = float("inf") indices = np.argsort(norms) op = op[indices] hu, hv = reduction_gauss(pcell, op[0], op[1]) op[0] = hu op[1] = hv invperm = np.argsort(perm) op = op[invperm][:, invperm] elif dim == 3: _, op = reduction_full(cell) # maintain cell handedness if dim == 3: if np.sign(np.linalg.det(cell)) != np.sign(np.linalg.det(op @ cell)): op = -op elif dim == 2: index = np.argmin(pbc) _cell = cell.copy() _cell[index] = (1, 1, 1) _rcell = op @ cell _rcell[index] = (1, 1, 1) if np.sign(np.linalg.det(_cell)) != np.sign(np.linalg.det(_rcell)): index = np.argmax(pbc) op[index] *= -1 norms1 = np.sort(np.linalg.norm(cell, axis=1)) norms2 = np.sort(np.linalg.norm(op @ cell, axis=1)) if not (norms2 <= norms1 + 1E-12).all(): raise RuntimeError("Minkowski reduction failed") return op @ cell, op
def minkowski_reduce(cell, pbc=True): """Calculate a Minkowski-reduced lattice basis. The reduced basis has the shortest possible vector lengths and has norm(a) <= norm(b) <= norm(c). Implements the method described in: Low-dimensional Lattice Basis Reduction Revisited Nguyen, Phong Q. and Stehlé, Damien, ACM Trans. Algorithms 5(4) 46:1--46:48, 2009 https://doi.org/10.1145/1597036.1597050 Parameters: cell: array The lattice basis to reduce (in row-vector format). pbc: array, optional The periodic boundary conditions of the cell (Default `True`). If `pbc` is provided, only periodic cell vectors are reduced. Returns: rcell: array The reduced lattice basis. op: array The unimodular matrix transformation (rcell = op @ cell). """ cell = Cell(cell) pbc = pbc2pbc(pbc) dim = pbc.sum() op = np.eye(3, dtype=int) if is_minkowski_reduced(cell, pbc): return cell, op if dim == 2: # permute cell so that first two vectors are the periodic ones perm = np.argsort(pbc, kind='merge')[::-1] # stable sort pcell = cell[perm][:, perm] # perform gauss reduction norms = np.linalg.norm(pcell, axis=1) norms[2] = float("inf") indices = np.argsort(norms) op = op[indices] hu, hv = reduction_gauss(pcell, op[0], op[1]) op[0] = hu op[1] = hv # undo above permutation invperm = np.argsort(perm) op = op[invperm][:, invperm] # maintain cell handedness index = np.argmin(pbc) normal = np.cross(cell[index - 2], cell[index - 1]) normal /= np.linalg.norm(normal) _cell = cell.copy() _cell[index] = normal _rcell = op @ cell _rcell[index] = normal if _cell.handedness != Cell(_rcell).handedness: op[index - 1] *= -1 elif dim == 3: _, op = reduction_full(cell) # maintain cell handedness if cell.handedness != Cell(op @ cell).handedness: op = -op norms1 = np.sort(np.linalg.norm(cell, axis=1)) norms2 = np.sort(np.linalg.norm(op @ cell, axis=1)) if (norms2 > norms1 + TOL).any(): raise RuntimeError("Minkowski reduction failed") return op @ cell, op
def is_minkowski_reduced(cell, pbc=True): """Tests if a cell is Minkowski-reduced. Parameters: cell: array The lattice basis to test (in row-vector format). pbc: array, optional The periodic boundary conditions of the cell (Default `True`). If `pbc` is provided, only periodic cell vectors are tested. Returns: is_reduced: bool True if cell is Minkowski-reduced, False otherwise. """ """These conditions are due to Minkowski, but a nice description in English can be found in the thesis of Carine Jaber: "Algorithmic approaches to Siegel's fundamental domain", https://www.theses.fr/2017UBFCK006.pdf This is also good background reading for Minkowski reduction. 0D and 1D cells are trivially reduced. For 2D cells, the conditions which an already-reduced basis fulfil are: |b1| ≤ |b2| |b2| ≤ |b1 - b2| |b2| ≤ |b1 + b2| For 3D cells, the conditions which an already-reduced basis fulfil are: |b1| ≤ |b2| ≤ |b3| |b1 + b2| ≥ |b2| |b1 + b3| ≥ |b3| |b2 + b3| ≥ |b3| |b1 - b2| ≥ |b2| |b1 - b3| ≥ |b3| |b2 - b3| ≥ |b3| |b1 + b2 + b3| ≥ |b3| |b1 - b2 + b3| ≥ |b3| |b1 + b2 - b3| ≥ |b3| |b1 - b2 - b3| ≥ |b3| """ pbc = pbc2pbc(pbc) dim = pbc.sum() if dim <= 1: return True if dim == 2: # reorder cell vectors to [shortest, longest, aperiodic] cell = cell.copy() cell[np.argmin(pbc)] = 0 norms = np.linalg.norm(cell, axis=1) cell = cell[np.argsort(norms)[[1, 2, 0]]] A = [[0, 1, 0], [1, -1, 0], [1, 1, 0]] lhs = np.linalg.norm(A @ cell, axis=1) norms = np.linalg.norm(cell, axis=1) rhs = norms[[0, 1, 1]] else: A = [[0, 1, 0], [0, 0, 1], [1, 1, 0], [1, 0, 1], [0, 1, 1], [1, -1, 0], [1, 0, -1], [0, 1, -1], [1, 1, 1], [1, -1, 1], [1, 1, -1], [1, -1, -1]] lhs = np.linalg.norm(A @ cell, axis=1) norms = np.linalg.norm(cell, axis=1) rhs = norms[[0, 1, 1, 2, 2, 1, 2, 2, 2, 2, 2, 2]] return (lhs >= rhs - TOL).all()