def test_cell_wrap(self): """Test that coordinates are correctly wrapped inside the cell. """ system = System( scaled_positions=[[0.0, 0.0, 0.0], [0.5, 0.5, 0.5]], symbols=["H", "H"], cell=[ [1, 0, 0], [0, 1, 0], [0, 0, 1] ], pbc = [True, True, True], ) #orig = np.array([[2, 1.45, -4.8]]) orig = np.array([[0.5, 0.5, 1.5]]) scal = system.to_scaled(orig, wrap = True) cart = system.to_cartesian(scal) self.assertFalse(np.allclose(orig, cart)) scal2 = system.to_scaled(orig) cart2 = system.to_cartesian(scal2, wrap = True) self.assertFalse(np.allclose(orig, cart2)) scal3 = system.to_scaled(orig, True) cart3 = system.to_cartesian(scal3, wrap = True) self.assertFalse(np.allclose(orig, cart3)) scal4 = system.to_scaled(orig) cart4 = system.to_cartesian(scal4) self.assertTrue(np.allclose(orig, cart4)) self.assertTrue(np.allclose(cart2, cart3))
def test_distances(self): """Tests that the periodicity is taken into account when calculating distances. """ scaled_positions = [[0.0, 0.0, 0.0], [0.5, 0.5, 0.5]] system = System( scaled_positions=scaled_positions, symbols=["H", "H"], cell=[ [5, 5, 0], [0, -5, -5], [5, 0, 5] ], ) disp = system.get_displacement_tensor() # For a non-periodic system, periodicity should not be taken into # account even if cell is defined. pos = system.get_positions() assumed = np.array([ [pos[0] - pos[0], pos[1] - pos[0]], [pos[0] - pos[1], pos[1] - pos[1]], ]) self.assertTrue(np.allclose(assumed, disp)) # For a periodic system, the nearest copy should be considered when # comparing distances to neighbors or to self system.set_pbc([True, True, True]) disp = system.get_displacement_tensor() assumed = np.array([ [[5.0, 5.0, 0.0], [5, 0, 0]], [[-5, 0, 0], [5.0, 5.0, 0.0]]]) self.assertTrue(np.allclose(assumed, disp)) # Tests that the displacement tensor is found correctly even for highly # non-orthorhombic systems. positions = np.array([ [1.56909, 2.71871, 6.45326], [3.9248, 4.07536, 6.45326] ]) cell = np.array([ [4.7077, -2.718, 0.], [0., 8.15225, 0.], [0., 0., 50.] ]) system = System( positions=positions, symbols=["H", "H"], cell=cell, pbc=True, ) # Fully periodic with minimum image convention dist_mat = system.get_distance_matrix() distance = dist_mat[0, 1] # The minimum image should be within the same cell expected = np.linalg.norm(positions[0, :] - positions[1, :]) self.assertTrue(np.allclose(distance, expected))
def test_transformations(self): """Test that coordinates are correctly transformed from scaled to cartesian and back again. """ system = System( scaled_positions=[[0.0, 0.0, 0.0], [0.5, 0.5, 0.5]], symbols=["H", "H"], cell=[[5, 5, 0], [0, -5, -5], [5, 0, 5]], ) orig = np.array([[2, 1.45, -4.8]]) scal = system.to_scaled(orig) cart = system.to_cartesian(scal) self.assertTrue(np.allclose(orig, cart))
def test_atoms_to_system(self): """Tests that an ASE Atoms is succesfully converted to a System object. """ class NaClFactory(SimpleCubicFactory): "A factory for creating NaCl (B1, Rocksalt) lattices." bravais_basis = [[0, 0, 0], [0, 0, 0.5], [0, 0.5, 0], [0, 0.5, 0.5], [0.5, 0, 0], [0.5, 0, 0.5], [0.5, 0.5, 0], [0.5, 0.5, 0.5]] element_basis = (0, 1, 1, 0, 1, 0, 0, 1) nacl = NaClFactory()(symbol=["Na", "Cl"], latticeconstant=5.6402) system = System.from_atoms(nacl) self.assertTrue( np.array_equal(nacl.get_positions(), system.get_positions())) self.assertTrue( np.array_equal(nacl.get_initial_charges(), system.get_initial_charges())) self.assertTrue( np.array_equal(nacl.get_atomic_numbers(), system.get_atomic_numbers())) self.assertTrue( np.array_equal(nacl.get_chemical_symbols(), system.get_chemical_symbols())) self.assertTrue(np.array_equal(nacl.get_cell(), system.get_cell())) self.assertTrue(np.array_equal(nacl.get_pbc(), system.get_pbc())) self.assertTrue( np.array_equal(nacl.get_scaled_positions(), system.get_scaled_positions()))
def test_system_input(self): """Tests that create takes internal system object. """ system = System.from_atoms(H2O) lmax = 5 nmax = 5 n_elems = 2 desc = SOAP(species=[1, 8], rcut=3, nmax=nmax, lmax=lmax, periodic=True) vec = desc.create(system)
def test_set_scaled_positions(self): """Test the method set_scaled_positions() of the System class """ scaled_positions = [[0.0, 0.0, 0.0], [0.5, 0.5, 0.5]] system = System( scaled_positions=scaled_positions, symbols=["H", "H"], cell=[ [5, 5, 0], [0, -5, -5], [5, 0, 5] ], ) pos = system.get_scaled_positions() new_pos = pos * 2 system.set_scaled_positions(new_pos) new_pos = system.get_scaled_positions() self.assertTrue(np.allclose(pos * 2, new_pos)) self.assertFalse(np.allclose(pos, new_pos))
def _get_k3(self, system, new_system, indices): """Calculates the second order terms where the scalar mapping is the inverse distance between atoms. Returns: 1D ndarray: flattened K2 values. """ grid = self.k3["grid"] start = grid["min"] stop = grid["max"] n = grid["n"] sigma = grid["sigma"] # Determine the weighting function and possible radial cutoff radial_cutoff = None weighting = self.k3.get("weighting") parameters = {} if weighting is not None: weighting_function = weighting["function"] if weighting_function == "exponential" or weighting_function == "exp": scale = weighting["scale"] cutoff = weighting["cutoff"] if scale != 0: radial_cutoff = -0.5 * math.log(cutoff) / scale parameters = {b"scale": scale, b"cutoff": cutoff} else: weighting_function = "unity" # Determine the geometry function geom_func_name = self.k3["geometry"]["function"] # Calculate extended system if self.periodic: centers_new = new_system.get_positions() centers_existing = system.get_positions()[indices] centers = np.concatenate((centers_new, centers_existing), axis=0) ext_system, cell_indices = dscribe.utils.geometry.get_extended_system( system, radial_cutoff, centers, return_cell_indices=True, ) ext_system = System.from_atoms(ext_system) else: ext_system = system cell_indices = np.zeros((len(system), 3), dtype=int) cmbtr = MBTRWrapper(self.atomic_number_to_index, self._interaction_limit, cell_indices) # If radial cutoff is finite, use it to calculate the sparse # distance matrix to reduce computational complexity from O(n^2) to # O(n log(n)) fin_system = ext_system + new_system n_atoms_ext = len(ext_system) n_atoms_fin = len(fin_system) n_atoms_new = len(new_system) ext_pos = ext_system.get_positions() new_pos = new_system.get_positions() if radial_cutoff is not None: # Calculate distance within the extended system dmat_ext_to_ext = ext_system.get_distance_matrix_within_radius( radial_cutoff, pos=ext_pos) col = dmat_ext_to_ext.col row = dmat_ext_to_ext.row data = dmat_ext_to_ext.data dmat = scipy.sparse.coo_matrix((data, (row, col)), shape=(n_atoms_fin, n_atoms_fin)) # Calculate the distances from the new positions to atoms in the # extended system using the cutoff if len(new_pos) != 0: dmat_ext_to_new = ext_system.get_distance_matrix_within_radius( radial_cutoff, pos=new_pos) col = dmat_ext_to_new.col row = dmat_ext_to_new.row data = dmat_ext_to_new.data dmat.col = np.append(dmat.col, col + n_atoms_ext) dmat.row = np.append(dmat.row, row) dmat.data = np.append(dmat.data, data) dmat.col = np.append(dmat.col, row) dmat.row = np.append(dmat.row, col + n_atoms_ext) dmat.data = np.append(dmat.data, data) # Calculate adjacencies and transform to the dense matrix for # sending information to C++ adj_list = dscribe.utils.geometry.get_adjacency_list(dmat) dmat_dense = np.full( (n_atoms_fin, n_atoms_fin), sys.float_info.max ) # The non-neighbor values are treated as "infinitely far". dmat_dense[dmat.row, dmat.col] = dmat.data # If no weighting is used, the full distance matrix is calculated else: dmat = scipy.sparse.lil_matrix((n_atoms_fin, n_atoms_fin)) # Fill in block for extended system dmat_ext_to_ext = ext_system.get_distance_matrix() dmat[0:n_atoms_ext, 0:n_atoms_ext] = dmat_ext_to_ext # Fill in block for extended system to new system dmat_ext_to_new = scipy.spatial.distance.cdist(ext_pos, new_pos) dmat[0:n_atoms_ext, n_atoms_ext:n_atoms_ext + n_atoms_new] = dmat_ext_to_new dmat[n_atoms_ext:n_atoms_ext + n_atoms_new, 0:n_atoms_ext] = dmat_ext_to_new.T # Calculate adjacencies and the dense version dmat = dmat.tocoo() adj_list = dscribe.utils.geometry.get_adjacency_list(dmat) dmat_dense = np.full( (n_atoms_fin, n_atoms_fin), sys.float_info.max ) # The non-neighbor values are treated as "infinitely far". dmat_dense[dmat.row, dmat.col] = dmat.data # Form new indices that include the existing atoms and the newly added # ones indices = np.array(np.append( indices, [n_atoms_ext + i for i in range(n_atoms_new)]), dtype=int) k3_list = cmbtr.get_k3_local( indices, fin_system.get_atomic_numbers(), dmat_dense, adj_list, geom_func_name.encode(), weighting_function.encode(), parameters, start, stop, sigma, n, ) k3_list = self._make_new_klist_local(k3_list) # Depending on flattening, use either a sparse matrix or a dense one. n_elem = self.n_elements n_loc = len(indices) if self.flatten: k3 = lil_matrix((n_loc, int((n_elem * (3 * n_elem - 1) * n / 2))), dtype=np.float32) for i_loc, k3_map in enumerate(k3_list): for key, gaussian_sum in k3_map.items(): i = key[0] j = key[1] k = key[2] # This is the index of the spectrum. It is given by enumerating the # elements of a three-dimensional array and only considering # elements for which k>=i and i || j == 0. The enumeration begins # from [0, 0, 0], and ends at [n_elem, n_elem, n_elem], looping the # elements in the order k, i, j. if j == 0: m = k + i * n_elem - i * (i + 1) / 2 else: m = n_elem * (n_elem + 1) / 2 + (j - 1) * n_elem + k start = int(m * n) end = int((m + 1) * n) # Denormalize if requested if not self.normalize_gaussians: max_val = 1 / (sigma * math.sqrt(2 * math.pi)) gaussian_sum /= max_val k3[i_loc, start:end] = gaussian_sum else: k3 = np.zeros((n_loc, n_elem, n_elem, n_elem, n), dtype=np.float32) for i_loc, k3_map in enumerate(k3_list): for key, gaussian_sum in k3_map.items(): i = key[0] j = key[1] k = key[2] # Denormalize if requested if not self.normalize_gaussians: max_val = 1 / (sigma * math.sqrt(2 * math.pi)) gaussian_sum /= max_val k3[i_loc, i, j, k, :] = gaussian_sum return k3
def _get_k2(self, system, new_system, indices): """Calculates the second order terms where the scalar mapping is the inverse distance between atoms. Returns: 1D ndarray: flattened K2 values. """ grid = self.k2["grid"] start = grid["min"] stop = grid["max"] n = grid["n"] sigma = grid["sigma"] # Determine the weighting function and possible radial cutoff radial_cutoff = None weighting = self.k2.get("weighting") parameters = {} if weighting is not None: weighting_function = weighting["function"] if weighting_function == "exponential" or weighting_function == "exp": scale = weighting["scale"] cutoff = weighting["cutoff"] if scale != 0: radial_cutoff = -math.log(cutoff) / scale parameters = { b"scale": weighting["scale"], b"cutoff": weighting["cutoff"] } else: weighting_function = "unity" # Determine the geometry function geom_func_name = self.k2["geometry"]["function"] # Calculate extended system if self.periodic: centers = new_system.get_positions() ext_system, cell_indices = dscribe.utils.geometry.get_extended_system( system, radial_cutoff, centers, return_cell_indices=True, ) ext_system = System.from_atoms(ext_system) else: ext_system = system cell_indices = np.zeros((len(system), 3), dtype=int) cmbtr = MBTRWrapper(self.atomic_number_to_index, self._interaction_limit, cell_indices) # If radial cutoff is finite, use it to calculate the sparse distance # matrix to reduce computational complexity from O(n^2) to O(n log(n)). # If radial cutoff is not available, calculate full matrix. n_atoms_ext = len(ext_system) n_atoms_new = len(new_system) ext_pos = ext_system.get_positions() new_pos = new_system.get_positions() if radial_cutoff is not None: dmat = new_system.get_distance_matrix_within_radius(radial_cutoff, pos=ext_pos) adj_list = dscribe.utils.geometry.get_adjacency_list(dmat) dmat_dense = np.full( (n_atoms_new, n_atoms_ext), sys.float_info.max ) # The non-neighbor values are treated as "infinitely far". dmat_dense[dmat.row, dmat.col] = dmat.data else: dmat_dense = scipy.spatial.distance.cdist(new_pos, ext_pos) adj_list = np.tile(np.arange(n_atoms_ext), (n_atoms_new, 1)) # Form new indices that include the existing atoms and the newly added # ones indices = np.array(np.append( indices, [n_atoms_ext + i for i in range(n_atoms_new - len(indices))]), dtype=int) k2_list = cmbtr.get_k2_local( indices, ext_system.get_atomic_numbers(), dmat_dense, adj_list, geom_func_name.encode(), weighting_function.encode(), parameters, start, stop, sigma, n, ) k2_list = self._make_new_klist_local(k2_list) # Depending on flattening, use either a sparse matrix or a dense one. n_elem = self.n_elements n_loc = len(indices) if self.flatten: k2 = lil_matrix((n_loc, n_elem * n), dtype=np.float32) for i_loc, k2_map in enumerate(k2_list): for key, gaussian_sum in k2_map.items(): i = key[1] m = i start = int(m * n) end = int((m + 1) * n) # Denormalize if requested if not self.normalize_gaussians: max_val = 1 / (sigma * math.sqrt(2 * math.pi)) gaussian_sum /= max_val k2[i_loc, start:end] = gaussian_sum else: k2 = np.zeros((n_loc, n_elem, n), dtype=np.float32) for i_loc, k2_map in enumerate(k2_list): for key, gaussian_sum in k2_map.items(): i = key[1] # Denormalize if requested if not self.normalize_gaussians: max_val = 1 / (sigma * math.sqrt(2 * math.pi)) gaussian_sum /= max_val k2[i_loc, i, :] = gaussian_sum return k2
def create_single( self, system, positions, ): """Return the local many-body tensor representation for the given system and positions. Args: system (:class:`ase.Atoms` | :class:`.System`): Input system. positions (iterable): Positions or atom index of points, from which local_mbtr is created. Can be a list of integer numbers or a list of xyz-coordinates. If integers provided, the atoms at that index are used as centers. If positions provided, new atoms are added at that position. Returns: 1D ndarray: The local many-body tensor representations of given positions, for k terms, as an array. These are ordered as given in positions. """ # Transform the input system into the internal System-object system = self.get_system(system) # Check that the system does not have elements that are not in the list # of atomic numbers atomic_number_set = set(system.get_atomic_numbers()) self.check_atomic_numbers(atomic_number_set) self._interaction_limit = len(system) system_positions = system.get_positions() system_atomic_numbers = system.get_atomic_numbers() # Ensure that the atomic number 0 is not present in the system if 0 in atomic_number_set: raise ValueError( "Please do not use the atomic number 0 in local MBTR as it " "is reserved to mark the atoms use as analysis centers.") # Form a list of indices, positions and atomic numbers for the local # centers. k=3 and k=2 use a slightly different approach, so two # versions are built i_new = len(system) indices_k2 = [] new_pos_k2 = [] new_atomic_numbers_k2 = [] indices_k3 = [] new_pos_k3 = [] new_atomic_numbers_k3 = [] n_atoms = len(system) if positions is not None: n_loc = len(positions) # Check validity of position definitions and create final cartesian # position list if len(positions) == 0: raise ValueError( "The argument 'positions' should contain a non-empty set of" " atomic indices or cartesian coordinates with x, y and z " "components.") for i in positions: if np.issubdtype(type(i), np.integer): i_len = len(system) if i >= i_len or i < 0: raise ValueError( "The provided index {} is not valid for the system " "with {} atoms.".format(i, i_len)) indices_k2.append(i) indices_k3.append(i) new_pos_k2.append(system_positions[i]) new_atomic_numbers_k2.append(system_atomic_numbers[i]) elif isinstance(i, (list, tuple, np.ndarray)): if len(i) != 3: raise ValueError( "The argument 'positions' should contain a " "non-empty set of atomic indices or cartesian " "coordinates with x, y and z components.") new_pos_k2.append(np.array(i)) new_pos_k3.append(np.array(i)) new_atomic_numbers_k2.append(0) new_atomic_numbers_k3.append(0) i_new += 1 else: raise ValueError( "Create method requires the argument 'positions', a " "list of atom indices and/or positions.") # If positions are not supplied, it is assumed that each atom is used # as a center else: n_loc = n_atoms indices_k2 = np.arange(n_atoms) indices_k3 = np.arange(n_atoms) new_pos_k2 = system.get_positions() new_atomic_numbers_k2 = system.get_atomic_numbers() # Calculate the "raw" outputs for each term. mbtr = {} if self.k2 is not None: new_system_k2 = System( symbols=new_atomic_numbers_k2, positions=new_pos_k2, ) mbtr["k2"] = self._get_k2(system, new_system_k2, indices_k2) if self.k3 is not None: new_system_k3 = System( symbols=new_atomic_numbers_k3, positions=new_pos_k3, ) mbtr["k3"] = self._get_k3(system, new_system_k3, indices_k3) # Handle normalization if self.normalization == "l2_each": if self.flatten is True: for key, value in mbtr.items(): value_normalized = normalize(value, norm='l2', axis=1) mbtr[key] = value_normalized else: for key, value in mbtr.items(): for array in value: i_data = array.ravel() i_norm = np.linalg.norm(i_data) array /= i_norm # Flatten output if requested if self.flatten: length = 0 datas = [] rows = [] cols = [] for key in sorted(mbtr.keys()): tensor = mbtr[key] size = tensor.shape[1] coo = tensor.tocoo() datas.append(coo.data) rows.append(coo.row) cols.append(coo.col + length) length += size datas = np.concatenate(datas) rows = np.concatenate(rows) cols = np.concatenate(cols) result = coo_matrix((datas, (rows, cols)), shape=[n_loc, length], dtype=np.float32) # Make into a dense array if requested if not self.sparse: result = result.toarray() # Otherwise return a list of dictionaries, each dictionary containing # the requested unflattened tensors else: result = np.empty((n_loc), dtype='object') for i_loc in range(n_loc): i_dict = {} for key in mbtr.keys(): tensor = mbtr[key] i_dict[key] = tensor[i_loc] result[i_loc] = i_dict return result
def create_extended_system(self, primitive_system, term_number): """Used to create a periodically extended system, that is as small as possible by rejecting atoms for which the given weighting will be below the given threshold. Modified for the local MBTR to only consider distances from the central atom and to enable taking the virtual sites into account. Args: primitive_system (System): The original primitive system to duplicate. term_number (int): The term number of the tensor. For k=2, the max distance is x, for k>2, the distance is given by 2*x. Returns: System: The new system that is extended so that each atom can at most have a weight that is larger or equivalent to the given threshold. """ # We need to speciy that the relative positions should not be wrapped. # Otherwise the repeated systems may overlap with the positions taken # with get_positions() relative_pos = np.array( primitive_system.get_scaled_positions(wrap=False)) numbers = np.array(primitive_system.numbers) cartesian_pos = np.array(primitive_system.get_positions()) cell = np.array(primitive_system.get_cell()) # Determine the upper limit of how many copies we need in each cell # vector direction. We take as many copies as needed for the # exponential weight to come down to the given threshold. cell_vector_lengths = np.linalg.norm(cell, axis=1) n_copies_axis = np.zeros(3, dtype=int) weight_info = self.weighting["k{}".format(term_number)] weighting_function = weight_info["function"] cutoff = self.weighting["k{}".format(term_number)]["cutoff"] if weighting_function == "exponential": scale = weight_info["scale"] function = lambda x: np.exp(-scale * x) for i_axis, axis_length in enumerate(cell_vector_lengths): limit_found = False n_copies = -1 while (not limit_found): n_copies += 1 distance = n_copies * cell_vector_lengths[0] # For terms k>2 we double the distances to take into # account the "loop" that is required. if term_number > 2: distance = 2 * distance weight = function(distance) if weight < cutoff: n_copies_axis[i_axis] = n_copies limit_found = True # Create copies of the cell but keep track of the atoms in the # original cell num_extended = [] pos_extended = [] num_extended.append(numbers) pos_extended.append(cartesian_pos) a = np.array([1, 0, 0]) b = np.array([0, 1, 0]) c = np.array([0, 0, 1]) for i in range(-n_copies_axis[0], n_copies_axis[0] + 1): for j in range(-n_copies_axis[1], n_copies_axis[1] + 1): for k in range(-n_copies_axis[2], n_copies_axis[2] + 1): if i == 0 and j == 0 and k == 0: continue # Calculate the positions of the copied atoms and filter # out the atoms that are farther away than the given # cutoff. # If the given position is virtual and does not correspond # to a physical atom, the position is not repeated in the # copies. if self.virtual_positions and self._interaction_limit == 1: num_copy = np.array(numbers)[1:] pos_copy = np.array(relative_pos)[1:] # If the given position is not virtual and corresponds to # an actual physical atom, the ghost atom is repeated in # the extended system. else: num_copy = np.array(numbers) pos_copy = np.array(relative_pos) pos_shifted = pos_copy - i * a - j * b - k * c pos_copy_cartesian = np.dot(pos_shifted, cell) # Only distances to the atoms within the interaction limit # are considered. positions_to_consider = cartesian_pos[0:self. _interaction_limit] distances = cdist(pos_copy_cartesian, positions_to_consider) # For terms above k==2 we double the distances to take into # account the "loop" that is required. if term_number > 2: distances *= 2 weights = function(distances) weight_mask = weights >= cutoff # Create a boolean mask that says if the atom is within the # range from at least one atom in the original cell valids_mask = np.any(weight_mask, axis=1) valid_pos = pos_copy_cartesian[valids_mask] valid_num = num_copy[valids_mask] pos_extended.append(valid_pos) num_extended.append(valid_num) pos_extended = np.concatenate(pos_extended) num_extended = np.concatenate(num_extended) extended_system = System(positions=pos_extended, numbers=num_extended, cell=cell, pbc=False) return extended_system
def system_stats(system_iterator): """ Args: system_stats(iterable containing ASE.Atoms or System): The atomic systems for which to gather statistics. Returns: Dict: A dictionary of different statistics for the system. The dictionary will contain: n_atoms_max: The maximum number of atoms in a system. max_atomic_number: The highest atomic number min_atomic_number: The lowest atomic number atomic_numbers: List of present atomic numbers element_symbols: List of present atomic symbols min_distance: Minimum distance in the system """ n_atoms_max = 0 atomic_numbers = set() symbols = set() min_distance = None for system in system_iterator: n_atoms = len(system) # Make ASE.Atoms into a System object if isinstance(system, Atoms): system = System.from_atoms(system) i_atomic_numbers = set(system.get_atomic_numbers()) i_symbols = set(system.get_chemical_symbols()) distance_matrix = system.get_distance_matrix() # Gather atomic numbers and symbols atomic_numbers = atomic_numbers.union(i_atomic_numbers) symbols = symbols.union(i_symbols) # Gather maximum number of atoms if n_atoms > n_atoms_max: n_atoms_max = n_atoms # Gather min distance. For periodic systems we must also consider # distances from an atom to it's periodic copy, as given by # get_distance_matrix() on the diagonal. if np.any(system.get_pbc()): triu_indices = np.triu_indices(len(distance_matrix), k=0) else: triu_indices = np.triu_indices(len(distance_matrix), k=1) distances = distance_matrix[triu_indices] i_min_dist = distances.min() if min_distance is None or i_min_dist < min_distance: min_distance = i_min_dist return { "n_atoms_max": n_atoms_max, "max_atomic_number": max(list(atomic_numbers)), "min_atomic_number": min(list(atomic_numbers)), "atomic_numbers": list(atomic_numbers), "element_symbols": list(symbols), "min_distance": min_distance, }
def _get_k2(self, system): """Calculates the second order terms where the scalar mapping is the inverse distance between atoms. Returns: 1D ndarray: flattened K2 values. """ grid = self.k2["grid"] start = grid["min"] stop = grid["max"] n = grid["n"] sigma = grid["sigma"] # Determine the weighting function and possible radial cutoff radial_cutoff = None weighting = self.k2.get("weighting") parameters = {} if weighting is not None: weighting_function = weighting["function"] if weighting_function == "exponential" or weighting_function == "exp": scale = weighting["scale"] cutoff = weighting["cutoff"] if scale != 0: radial_cutoff = -math.log(cutoff) / scale parameters = {b"scale": scale, b"cutoff": cutoff} else: weighting_function = "unity" # Determine the geometry function geom_func_name = self.k2["geometry"]["function"] # If needed, create the extended system if self.periodic: centers = system.get_positions() ext_system, cell_indices = dscribe.utils.geometry.get_extended_system( system, radial_cutoff, centers, return_cell_indices=True) ext_system = System.from_atoms(ext_system) else: ext_system = system cell_indices = np.zeros((len(system), 3), dtype=int) cmbtr = MBTRWrapper(self.atomic_number_to_index, self._interaction_limit, cell_indices) # If radial cutoff is finite, use it to calculate the sparse # distance matrix to reduce computational complexity from O(n^2) to # O(n log(n)) n_atoms = len(ext_system) if radial_cutoff is not None: dmat = ext_system.get_distance_matrix_within_radius(radial_cutoff) adj_list = dscribe.utils.geometry.get_adjacency_list(dmat) dmat_dense = np.full( (n_atoms, n_atoms), sys.float_info.max ) # The non-neighbor values are treated as "infinitely far". dmat_dense[dmat.row, dmat.col] = dmat.data # If no weighting is used, the full distance matrix is calculated else: dmat_dense = ext_system.get_distance_matrix() adj_list = np.tile(np.arange(n_atoms), (n_atoms, 1)) k2_map = cmbtr.get_k2( ext_system.get_atomic_numbers(), dmat_dense, adj_list, geom_func_name.encode(), weighting_function.encode(), parameters, start, stop, sigma, n, ) k2_map = self._make_new_kmap(k2_map) # Depending of flattening, use either a sparse matrix or a dense one. n_elem = self.n_elements if self.flatten: k2 = lil_matrix((1, int(n_elem * (n_elem + 1) / 2 * n)), dtype=np.float32) else: k2 = np.zeros((self.n_elements, self.n_elements, n), dtype=np.float32) for key, gaussian_sum in k2_map.items(): i = key[0] j = key[1] # This is the index of the spectrum. It is given by enumerating the # elements of an upper triangular matrix from left to right and top # to bottom. m = int(j + i * n_elem - i * (i + 1) / 2) # Denormalize if requested if not self.normalize_gaussians: max_val = 1 / (sigma * math.sqrt(2 * math.pi)) gaussian_sum /= max_val if self.flatten: start = m * n end = (m + 1) * n k2[0, start:end] = gaussian_sum else: k2[i, j, :] = gaussian_sum return k2
def create(self, system, positions, scaled_positions=False): """Return the local many-body tensor representation for the given system and positions. Args: system (:class:`ase.Atoms` | :class:`.System`): Input system. positions (iterable): Positions or atom index of points, from which local_mbtr is created. Can be a list of integer numbers or a list of xyz-coordinates. scaled_positions (boolean): Controls whether the given positions are given as scaled to the unit cell basis or not. Scaled positions require that a cell is available for the system. Returns: 1D ndarray: The local many-body tensor representations of given positions, for k terms, as an array. These are ordered as given in positions. """ # Transform the input system into the internal System-object system = self.get_system(system) # Ensure that the atomic number 0 is not present in the system if 0 in system.get_atomic_numbers(): raise ValueError( "Please do not use the atomic number 0 in local MBTR " ", as it is reserved for the ghost atom used by the " "implementation.") # Ensuring self is updated self.update() # Checking scaled position if scaled_positions: if np.linalg.norm(system.get_cell()) == 0: raise ValueError( "System doesn't have cell to justify scaled positions.") # Figure out the atom index or atom location from the given positions systems = [] # If virtual positions requested, create new atoms with atomic number 0 # at the requested position. for i_pos in positions: if self.virtual_positions: if not isinstance(i_pos, (list, tuple, np.ndarray)): raise ValueError( "The given position of type '{}' could not be " "interpreted as a valid location. If you wish to use " "existing atoms as centers, please set " "'virtual_positions' to False.".format(type(i_pos))) if scaled_positions: i_pos = np.dot(i_pos, system.get_cell()) else: i_pos = np.array(i_pos) i_pos = np.expand_dims(i_pos, axis=0) new_system = System('X', positions=i_pos) new_system += system else: if not np.issubdtype(type(i_pos), np.integer): raise ValueError( "The given position of type '{}' could not be " "interpreted as a valid index. If you wish to use " "custom locations as centers, please set " "'virtual_positions' to True.".format(type(i_pos))) new_system = Atoms() center_atom = system[i_pos] new_system += center_atom new_system.set_atomic_numbers([0]) system_copy = system.copy() del system_copy[i_pos] new_system += system_copy # Set the periodicity and cell to match the original system, as # they are lost in the system concatenation new_system.set_cell(system.get_cell()) new_system.set_pbc(system.get_pbc()) systems.append(new_system) # Request MBTR for each position. Return type depends on flattening and # whether a spares of dense result is requested. n_pos = len(positions) n_features = self.get_number_of_features() if self._flatten and self._sparse: data = [] cols = [] rows = [] row_offset = 0 for i, i_system in enumerate(systems): i_res = super().create(i_system) data.append(i_res.data) rows.append(i_res.row + row_offset) cols.append(i_res.col) # Increase the row offset row_offset += 1 # Saves the descriptors as a sparse matrix data = np.concatenate(data) rows = np.concatenate(rows) cols = np.concatenate(cols) desc = coo_matrix((data, (rows, cols)), shape=(n_pos, n_features), dtype=np.float32) else: if self._flatten and not self._sparse: desc = np.empty((n_pos, n_features), dtype=np.float32) else: desc = np.empty((n_pos), dtype='object') for i, i_system in enumerate(systems): i_desc = super().create(i_system) desc[i] = i_desc return desc
from dscribe.core import System import matplotlib.pyplot as mpl import ase.data # Define the system under study: NaCl in a conventional cell. NaCl_conv = System( cell=[ [5.6402, 0.0, 0.0], [0.0, 5.6402, 0.0], [0.0, 0.0, 5.6402] ], scaled_positions=[ [0.0, 0.5, 0.0], [0.0, 0.5, 0.5], [0.0, 0.0, 0.5], [0.0, 0.0, 0.0], [0.5, 0.5, 0.5], [0.5, 0.5, 0.0], [0.5, 0.0, 0.0], [0.5, 0.0, 0.5] ], symbols=["Na", "Cl", "Na", "Cl", "Na", "Cl", "Na", "Cl"], ) # view(NaCl_conv) # Create a local MBTR desciptor around the atomic index 6 corresponding to a Na # atom decay_factor = 0.5 mbtr = LMBTR( species=[11, 17],
def create_extended_system(self, primitive_system, centers, radial_cutoff): """Used to create a periodically extended system, that is as small as possible by rejecting atoms for which the given weighting will be below the given threshold. Modified for the local MBTR to only consider distances from the central atom and to enable taking the virtual sites into account. Args: primitive_system (System): The original primitive system to duplicate. radial_cutoff (float): The radial cutoff to use in constructing the extended system. Returns: tuple: Tuple containing the new extended system as the first entry and the index of the periodically repeated cell for each atom as the second entry. The extended system is determined is extended so that each atom can at most have a weight that is larger or equivalent to the given threshold. """ # We need to specify that the relative positions should not be wrapped. # Otherwise the repeated systems may overlap with the positions taken # with get_positions() relative_pos = np.array( primitive_system.get_scaled_positions(wrap=False)) numbers = np.array(primitive_system.numbers) cartesian_pos = np.array(primitive_system.get_positions()) cell = np.array(primitive_system.get_cell()) # Determine the upper limit of how many copies we need in each cell # vector direction. We take as many copies as needed to reach the # radial cutoff. cell_vector_lengths = np.linalg.norm(cell, axis=1) n_copies_axis = np.zeros(3, dtype=int) cell_cuts = radial_cutoff / cell_vector_lengths n_copies_axis = np.ceil(cell_cuts).astype(np.int) # Create copies of the cell but keep track of the atoms in the # original cell num_extended = [] pos_extended = [] num_extended.append(numbers) pos_extended.append(cartesian_pos) a = np.array([1, 0, 0]) b = np.array([0, 1, 0]) c = np.array([0, 0, 1]) cell_indices = [np.zeros((len(primitive_system), 3), dtype=int)] for i in range(-n_copies_axis[0], n_copies_axis[0] + 1): for j in range(-n_copies_axis[1], n_copies_axis[1] + 1): for k in range(-n_copies_axis[2], n_copies_axis[2] + 1): if i == 0 and j == 0 and k == 0: continue # Calculate the positions of the copied atoms and filter # out the atoms that are farther away than the given # cutoff. num_copy = np.array(numbers) pos_copy = np.array(relative_pos) pos_shifted = pos_copy - i * a - j * b - k * c pos_copy_cartesian = np.dot(pos_shifted, cell) # Only distances to the atoms within the interaction limit # are considered. distances = cdist(pos_copy_cartesian, centers) weight_mask = distances < radial_cutoff # Create a boolean mask that says if the atom is within the # range from at least one atom in the original cell valids_mask = np.any(weight_mask, axis=1) if np.any(valids_mask): valid_pos = pos_copy_cartesian[valids_mask] valid_num = num_copy[valids_mask] valid_ind = np.tile(np.array([i, j, k], dtype=int), (len(valid_num), 1)) pos_extended.append(valid_pos) num_extended.append(valid_num) cell_indices.append(valid_ind) pos_extended = np.concatenate(pos_extended) num_extended = np.concatenate(num_extended) cell_indices = np.vstack(cell_indices) extended_system = System(positions=pos_extended, numbers=num_extended, cell=cell, pbc=False) return extended_system, cell_indices
def _get_k3(self, system): """Calculates the third order terms. Returns: 1D ndarray: flattened K3 values. """ grid = self.k3["grid"] start = grid["min"] stop = grid["max"] n = grid["n"] sigma = grid["sigma"] # Determine the weighting function and possible radial cutoff radial_cutoff = None weighting = self.k3.get("weighting") parameters = {} if weighting is not None: weighting_function = weighting["function"] if weighting_function == "exp" or weighting_function == "exponential": scale = weighting["scale"] threshold = weighting["threshold"] if scale != 0: radial_cutoff = -0.5 * math.log(threshold) / scale parameters = {b"scale": scale, b"threshold": threshold} else: weighting_function = "unity" # Determine the geometry function geom_func_name = self.k3["geometry"]["function"] # If needed, create the extended system if self.periodic: centers = system.get_positions() ext_system, cell_indices = dscribe.utils.geometry.get_extended_system( system, radial_cutoff, centers, return_cell_indices=True) ext_system = System.from_atoms(ext_system) else: ext_system = system cell_indices = np.zeros((len(system), 3), dtype=int) cmbtr = MBTRWrapper(self.atomic_number_to_index, self._interaction_limit, cell_indices) # If radial cutoff is finite, use it to calculate the sparse # distance matrix to reduce computational complexity from O(n^2) to # O(n log(n)) n_atoms = len(ext_system) if radial_cutoff is not None: dmat = ext_system.get_distance_matrix_within_radius(radial_cutoff) adj_list = dscribe.utils.geometry.get_adjacency_list(dmat) dmat_dense = np.full( (n_atoms, n_atoms), sys.float_info.max ) # The non-neighbor values are treated as "infinitely far". dmat_dense[dmat.col, dmat.row] = dmat.data # If no weighting is used, the full distance matrix is calculated else: dmat_dense = ext_system.get_distance_matrix() adj_list = np.tile(np.arange(n_atoms), (n_atoms, 1)) k3_map = cmbtr.get_k3( ext_system.get_atomic_numbers(), dmat_dense, adj_list, geom_func_name.encode(), weighting_function.encode(), parameters, start, stop, sigma, n, ) k3_map = self._make_new_kmap(k3_map) # Depending of flattening, use either a sparse matrix or a dense one. n_elem = self.n_elements if self.flatten: k3 = sparse.DOK((int(n_elem * n_elem * (n_elem + 1) / 2 * n)), dtype=np.float32) else: k3 = np.zeros((n_elem, n_elem, n_elem, n), dtype=np.float32) for key, gaussian_sum in k3_map.items(): i = key[0] j = key[1] k = key[2] # This is the index of the spectrum. It is given by enumerating the # elements of a three-dimensional array where for valid elements # k>=i. The enumeration begins from [0, 0, 0], and ends at [n_elem, # n_elem, n_elem], looping the elements in the order j, i, k. m = int(j * n_elem * (n_elem + 1) / 2 + k + i * n_elem - i * (i + 1) / 2) # Denormalize if requested if not self.normalize_gaussians: max_val = 1 / (sigma * math.sqrt(2 * math.pi)) gaussian_sum /= max_val if self.flatten: start = m * n end = (m + 1) * n k3[start:end] = gaussian_sum else: k3[i, j, k, :] = gaussian_sum if self.flatten: k3 = k3.to_coo() return k3
def create_single( self, system, positions, ): """Return the local many-body tensor representation for the given system and positions. Args: system (:class:`ase.Atoms` | :class:`.System`): Input system. positions (iterable): Positions or atom index of points, from which local_mbtr is created. Can be a list of integer numbers or a list of xyz-coordinates. Returns: 1D ndarray: The local many-body tensor representations of given positions, for k terms, as an array. These are ordered as given in positions. """ # Transform the input system into the internal System-object system = self.get_system(system) # Check that the system does not have elements that are not in the list # of atomic numbers atomic_number_set = set(system.get_atomic_numbers()) self.check_atomic_numbers(atomic_number_set) # Ensure that the atomic number 0 is not present in the system if 0 in atomic_number_set: raise ValueError( "Please do not use the atomic number 0 in local MBTR" ", as it is reserved for the ghost atom used by the " "implementation.") # Figure out the atom index or atom location from the given positions systems = [] # Positions specified, use them if positions is not None: # Check validity of position definitions and create final cartesian # position list list_positions = [] if len(positions) == 0: raise ValueError( "The argument 'positions' should contain a non-empty set of" " atomic indices or cartesian coordinates with x, y and z " "components.") for i in positions: if np.issubdtype(type(i), np.integer): i_len = len(system) if i >= i_len or i < 0: raise ValueError( "The provided index {} is not valid for the system " "with {} atoms.".format(i, i_len)) list_positions.append(system.get_positions()[i]) elif isinstance(i, (list, tuple, np.ndarray)): if len(i) != 3: raise ValueError( "The argument 'positions' should contain a " "non-empty set of atomic indices or cartesian " "coordinates with x, y and z components.") list_positions.append(i) else: raise ValueError( "Create method requires the argument 'positions', a " "list of atom indices and/or positions.") for i_pos in positions: # Position designated as cartesian position, add a new atom at that # location with the chemical element X and place is as the first # atom in the system. The interaction limit makes sure that only # interactions of this first atom to every other atom are # considered. if isinstance(i_pos, (list, tuple, np.ndarray)): if len(i_pos) != 3: raise ValueError( "The argument 'positions' should contain a " "non-empty set of atomic indices or cartesian " "coordinates with x, y and z components.") i_pos = np.array(i_pos) i_pos = np.expand_dims(i_pos, axis=0) new_system = System('X', positions=i_pos) new_system += system # Position designated as integer, use the atom at that index as # center. For the calculation this central atoms is shifted to be # the first atom in the system, and the interaction limit makes # sure that only interactions of this first atom to every other # atom are considered. elif np.issubdtype(type(i_pos), np.integer): new_system = Atoms() center_atom = system[i_pos] new_system += center_atom new_system.set_atomic_numbers([0]) system_copy = system.copy() del system_copy[i_pos] new_system += system_copy else: raise ValueError( "Create method requires the argument 'positions', a " "list of atom indices and/or positions.") # Set the periodicity and cell to match the original system, as # they are lost in the system concatenation new_system.set_cell(system.get_cell()) new_system.set_pbc(system.get_pbc()) systems.append(new_system) # Request MBTR for each position. Return type depends on flattening and # whether a spares of dense result is requested. n_pos = len(positions) n_features = self.get_number_of_features() if self._flatten and self._sparse: data = [] cols = [] rows = [] row_offset = 0 for i, i_system in enumerate(systems): i_res = super().create_single(i_system) data.append(i_res.data) rows.append(i_res.row + row_offset) cols.append(i_res.col) # Increase the row offset row_offset += 1 # Saves the descriptors as a sparse matrix data = np.concatenate(data) rows = np.concatenate(rows) cols = np.concatenate(cols) desc = coo_matrix((data, (rows, cols)), shape=(n_pos, n_features), dtype=np.float32) else: if self._flatten and not self._sparse: desc = np.empty((n_pos, n_features), dtype=np.float32) else: desc = np.empty((n_pos), dtype='object') for i, i_system in enumerate(systems): i_desc = super().create_single(i_system) desc[i] = i_desc return desc