def apply(self, positions, rvecs=None): """Computes cluster centers of mass For periodic systems, clusters are not allowed to contain relative vectors which are longer than half of the smallest diagonal box vector component in its reduced representation (i.e. a_x, b_y, or c_z). A ValueError is raised if this is the case. Parameters ---------- positions : 2darray of shape (natom, 3) [angstrom] atomic positions. rvecs : 2darray of shape (3, 3) [angstrom] or None box vectors of the configuration, if applicable. """ dvecs = self.deltas @ positions if rvecs is not None: # apply mic to dvecs apply_mic(dvecs, rvecs) rcut = determine_rcut(rvecs) if not np.all(np.linalg.norm(dvecs, axis=1) < rcut): raise ValueError('Maximum allowed size of distance vectors' ' was exceeded (rcut = {}).'.format(rcut)) positions_com = np.zeros((self.nclusters, 3)) for i, group in enumerate(self): positions_com[i, :] = positions[group[0], :].copy() positions_com += self.transform @ dvecs return positions_com
def test_mic(): length = 20 rvecs = length * np.eye(3) rvecs[1, 0] = 2 * length + 0.2 # create strongly triclinic box vectors = np.random.uniform(-6, 6, size=(10000, 3)) deltas = vectors.copy() apply_mic(deltas, rvecs) # vectors already satisfy the mic assert np.allclose(vectors, deltas) deltas += np.random.randint(-3, 3, size=(1, 3)).dot(rvecs) apply_mic(deltas, rvecs) # vectors already satisfy the mic assert np.allclose(vectors, deltas)
def test_distance_matrix(): natoms = 10 positions = np.random.uniform(-4, 4, size=(natoms, 3)) distances = np.zeros((natoms, natoms)) for i in range(natoms): for j in range(natoms): distances[i, j] = np.linalg.norm(positions[i, :] - positions[j, :]) assert np.allclose(distances, compute_distance_matrix(positions)) distances = np.zeros((natoms, natoms)) rvecs = 6 * np.eye(3) + np.random.uniform(-1, 1, size=(3, 3)) for i in range(natoms): for j in range(natoms): delta = positions[i, :] - positions[j, :] apply_mic(delta.reshape(1, 3), rvecs) distances[i, j] = np.linalg.norm(delta) assert np.allclose(distances, compute_distance_matrix(positions, rvecs))
def check_mic(self, positions, rvecs, translate_atoms=True): """Verifies whether clusters are sufficiently small as to apply the mic For each cluster, the largest relative vector between member atoms is determined and compared with the allowed cutoff of the unit cell. Parameters ---------- positions : 2darray of shape (natom, 3) [angstrom] atomic positions. rvecs : 2darray of shape (3, 3) [angstrom] box vectors of the configuration translate_atoms : bool determines whether to translate atoms such that individual clusters do not require the box vectors for the computation of their center of mass. """ rcut = determine_rcut(rvecs) for group in self: n = len(group) if n > 1: r = group[0] # take random central atom as reference deltas = np.zeros((n, self.natoms)) for i in range(n): deltas[i, r] -= 1 deltas[i, group[i]] += 1 # becomes zero for index == r dvecs = deltas @ positions apply_mic(dvecs, rvecs) group_pos = np.zeros((n, 3)) # construct pos with dvecs group_pos = positions[r, :].reshape((1, 3)) + dvecs if translate_atoms: positions[group, :] = group_pos[:] # iterate over all relative vectors for k in range(n): for l in range(n): d = np.linalg.norm(group_pos[k, :] - group_pos[l, :]) if d > rcut: return False return True
def generate_environments(mapping, harmonic, cutoff, tol=1e-1): """Generates ``ClusterEnvironment`` instances based on cluster positions An ASE neighborlist object is used to identify clusters within a certain cutoff radius. Care is taken to ensure that no cluster resides close to the cutoff radius, as this may cause similar environments to contain a different number of atoms. To achieve this, a 'radius' is determined that is smaller than or equal to the cutoff but for which it is guaranteed that clusters are far enough from the boundary and do not cause any issues. The radius is determined per cluster_type. Parameters ---------- mapping : easymap.Mapping mapping for which to generate a list of candidates harmonic : easymap.Harmonic contains the structure based on which the neighbor list is generated cutoff: float [angstrom] cutoff of the neighborlist used to construct the environment tol : float [angstrom] distance threshold above which atoms are considered distinguishable """ logger.debug('\tgenerating neighbor list') cell = harmonic.atoms.get_cell() if np.allclose(cell.lengths(), np.zeros(3)): rvecs = None else: rvecs = cell positions = mapping.apply(harmonic.atoms.get_positions(), rvecs) nlist = get_nlist(positions, rvecs, cutoff=cutoff) # build lookup table of cluster_types table = {} for i, cluster_type in mapping.cluster_types.items(): if cluster_type not in table.keys(): table[cluster_type] = [] table[cluster_type].append(i) environments = {} # environments per cluster_type radii = {} # determined environment radii per cluster_type for cluster_type, cluster_indices in table.items(): logger.debug( '\tbuilding environments for cluster type {}'.format(cluster_type)) environments[cluster_type] = [] indices_distances = [] for i in cluster_indices: # parse nlist for every cluster indices, _ = nlist.get_neighbors(i) if len(indices) > 0: _dist_indices = np.array([(i, j) for j in indices]) distances = get_distances(_dist_indices, positions, rvecs) indices_distances.append((i, indices, distances), ) else: # add empty entry indices_distances.append((i, np.array([]), np.array([]))) # search for proper radius value based on all distances _all_distances = [d for (_, __, d) in indices_distances] _all_distances = np.concatenate(_all_distances) radius = cutoff - 2 * tol # start safely below cutoff if len(_all_distances) > 0: while np.min(np.abs(_all_distances - radius)) < 2 * tol: radius -= 0.1 * tol radii[cluster_type] = radius logger.debug('\tdetermined safe radius at {} angstrom'.format(radius)) for i, indices, distances in indices_distances: within_radius = indices[np.where(distances < radius)[0]].astype( np.int32) within_radius_all = np.concatenate(( np.array([i]), within_radius, )) types = [mapping.cluster_types[j] for j in within_radius_all] pos = positions[within_radius_all] if rvecs is not None: # apply minimum image convention deltas = pos - positions[i, :].reshape(1, -1) apply_mic(deltas, rvecs) assert np.allclose(np.zeros(3), deltas[0]) pos = deltas + positions[i, :].reshape(1, -1) env = ClusterEnvironment( pos, types, within_radius_all, ) environments[cluster_type].append(env) return environments, radii