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_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_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_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 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 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 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_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