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